This commit is contained in:
payacom
2026-10-01 21:12:05 +03:30
parent 806471241e
commit d4ebc20aa8
41 changed files with 1015 additions and 135 deletions

View File

@@ -302,18 +302,10 @@ const parseCourseContactInfo = (body) => {
const creatCourse = async (req, res, next) => {
try {
const token = req.header('Authorization').split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
const user = await UserModel.findById(userId)
if (!user) {
return res.status(422).json({
error: true,
message: 'کاربر یافت نشد'
})
}
// با blockCheck ست می‌شه (req.user) — برای کاربرِ زیرمجموعه، همون اکانتِ اصلیه
// (نه خودِ زیرمجموعه)، پس آکادمی درست پیدا می‌شه
const user = req.user
const userId = user._id
const academy = await AcademyModel.findOne({ userId })
if (!academy) {
@@ -388,11 +380,9 @@ const creatCourse = async (req, res, next) => {
const updateCourse = async (req, res, next) => {
try {
const token = req.header('Authorization')?.split(' ')[1]
if (!token) return res.status(401).send('Access Denied')
const decodedToken = jwt.verify(token, process.env.APP_SECRET)
const userId = decodedToken.id
// با blockCheck ست می‌شه (req.user) — برای کاربرِ زیرمجموعه، همون اکانتِ اصلیه
const user = req.user
const userId = user._id
const {
price,
@@ -422,14 +412,6 @@ const updateCourse = async (req, res, next) => {
})
}
const user = await UserModel.findById(userId)
if (!user) {
return res.status(422).json({
error: true,
message: 'کاربر یافت نشد'
})
}
const course = await CourseModel.findById(courseId)
if (!course) {
return res.status(422).json({

View File

@@ -1,5 +1,5 @@
const mongoose = require('mongoose')
const crypto = require('crypto')
const bcrypt = require('bcryptjs')
const UserModel = require('../../../models/UserModel')
const DeviceLinkSessionModel = require('../../../models/DeviceLinkSessionModel')
@@ -154,34 +154,26 @@ const pollDeviceLink = async (req, res, next) => {
}
/**
* رمزِ خودکارِ per-account برای رمزنگاری/بازگشاییِ خودکارِ بک‌آپِ کلیدِ E2EE — به‌جای
* PIN دستیِ کاربر. با یه درخواستِ احرازهویت‌شده به همین اکانت (از هر دستگاهی) قابل‌
* دریافته، پس بازیابیِ هویت روی دستگاهِ جدید بدون هیچ دخالتِ کاربری انجام می‌شه.
* اولین‌بار که خواسته بشه lazily ساخته می‌شه؛ با یه آپدیتِ شرطی (فقط اگه هنوز null
* باشه) از race بینِ دو دستگاهی که هم‌زمان اولین‌بار درخواستش می‌کنن جلوگیری می‌شه.
* کلیدِ E2EE حالا از رمزِ عبورِ خودِ کاربر مشتق می‌شه (نه یه رمزِ خودکارِ ساخته‌شده‌ی
* سرور) — این اندپوینت فقط رمزِ واردشده رو با bcrypt در برابرِ هشِ ذخیره‌شده تایید
* می‌کنه (عینِ loginWithUserName.js)، تا کلاینت قبل از اجرای PBKDF2ِ ۲۵۰هزار-تکراریِ
* محلی (برای decryptPrivateKeyFromBackup) یه فیدبکِ سریع برای «رمزِ اشتباه» داشته
* باشه؛ خودِ مشتق‌سازیِ کلید کاملاً سمتِ کلاینت انجام می‌شه، سرور هیچ‌وقت کلید رو نمی‌بینه
*/
const getOrCreateAutoBackupSecret = async (req, res, next) => {
const verifyPassword = async (req, res, next) => {
try {
const userId = req.user._id
const existing = await UserModel.findById(userId).select('e2ee_auto_backup_secret').lean()
if (existing?.e2ee_auto_backup_secret) {
return res.json({ secret: existing.e2ee_auto_backup_secret })
const { password } = req.body
if (!password || typeof password !== 'string') {
return res.status(422).json({ error: 'password is required' })
}
const candidate = crypto.randomBytes(32).toString('base64')
const updated = await UserModel.findOneAndUpdate(
{ _id: userId, e2ee_auto_backup_secret: null },
{ $set: { e2ee_auto_backup_secret: candidate } },
{ new: true }
).select('e2ee_auto_backup_secret').lean()
const user = await UserModel.findById(req.user._id).select('password').lean()
if (!user?.password) {
return res.json({ valid: false })
}
// اگه updated خالی برگشت یعنی یه درخواستِ هم‌زمانِ دیگه (دستگاهِ دیگه) بین
// خوندنِ بالا و همین آپدیت، خودش رمز رو ساخته بود — بخونش تا هر دو دستگاه دقیقاً
// یه رمزِ یکسان بگیرن، نه دوتا رمزِ متفاوت
const secret = updated?.e2ee_auto_backup_secret ||
(await UserModel.findById(userId).select('e2ee_auto_backup_secret').lean())?.e2ee_auto_backup_secret
res.json({ secret })
const valid = await bcrypt.compare(password, user.password)
res.json({ valid })
} catch (error) {
next(error)
}
@@ -243,7 +235,7 @@ module.exports = {
getDeviceLinkSession,
approveDeviceLink,
pollDeviceLink,
getOrCreateAutoBackupSecret,
verifyPassword,
putKeyBackup,
getKeyBackup,
deleteKeyBackup

View File

@@ -0,0 +1,129 @@
const UserModel = require('../../../models/UserModel')
const { OTP_VALID_MS, isOtpExpired } = require('../../../utils/otpExpiry')
const { sendSmsIrVerify, getSmsIrTemplateId } = require('../../../utils/smsIr')
function normalizeMobile (value) {
return String(value || '').replace(/\D/g, '')
}
function generateOTP () {
return Math.floor(100000 + Math.random() * 900000)
}
// کاربرانی که با گوگل/اینستاگرام ثبت‌نام کردن و شماره موبایل ندارن، از همین‌جا
// (نه فلوی لاگین) می‌تونن یه بار برای همیشه شماره‌شون رو ثبت کنن. بعد از ثبت،
// دیگه هیچ مسیرِ ویرایشی براش نیست — طبق خواسته‌ی کاربر.
const requestMobileOtp = async (req, res, next) => {
try {
if (req.user.mobile) {
return res.status(422).json({
error: true,
message: 'شماره موبایل شما قبلاً ثبت شده و قابل تغییر نیست'
})
}
const mobile = normalizeMobile(req.body.mobile)
if (!mobile || !/^09[0-9]{9}$/.test(mobile)) {
return res.status(422).json({
error: true,
message: 'فرمت شماره موبایل صحیح نیست'
})
}
const existing = await UserModel.findOne({ mobile })
if (existing) {
return res.status(422).json({
error: true,
message: 'این شماره موبایل قبلاً توسط کاربر دیگری ثبت شده است'
})
}
const otp = generateOTP()
const otpSentAt = new Date()
await UserModel.updateOne(
{ _id: req.user._id },
{ $set: { pending_mobile: mobile, otp: String(otp), otpSentAt } }
)
setTimeout(() => {
UserModel.updateOne(
{ _id: req.user._id, otp: String(otp) },
{ $set: { otp: null, otpSentAt: null, pending_mobile: null } }
).catch((error) => console.error('[requestMobileOtp] clear OTP failed:', error))
}, OTP_VALID_MS)
const smsResult = await sendSmsIrVerify({
mobile,
otp,
templateId: getSmsIrTemplateId('930719')
})
if (!smsResult.ok) {
return res.status(502).json({
error: true,
message: 'ارسال پیامک ناموفق بود. لطفاً کمی بعد دوباره تلاش کنید.'
})
}
return res.json({ success: true, message: 'کد تایید ارسال شد' })
} catch (error) {
next(error)
}
}
const verifyMobileOtp = async (req, res, next) => {
try {
const { otp } = req.body
if (!otp) {
return res.status(422).json({ error: true, message: 'کد تایید الزامی است' })
}
const user = await UserModel.findById(req.user._id)
if (!user.pending_mobile || !user.otp) {
return res.status(422).json({
error: true,
message: 'درخواستی برای ثبت شماره موبایل یافت نشد. دوباره تلاش کنید'
})
}
if (isOtpExpired(user)) {
return res.status(422).json({
error: true,
message: 'کد تایید منقضی شده است. لطفاً دوباره درخواست ارسال کد دهید'
})
}
if (String(user.otp) !== String(otp)) {
return res.status(422).json({ error: true, message: 'کد را اشتباه وارد کردید' })
}
// بینِ درخواستِ کد و تاییدش، ممکنه یه کاربرِ دیگه دقیقاً همین شماره رو گرفته باشه
const conflict = await UserModel.findOne({ mobile: user.pending_mobile })
if (conflict) {
return res.status(422).json({
error: true,
message: 'این شماره موبایل قبلاً توسط کاربر دیگری ثبت شده است'
})
}
user.mobile = user.pending_mobile
user.pending_mobile = null
user.otp = null
user.otpSentAt = null
await user.save()
return res.json({
success: true,
message: 'شماره موبایل با موفقیت ثبت شد',
mobile: user.mobile
})
} catch (error) {
next(error)
}
}
module.exports = {
requestMobileOtp,
verifyMobileOtp
}

View File

@@ -21,6 +21,7 @@ const {
resolveIdleTimeoutMs
} = require('./chatRoomHelpers')
const { opaquePreview } = require('../../../utils/e2ee')
const { needsUsernameAndPassword } = require('../../../utils/authSteps')
const DEFAULT_ROOM = {
title: 'چت روم مدستاگرام',
@@ -395,6 +396,17 @@ const sendRoomMessage = async (req, res, next) => {
})
}
// عینِ گیتِ /api/v1/chat (index.js) — بدونِ رمزِ عبور، کلیدِ E2EE نمی‌تونه
// بک‌آپ/بازیابی بشه، پس ارسالِ پیام (حتی توی روم) باید قبلش کاربرِ گوگل/
// اینستاگرام‌ـی که هنوز رمز نساخته رو مسدود کنه
if (needsUsernameAndPassword(req.user)) {
return res.status(403).json({
error: true,
type: 'needs_username_password',
message: 'برای ارسال پیام باید نام‌کاربری و رمز عبور خود را تکمیل کنید'
})
}
let fileUrl = null
if (uploaded) {
const uploadDir = path.join(__dirname, '../../../storage/chat-rooms/messages')

View File

@@ -6,7 +6,9 @@ const {
expireBlockSuspensionIfNeeded,
getBlockedAccountResponse
} = require('../../../utils/blockSuspension')
const { buildLoginResponseForUser } = require('../../../utils/loginResponse')
const { signTemp2faToken } = require('../../../utils/loginResponse')
const { readTotpState } = require('../../../utils/totpStore')
const { buildAuthResponse } = require('../../../utils/authSteps')
const INVALID_CREDENTIALS = 'نام کاربری یا کلمه عبور را اشتباه وارد کردید'
@@ -49,9 +51,67 @@ const loginWithUserName = async (req, res, next) => {
message: INVALID_CREDENTIALS
})
}
// اگر هم نام کاربری و هم رمز عبور درست بود، ارسال پیام موفقیت آمیز
// توکن همیشه با آیدیِ خودِ کاربرِ لاگین‌کننده ساخته می‌شه (چه مالکِ اصلی چه
// زیرمجموعه) — چون blockCheck/resolveActingUser هر درخواست رو از رویِ همین
// آیدی دوباره از دیتابیس می‌خونه و اگه زیرمجموعه بود، به اکانتِ اصلی resolve
// می‌کنه. اما خودِ ریسپانسِ لاگین (هویتی که کلاینت ذخیره می‌کنه) هم باید از
// همین‌جا به اکانتِ اصلی resolve بشه — وگرنه کلاینت فکر می‌کنه یه حسابِ کاملاً
// جدا و مجزاست (نه همون اکانتِ اصلی با دسترسیِ محدود)
let identityUser = user
let subUserFields = null
if (user.is_sub_user) {
if (user.sub_user_active === false) {
return res.status(403).json({
error: true,
message: 'این حساب کاربری غیرفعال شده است'
})
}
const parent = user.parent_account_id
? await UserModel.findById(user.parent_account_id)
: null
if (!parent) {
return res.status(422).json({
error: true,
message: INVALID_CREDENTIALS
})
}
identityUser = parent
subUserFields = {
is_sub_user: true,
sub_user_permissions: user.sub_user_permissions || []
}
}
const token = TokenService.sign({ id: user._id })
return res.json(await buildLoginResponseForUser(user, token))
// ۲مرحله‌ای همیشه روی خودِ کاربرِ لاگین‌کننده چک می‌شه، نه روی اکانتِ اصلی —
// وگرنه اگه صاحبِ اکانت ۲مرحله‌ای رو فعال کنه، هر زیرمجموعه‌ای موقعِ لاگین با
// رمزِ خودش گیرِ کدِ ۲مرحله‌ایِ صاحبِ اکانت می‌افتاد که اصلاً بهش دسترسی نداره.
// زیرمجموعه‌ها هیچ UIای برای تنظیمِ ۲مرحله‌ایِ خودشون ندارن، پس این چک براشون
// همیشه false می‌مونه
try {
const totpState = await readTotpState(user._id)
if (totpState?.totp_enabled && totpState?.totp_secret) {
return res.json({
requires_2fa: true,
temp_token: signTemp2faToken(user._id),
message: 'کد دو مرحله‌ای را وارد کنید'
})
}
} catch (error) {
console.error('[loginWithUserName] totp check failed:', error)
}
// buildAuthResponse مستقیم (نه buildLoginResponseForUser) چون اون دوباره خودش
// TOTP رو چک می‌کرد — این‌بار روی identityUser (اکانتِ اصلی)، که برای یه
// زیرمجموعه دقیقاً همون چکِ اشتباهی‌ای بود که بالا عمداً دورش زدیم
const response = buildAuthResponse(identityUser, token)
if (subUserFields) {
response.is_sub_user = subUserFields.is_sub_user
response.sub_user_permissions = subUserFields.sub_user_permissions
}
return res.json(response)
} catch (error) {
next(error)
}

View File

@@ -17,6 +17,7 @@ const WalletTransactionModel = require('../../../models/WalletTransactionModel')
const { PROJECT_ACCEPT_RATE, PROJECT_COMPLETION_COMMISSION_RATE, creditProjectShare } = require('../../../utils/projectCommission')
const { isVerifiedBadge } = require('../../../utils/verifyBadge')
const { resolveWalletSplit, debitWalletForPurchase } = require('../../../utils/walletSplit')
const { needsUsernameAndPassword } = require('../../../utils/authSteps')
const OFFER_COMMISSION_RATE = 0.5
@@ -108,8 +109,8 @@ const initiatePaymentWeb = async (req, res) => {
message: 'شما قبلاً از درخواست رایگان امروز استفاده کرده‌اید.'
})
}
user.daily_free_request -= 1
await user.save()
// updateOne + $inc به‌جای user.save() — رجوع کنید به توضیح مشابه در initiateOfferPaymentWeb
await UserModel.updateOne({ _id: userId }, { $inc: { daily_free_request: -1 } })
const project = await ProjectModel.findById(projectId)
if (!project) {
return res.status(404).json({ error: true, message: 'پروژه یافت نشد' })
@@ -573,7 +574,8 @@ const initiateAdvertisingPaymentWeb = async (req, res) => {
if (item.name === 'free' && item.price === 0) {
if (canUseFreeBillboard(user)) {
await consumeFreeBillboard(user)
await user.save()
// updateOne + $inc به‌جای user.save() — رجوع کنید به توضیح مشابه در initiateOfferPaymentWeb
await UserModel.updateOne({ _id: userId }, { $inc: { free_billboard_count: 1 } })
const project = await AdvertisingModel.findById(advertisingId)
if (!project) {
res.status(404).send('Advertising not found')
@@ -727,7 +729,8 @@ const republishAdvertisingPaymentWeb = async (req, res) => {
if (item.name === 'free' && item.price === 0) {
if (canUseFreeBillboard(user)) {
await consumeFreeBillboard(user)
await user.save()
// updateOne + $inc به‌جای user.save() — رجوع کنید به توضیح مشابه در initiateOfferPaymentWeb
await UserModel.updateOne({ _id: userId }, { $inc: { free_billboard_count: 1 } })
const project = await AdvertisingModel.findById(advertisingId)
if (!project) {
res.status(404).send('Advertising not found')
@@ -874,7 +877,13 @@ const initiateOfferPaymentWeb = async (req, res) => {
const receiver = await UserModel.findById(receiverId)
if (!user || !receiver) { return res.status(404).json({ error: 'User not found' }) }
const price = await getOfferPriceFromDatabase(offerType) // تومان
if (needsUsernameAndPassword(user)) {
return res.status(403).json({
error: true,
type: 'needs_username_password',
message: 'برای ثبت درخواست همکاری باید نام‌کاربری و رمز عبور خود را تکمیل کنید'
})
}
// 🎁 free offer
if (offerType === 'free' && user.monthly_free_offer > 0) {
@@ -884,8 +893,10 @@ const initiateOfferPaymentWeb = async (req, res) => {
transaction_id: null
})
user.monthly_free_offer -= 1
await user.save()
// updateOne + $inc به‌جای user.save() — save() کل سند رو ری‌ولیدیت می‌کنه و برای
// کاربرهایی که مقدار قدیمی/نامعتبر توی فیلدهای دیگه (مثلاً income_range) دارن
// با ValidationError شکست می‌خوره، حتی وقتی فقط monthly_free_offer تغییر کرده
await UserModel.updateOne({ _id: userId }, { $inc: { monthly_free_offer: -1 } })
return res.json({
type: 'free',
@@ -908,6 +919,11 @@ const initiateOfferPaymentWeb = async (req, res) => {
})
}
// نوعِ «رایگان» قیمتی توی OfferTypeModel نداره (اصلاً پرایس‌دار نیست، سهمیه‌محوره) —
// پس این لوکاپ فقط باید برای نوع‌های واقعاً پولی اجرا بشه، بعد از هر دو حالتِ بالا؛
// قبلاً بی‌قید-و-شرط بالای تابع اجرا می‌شد و برای «رایگان» با خطای «not found» throw می‌کرد
const price = await getOfferPriceFromDatabase(offerType) // تومان
const { walletAmount, gatewayAmount, fullyCovered } = await resolveWalletSplit(userId, price, useWallet)
// کاملاً از کیف پول پوشش داده شد — اصلاً نیازی به درگاه نیست

View File

@@ -14,7 +14,7 @@ function toRial (tomanAmount) {
const initiateOrderPayment = async (req, res) => {
try {
const { orderId, useWallet } = req.body
const { orderId, useWallet, platform } = req.body
if (!orderId) {
return res.status(422).json({ error: true, message: 'سفارش مشخص نشده است' })
}
@@ -71,13 +71,14 @@ const initiateOrderPayment = async (req, res) => {
return res.status(500).json({ error: true, message: 'آدرس بازگشت پرداخت تنظیم نشده است' })
}
const platformSuffix = platform ? `&platform=${encodeURIComponent(platform)}` : ''
const response = await axios.post(
'https://api.zarinpal.com/pg/v4/payment/request.json',
{
merchant_id: MERCHANT_ID,
amount: toRial(gatewayAmount),
description: `پرداخت سفارش شماره ${order._id}`,
callback_url: `${callbackBase}/payment/shop-order/callback?orderId=${order._id}`
callback_url: `${callbackBase}/payment/shop-order/callback?orderId=${order._id}${platformSuffix}`
},
{ headers: { 'Content-Type': 'application/json' } }
)
@@ -101,9 +102,18 @@ const initiateOrderPayment = async (req, res) => {
}
const handleOrderPaymentCallback = async (req, res) => {
const { orderId, Authority, Status } = req.query
const failRedirect = `${process.env.APP_SITE}/shops/payment/failed?orderId=${orderId || ''}`
const successRedirect = (id) => `${process.env.APP_SITE}/shops/payment/success?orderId=${id}`
const { orderId, Authority, Status, platform } = req.query
// اندروید: نتیجه با دیپ‌لینکِ اپ (modstagram://) برگردونده می‌شه، نه یه صفحه‌ی وب —
// عینِ همه‌ی مسیرهای پرداختِ دیگه (آفر/بیلبورد/پروژه). قبلاً این مسیر فقط به وب
// ریدایرکت می‌کرد، یعنی اپ هیچ‌وقت نمی‌فهمید پرداخت واقعاً موفق شده یا نه
const isAndroid = platform === 'android'
const failRedirect = isAndroid
? `modstagram://FailedPay?type=shop&orderId=${orderId || ''}`
: `${process.env.APP_SITE}/shops/payment/failed?orderId=${orderId || ''}`
const successRedirect = (id) =>
isAndroid
? `modstagram://SuccessPay?type=shop&orderId=${id}`
: `${process.env.APP_SITE}/shops/payment/success?orderId=${id}`
const io = req.app.get('io')

View File

@@ -6,11 +6,18 @@ const ProjectModel = require('../../../models/ProjectModel')
const AdvertisingModel = require('../../../models/AdvertisingModel')
const ShopModel = require('../../../models/ShopModel')
const ShopProductListingModel = require('../../../models/ShopProductListingModel')
const LikeModel = require('../../../models/LikeModel')
const CommentModel = require('../../../models/CommentModel')
const PostReportModel = require('../../../models/PostReportModel')
const PostBookmarkModel = require('../../../models/PostBookmarkModel')
const PostShareModel = require('../../../models/PostShareModel')
const PostSendModel = require('../../../models/PostSendModel')
const { CountryModel, ProvinceModel, CityModel } = require('../../../models/StateCity')
const { resolveLinkedEntity } = require('../../../utils/resolveLinkedEntity')
const { check, validationResult } = require('express-validator')
const jwt = require('jsonwebtoken')
const path = require('path')
const fs = require('fs-extra')
const { default: mongoose } = require('mongoose')
const { viewerIsBlockedBy } = require('../../../utils/blockVisibility')
const { attachSinglePostEngagement } = require('../../../utils/postEngagement')
@@ -564,6 +571,56 @@ const getUserPostsWeb = async (req, res, next) => {
}
}
// حذفِ کاملِ پست — غیرقابل‌بازگشت. علاوه بر خودِ پست، هرچی که بهش ارجاع می‌ده هم پاک
// می‌شه (لایک/کامنت/گزارش/بوکمارک/اشتراک/ارسال‌شده‌درچت + پین‌شده از پروفایل مالک) تا
// دیتای یتیم تو دیتابیس نمونه؛ فایل‌های فیزیکی (عکس/ویدیو) هم از دیسک حذف می‌شن
const deletePost = async (req, res, next) => {
try {
const userId = req.user?._id || req.user?.id
if (!userId) {
return res.status(401).json({ error: true, message: 'توکن ارائه نشده است' })
}
const { postId } = req.params
if (!mongoose.Types.ObjectId.isValid(postId)) {
return res.status(400).json({ error: true, message: 'شناسه پست معتبر نیست' })
}
const post = await PostModel.findById(postId)
if (!post) {
return res.status(404).json({ error: true, message: 'پست یافت نشد' })
}
if (String(post.user_id) !== String(userId)) {
return res.status(403).json({ error: true, message: 'اجازه‌ی حذف این پست را ندارید' })
}
await Promise.all([
LikeModel.deleteMany({ postId }),
CommentModel.deleteMany({ post: postId }),
PostReportModel.deleteMany({ post_id: postId }),
PostBookmarkModel.deleteMany({ postId }),
PostShareModel.deleteMany({ postId }),
PostSendModel.deleteMany({ postId }),
UserModel.updateMany(
{ pinned_post_ids: postId },
{ $pull: { pinned_post_ids: postId } }
)
])
for (const file of post.files || []) {
if (file?.path && fs.existsSync(file.path)) {
await fs.remove(file.path).catch(() => {})
}
}
await PostModel.deleteOne({ _id: postId })
return res.status(200).json({ message: 'پست با موفقیت حذف شد' })
} catch (error) {
next(error)
}
}
const getPostByIdWeb = async (req, res, next) => {
try {
const { postId } = req.params
@@ -659,6 +716,7 @@ module.exports = {
createPostValidationRules,
createPost,
updatePost,
deletePost,
getUserPosts,
getUserPostsWeb,
getPostByIdWeb

View File

@@ -0,0 +1,96 @@
/* eslint-disable camelcase */
const bcrypt = require('bcryptjs')
const { validateUsername } = require('../../../utils/usernameValidation')
const { validateStrongPassword } = require('../../../utils/strongPasswordValidation')
const { generateUsernameSuggestions } = require('../../../utils/usernameSuggestions')
const { isUsernameBlockedByTombstone } = require('../../../utils/accountLifecycle')
const { isSocialUser } = require('../../../utils/authSteps')
const UserModel = require('../../../models/UserModel')
/**
* فقط برای کاربران گوگل/اینستاگرام که هنوز نام‌کاربری یا رمز عبور ندارند —
* قبل از اولین پیام یا ثبت درخواست همکاری، هر دو فیلد را یک‌جا (اتمیک، در یک
* درخواست) تکمیل می‌کند تا وضعیت نصفه‌کاره (نام‌کاربری ثبت‌شده ولی رمز خیر)
* پیش نیاید.
*/
const completeSocialSignup = async (req, res, next) => {
try {
const user = req.user
if (!user) {
return res.status(401).json({ error: true, message: 'کاربری یافت نشد' })
}
if (!isSocialUser(user)) {
return res.status(422).json({
error: true,
message: 'این عملیات فقط برای کاربرانی است که با گوگل یا اینستاگرام وارد شده‌اند'
})
}
const { user_name, password } = req.body
// اگر کاربر از قبل نام‌کاربری دارد، فقط رمز را تنظیم می‌کنیم؛ در غیر این
// صورت هر دو باید هم‌زمان معتبر باشند تا save() صدا زده شود (اتمیک).
const needsUsername = !user.user_name
const targetUserName = needsUsername ? user_name : user.user_name
if (needsUsername) {
const usernameError = validateUsername(targetUserName)
if (usernameError) {
return res.status(422).json({ error: true, field: 'user_name', message: usernameError })
}
}
const passwordError = validateStrongPassword(password)
if (passwordError) {
return res.status(422).json({ error: true, field: 'password', message: passwordError })
}
let normalizedUserName
if (needsUsername) {
normalizedUserName = targetUserName.trim().toLowerCase()
if (await isUsernameBlockedByTombstone(normalizedUserName)) {
return res.status(422).json({
error: true,
field: 'user_name',
message: 'تا ۳۰ روز پس از حذف حساب، امکان استفاده از این نام کاربری وجود ندارد'
})
}
const existingUser = await UserModel.findOne({
user_name: { $regex: new RegExp('^' + normalizedUserName + '$', 'i') }
})
if (existingUser && existingUser._id.toString() !== user._id.toString()) {
const suggestions = await generateUsernameSuggestions(
UserModel,
normalizedUserName,
user._id.toString()
)
return res.status(422).json({
error: true,
field: 'user_name',
message: 'نام کاربری تکراری است',
suggestions
})
}
}
const hashedPassword = await bcrypt.hash(password, 10)
if (needsUsername) {
user.user_name = normalizedUserName
}
user.password = hashedPassword
await user.save()
return res.json({
message: 'نام کاربری و رمز عبور با موفقیت ثبت شد',
user_name: user.user_name
})
} catch (error) {
next(error)
}
}
module.exports = { completeSocialSignup }

View File

@@ -410,6 +410,13 @@ const getStoriesFeed = async (req, res, next) => {
if (mine) myActiveStory = mine
}
// طبق خواسته‌ی کاربر: نوار استوری فقط کاربرانی رو نشون بده که ویووَر فالو کرده —
// استوریِ خودِ کاربر جداگانه از my_story میاد (بالاتر استخراج شد)، پس اینجا از
// feed حذف می‌شه تا دوباره در قالب یه آیتمِ دیگه نشون داده نشه
feed = viewerId
? feed.filter((f) => f.user._id !== viewerId && followingSet.has(f.user._id))
: []
// تبلیغِ نوع first_story — همیشه اولِ نوار، فقط برای کاربرِ واردشده (utils/adDelivery.js)
if (viewerId) {
try {

View File

@@ -0,0 +1,220 @@
/* eslint-disable camelcase */
const fs = require('fs-extra')
const path = require('path')
const bcrypt = require('bcryptjs')
const UserModel = require('../../../models/UserModel')
const { validateUsername } = require('../../../utils/usernameValidation')
const { optimizeImageFile } = require('../../../utils/imageOptimize')
const { sanitizeSubUserPermissions } = require('../../../utils/subUserPermissions')
const AVATAR_MAX_DIMENSION = 800
const PASSWORD_REGEX = /^(?=.*[a-zA-Zآ-ی])(?=.*\d).{8,}$/u
// یه کاربرِ زیرمجموعه فقط باید صاحبِ خودش رو ببینه/ویرایش کنه — نه سایرِ
// زیرمجموعه‌های حساب‌های دیگه، و خودِ یه زیرمجموعه هم اصلاً نباید بتونه این
// مسیرها رو صدا بزنه (چک با req.actingSubUser تو خودِ روت انجام می‌شه)
async function findOwnedSubUser (ownerId, subUserId) {
const subUser = await UserModel.findOne({
_id: subUserId,
parent_account_id: ownerId,
is_sub_user: true
})
return subUser
}
async function saveAvatarIfProvided (user, files) {
const profile_image = files?.profile_image
if (!profile_image) return
const uploadDir = path.join(__dirname, '../../../storage/profiles')
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
const uniqueFileName = `${user.user_name}-${Date.now()}${path.extname(profile_image.name)}`
const filePath = path.join(uploadDir, uniqueFileName)
await fs.move(profile_image.path, filePath)
await optimizeImageFile(filePath, { maxDimension: AVATAR_MAX_DIMENSION })
user.profile_image = `/profiles/${uniqueFileName}`
}
const createSubUser = async (req, res, next) => {
try {
const { first_name, last_name, user_name, password } = req.body
let permissions = req.body.permissions
// فرم‌دیتا (چون همراه با آپلود عکسه) ممکنه permissions رو به‌صورت رشته‌ی JSON بفرسته
if (typeof permissions === 'string') {
try { permissions = JSON.parse(permissions) } catch { permissions = [] }
}
if (!first_name || !first_name.trim()) {
return res.status(422).json({ error: true, message: 'نام الزامی است' })
}
const usernameError = validateUsername(user_name)
if (usernameError) {
return res.status(422).json({ error: true, message: usernameError })
}
if (!password || !PASSWORD_REGEX.test(password)) {
return res.status(422).json({
error: true,
message: 'رمز عبور باید حداقل ۸ کاراکتر و شامل حروف و اعداد باشد'
})
}
const normalizedUsername = user_name.trim().toLowerCase()
const existing = await UserModel.findOne({ user_name: normalizedUsername })
if (existing) {
return res.status(422).json({ error: true, message: 'این نام کاربری قبلاً استفاده شده است' })
}
const subUser = new UserModel({
first_name: first_name.trim(),
last_name: (last_name || '').trim(),
user_name: normalizedUsername,
password: await bcrypt.hash(password, 10),
parent_account_id: req.user._id,
is_sub_user: true,
sub_user_active: true,
sub_user_permissions: sanitizeSubUserPermissions(permissions)
})
await saveAvatarIfProvided(subUser, req.files)
await subUser.save()
return res.json({
message: 'کاربر زیرمجموعه با موفقیت ساخته شد',
subUser: serializeSubUser(subUser)
})
} catch (error) {
next(error)
}
}
const listSubUsers = async (req, res, next) => {
try {
const subUsers = await UserModel.find({
parent_account_id: req.user._id,
is_sub_user: true
}).sort({ createdAt: -1 })
return res.json({ subUsers: subUsers.map(serializeSubUser) })
} catch (error) {
next(error)
}
}
const getSubUser = async (req, res, next) => {
try {
const subUser = await findOwnedSubUser(req.user._id, req.params.id)
if (!subUser) {
return res.status(404).json({ error: true, message: 'کاربر زیرمجموعه یافت نشد' })
}
return res.json({ subUser: serializeSubUser(subUser) })
} catch (error) {
next(error)
}
}
const updateSubUser = async (req, res, next) => {
try {
const subUser = await findOwnedSubUser(req.user._id, req.params.id)
if (!subUser) {
return res.status(404).json({ error: true, message: 'کاربر زیرمجموعه یافت نشد' })
}
const { first_name, last_name, user_name, password } = req.body
let permissions = req.body.permissions
if (typeof permissions === 'string') {
try { permissions = JSON.parse(permissions) } catch { permissions = undefined }
}
if (first_name !== undefined) {
if (!first_name.trim()) {
return res.status(422).json({ error: true, message: 'نام الزامی است' })
}
subUser.first_name = first_name.trim()
}
if (last_name !== undefined) {
subUser.last_name = last_name.trim()
}
if (user_name !== undefined) {
const usernameError = validateUsername(user_name)
if (usernameError) {
return res.status(422).json({ error: true, message: usernameError })
}
const normalizedUsername = user_name.trim().toLowerCase()
if (normalizedUsername !== subUser.user_name) {
const existing = await UserModel.findOne({ user_name: normalizedUsername })
if (existing) {
return res.status(422).json({ error: true, message: 'این نام کاربری قبلاً استفاده شده است' })
}
subUser.user_name = normalizedUsername
}
}
if (password) {
if (!PASSWORD_REGEX.test(password)) {
return res.status(422).json({
error: true,
message: 'رمز عبور باید حداقل ۸ کاراکتر و شامل حروف و اعداد باشد'
})
}
subUser.password = await bcrypt.hash(password, 10)
}
if (permissions !== undefined) {
subUser.sub_user_permissions = sanitizeSubUserPermissions(permissions)
}
await saveAvatarIfProvided(subUser, req.files)
await subUser.save()
return res.json({
message: 'اطلاعات با موفقیت به‌روزرسانی شد',
subUser: serializeSubUser(subUser)
})
} catch (error) {
next(error)
}
}
const setSubUserActive = async (req, res, next) => {
try {
const subUser = await findOwnedSubUser(req.user._id, req.params.id)
if (!subUser) {
return res.status(404).json({ error: true, message: 'کاربر زیرمجموعه یافت نشد' })
}
subUser.sub_user_active = Boolean(req.body.active)
await subUser.save()
return res.json({
message: subUser.sub_user_active ? 'کاربر فعال شد' : 'کاربر غیرفعال شد',
subUser: serializeSubUser(subUser)
})
} catch (error) {
next(error)
}
}
function serializeSubUser (subUser) {
return {
_id: subUser._id,
first_name: subUser.first_name,
last_name: subUser.last_name,
user_name: subUser.user_name,
profile_image: subUser.profile_image,
sub_user_active: subUser.sub_user_active,
sub_user_permissions: subUser.sub_user_permissions || [],
createdAt: subUser.createdAt
}
}
module.exports = {
createSubUser,
listSubUsers,
getSubUser,
updateSubUser,
setSubUserActive
}

View File

@@ -440,6 +440,12 @@ const grantGoldBadge = async (req, res, next) => {
return res.status(404).json({ message: 'کاربر مورد نظر یافت نشد' })
}
if (user.is_verified !== 'verified') {
return res.status(422).json({
message: 'ابتدا باید مدارک هویتی کاربر (تیک آبی) تایید شود'
})
}
user.gold_badge = true
syncVerifyBadge(user)
await user.save()

View File

@@ -217,6 +217,7 @@ const {
} = require('./helpers/expiredMessages')
const { purgeInactiveChatRooms } = require('./controllers/application/chatRooms/chatRoomHelpers')
const { trackOfferMessageForCommission } = require('./utils/offerReplyGate')
const { needsUsernameAndPassword } = require('./utils/authSteps')
const {
parseViewOnce,
isViewOnceMediaType,
@@ -302,6 +303,22 @@ app.post('/api/v1/chat', [blockCheck, chatPeerBlock], async (req, res) => {
return res.status(422).json({ error: 'receiverId is required' })
}
if (needsUsernameAndPassword(req.user)) {
const alreadyChatting = await MessageModel.exists({
$or: [
{ senderId, receiverId },
{ senderId: receiverId, receiverId: senderId }
]
})
if (!alreadyChatting) {
return res.status(403).json({
error: true,
type: 'needs_username_password',
message: 'برای ارسال اولین پیام باید نام‌کاربری و رمز عبور خود را تکمیل کنید'
})
}
}
const payload = {
senderId,
receiverId,
@@ -412,6 +429,22 @@ app.post('/api/v1/chat/file', [blockCheck, chatPeerBlock], async (req, res) => {
return res.status(422).json({ error: 'receiverId is required' })
}
if (needsUsernameAndPassword(req.user)) {
const alreadyChatting = await MessageModel.exists({
$or: [
{ senderId, receiverId },
{ senderId: receiverId, receiverId: senderId }
]
})
if (!alreadyChatting) {
return res.status(403).json({
error: true,
type: 'needs_username_password',
message: 'برای ارسال اولین پیام باید نام‌کاربری و رمز عبور خود را تکمیل کنید'
})
}
}
if (viewOnceFlag && !isViewOnceMediaType(resolvedFileType)) {
return res.status(422).json({ error: 'View once is only for image, video, and voice' })
}

View File

@@ -1,9 +1,9 @@
const UserModel = require('../models/UserModel')
const jwt = require('jsonwebtoken')
const {
expireBlockSuspensionIfNeeded,
getBlockedAccountResponse
} = require('../utils/blockSuspension')
const { resolveActingUser } = require('../utils/subUserAuth')
module.exports = async (req, res, next) => {
try {
@@ -27,8 +27,15 @@ module.exports = async (req, res, next) => {
})
}
let user = await UserModel.findById(decodedToken.id)
if (!user) {
const resolved = await resolveActingUser(decodedToken.id)
if (resolved.error === 'SUB_USER_DISABLED') {
return res.status(401).send({
status: 'error',
code: 401,
message: 'این حساب کاربری غیرفعال شده است'
})
}
if (resolved.error) {
return res.status(401).send({
status: 'error',
code: 401,
@@ -36,6 +43,7 @@ module.exports = async (req, res, next) => {
})
}
let user = resolved.user
user = await expireBlockSuspensionIfNeeded(user)
const blockResponse = getBlockedAccountResponse(user)
@@ -44,6 +52,7 @@ module.exports = async (req, res, next) => {
}
req.user = user
req.actingSubUser = resolved.actingSubUser
next()
} catch (error) {
console.error('Error in blockCheck middleware:', error)

View File

@@ -1,5 +1,4 @@
const bodyParser = require('body-parser')
const formData = require('express-form-data')
const session = require('express-session')
const { RedisStore } = require('connect-redis')
const { getRedisClient, getRawClient } = require('../utils/redisClient')
@@ -49,7 +48,11 @@ function dispatchSession (req, res, next) {
}
module.exports = (app) => {
app.use(formData.parse())
// express-form-data قبلاً اینجا global و بدون هیچ استثنایی نصب بود — بدنه‌ی هر
// درخواست multipart رو (پست/استوری ویدیو و هر آپلودِ دیگه) کامل می‌خوند و مصرف
// می‌کرد، درحالی‌که هیچ‌جای کدبیس از خروجیِ خودش استفاده نمی‌کرد؛ فقط باعث می‌شد
// وقتی نوبت به multer (همونی که این روت‌ها واقعاً استفاده می‌کنن) می‌رسید، دیگه
// هیچ بایتی از بدنه باقی نمونده باشه (busboy با «Unexpected end of form» شکست می‌خورد)
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(dispatchSession)

View File

@@ -1,9 +1,9 @@
const UserModel = require('../models/UserModel')
const jwt = require('jsonwebtoken')
const {
canUseProjects,
respondProjectProfileIncomplete,
} = require('../utils/projectProfile')
const { resolveActingUser } = require('../utils/subUserAuth')
/** حداقل ثبت‌نام برای پروژه: نام کاربری + نام */
module.exports = async (req, res, next) => {
@@ -28,8 +28,15 @@ module.exports = async (req, res, next) => {
})
}
const user = await UserModel.findById(decodedToken.id)
if (!user) {
const resolved = await resolveActingUser(decodedToken.id)
if (resolved.error === 'SUB_USER_DISABLED') {
return res.status(401).send({
status: 'error',
code: 401,
message: 'این حساب کاربری غیرفعال شده است',
})
}
if (resolved.error) {
return res.status(401).send({
status: 'error',
code: 401,
@@ -37,11 +44,12 @@ module.exports = async (req, res, next) => {
})
}
if (!canUseProjects(user)) {
if (!canUseProjects(resolved.user)) {
return respondProjectProfileIncomplete(res)
}
req.user = user
req.user = resolved.user
req.actingSubUser = resolved.actingSubUser
next()
} catch (error) {
console.error('isBasicRegister middleware:', error)

View File

@@ -1,5 +1,5 @@
const UserModel = require('../models/UserModel') // مسیر درست به مدل کاربر
const jwt = require('jsonwebtoken')
const { resolveActingUser } = require('../utils/subUserAuth')
module.exports = async (req, res, next) => {
try {
@@ -21,19 +21,28 @@ module.exports = async (req, res, next) => {
message: 'Your token is not valid!'
})
}
const user = await UserModel.findById(decodedToken.id)
if (!user) {
const resolved = await resolveActingUser(decodedToken.id)
if (resolved.error === 'SUB_USER_DISABLED') {
return res.status(401).send({
status: 'error',
code: 401,
message: 'این حساب کاربری غیرفعال شده است'
})
}
if (resolved.error) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'Your token is not valid!'
})
}
if (!user.user_name ||
!user.first_name ||
!user.last_name ||
!user.user_type ||
!user.password) {
const rawUser = resolved.rawUser
if (!rawUser.user_name ||
!rawUser.first_name ||
!rawUser.last_name ||
(!rawUser.is_sub_user && !rawUser.user_type) ||
!rawUser.password) {
return res.status(403).send({
status: 'error',
code: 403,
@@ -41,7 +50,8 @@ module.exports = async (req, res, next) => {
type: 'not_register'
})
}
req.user = user // کاربر را به درخواست اضافه کنید تا در سایر middleware ها و کنترلرها استفاده شود.
req.user = resolved.user // کاربر را به درخواست اضافه کنید تا در سایر middleware ها و کنترلرها استفاده شود.
req.actingSubUser = resolved.actingSubUser
next()
} catch (error) {
console.error('not_register middleware:', error)

View File

@@ -1,5 +1,5 @@
const UserModel = require('../models/UserModel') // مسیر درست به مدل کاربر
const jwt = require('jsonwebtoken')
const { resolveActingUser } = require('../utils/subUserAuth')
module.exports = async (req, res, next) => {
try {
@@ -23,18 +23,29 @@ module.exports = async (req, res, next) => {
})
}
const user = await UserModel.findById(decodedToken.id)
if (!user) {
const resolved = await resolveActingUser(decodedToken.id)
if (resolved.error === 'SUB_USER_DISABLED') {
return res.status(401).send({
status: 'error',
code: 401,
message: 'این حساب کاربری غیرفعال شده است'
})
}
if (resolved.error) {
return res.status(401).send({
status: 'error',
code: 401,
message: 'Your token is not valid!'
})
}
if (!user.user_name ||
!user.first_name ||
!user.password ||
!user.user_type) {
// برای زیرمجموعه‌ها user_type معنا نداره (مالِ خودِ اکانتِ اصلیه، نه اونا) —
// فقط برای اکانتِ اصلی این فیلد رو چک می‌کنیم
const rawUser = resolved.rawUser
if (!rawUser.user_name ||
!rawUser.first_name ||
!rawUser.password ||
(!rawUser.is_sub_user && !rawUser.user_type)) {
return res.status(403).send({
status: 'error',
code: 403,
@@ -43,7 +54,8 @@ module.exports = async (req, res, next) => {
})
}
req.user = user
req.user = resolved.user
req.actingSubUser = resolved.actingSubUser
next()
} catch (error) {
console.error('not_register middleware:', error)

View File

@@ -0,0 +1,18 @@
// مطابق middlewares/requirePermission.js (پنل ادمین) ولی برای زیرمجموعه‌های
// کاربرانِ عادی. باید بعد از میدل‌ویری که req.actingSubUser رو ست می‌کنه
// (blockCheck / isRegister / isHalfRegister / isBasicRegister) بیاد.
// اگه req.actingSubUser خالی باشه یعنی خودِ صاحبِ اکانته — دسترسیِ کامل داره.
module.exports = function requireSubUserPermission (key) {
return (req, res, next) => {
const actingSubUser = req.actingSubUser
if (!actingSubUser) return next()
if (Array.isArray(actingSubUser.permissions) && actingSubUser.permissions.includes(key)) {
return next()
}
return res.status(403).json({
status: 'error',
code: 403,
message: 'شما دسترسی انجام این عملیات را ندارید'
})
}
}

View File

@@ -22,6 +22,15 @@ const userSchema = new mongoose.Schema({
required: false,
default: null
},
// شماره‌ای که کاربرِ گوگلی/اینستاگرامیِ بدونِ موبایل، در حینِ فرایندِ تک‌مرحله‌ایِ
// «ثبت شماره موبایل» (تنظیمات) وارد کرده و منتظرِ تاییدِ OTP ـه — بعد از تایید،
// به mobile منتقل می‌شه و خودش null می‌شه
pending_mobile: {
type: String,
required: false,
trim: true,
default: null
},
email: {
type: String,
required: false,
@@ -172,6 +181,25 @@ const userSchema = new mongoose.Schema({
enum: ['none', 'gray', 'blue', 'green', 'gold'],
default: 'none'
},
// کاربرِ زیرمجموعه — یه سندِ UserModel معمولی که به‌جای اکانتِ خودش، اکانتِ
// parent_account_id رو مدیریت می‌کنه؛ فقط با پرمیژن‌های sub_user_permissions
parent_account_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'UserModel',
default: null
},
is_sub_user: {
type: Boolean,
default: false
},
sub_user_active: {
type: Boolean,
default: true
},
sub_user_permissions: {
type: [String],
default: []
},
profile_image: {
type: String,
required: false,
@@ -803,17 +831,6 @@ const userSchema = new mongoose.Schema({
required: false,
default: null
},
// یک رشته‌ی تصادفیِ ۳۲بایتیِ per-account که با درخواستِ احرازهویت‌شده به همین
// اکانت، از هر دستگاهی (وب/اندروید) قابل‌دریافته — به‌جای PIN دستی، به‌عنوانِ
// رمزِ رمزنگاریِ e2ee_key_backup استفاده می‌شه تا بازیابیِ هویتِ E2EE روی دستگاهِ
// جدید کاملاً خودکار و بدون دخالتِ کاربر انجام بشه. توجه: این یعنی تضمینِ
// end-to-end دیگه واقعی نیست — هرکسی که به این فیلد (یا سرور) دسترسی پیدا کنه
// می‌تونه بک‌آپ رو باز کنه؛ این تصمیم آگاهانه به نفعِ هیچ‌وقت‌گم‌نشدنِ پیام‌ها گرفته شده
e2ee_auto_backup_secret: {
type: String,
required: false,
default: null
},
pinned_post_ids: {
type: [{
type: mongoose.Schema.Types.ObjectId,

View File

@@ -5,6 +5,9 @@ const multer = require("multer");
const path = require("path");
const fs = require("fs");
const adminAuth = require("../../../middlewares/adminAuth");
const auth = require("../../../middlewares/auth");
const blockCheck = require("../../../middlewares/blockCheck");
const requireSubUserPermission = require("../../../middlewares/requireSubUserPermission");
// const academyCategoryController = require("../../../controllers/application/academy/academyCategoryController");
// ==========================================
@@ -79,10 +82,10 @@ router.post("/academy/profile", academyController.academyProfile);
// ==========================================
// ایجاد دوره جدید
router.post("/academy/creat/course", academyController.creatCourse);
router.post("/academy/creat/course", [auth, blockCheck, requireSubUserPermission('academy_packages')], academyController.creatCourse);
// ویرایش دوره
router.patch("/academy/update/course", academyController.updateCourse);
router.patch("/academy/update/course", [auth, blockCheck, requireSubUserPermission('academy_packages')], academyController.updateCourse);
// حذف دوره (تغییر وضعیت به reject)
router.delete("/academy/delete/course", academyController.deleteCourse);

View File

@@ -1,6 +1,7 @@
const express = require('express')
const auth = require('../../../middlewares/auth')
const blockCheck = require('../../../middlewares/blockCheck')
const authRateLimit = require('../../../middlewares/authRateLimit')
const {
deactivateAccount,
reactivateAccount,
@@ -15,6 +16,10 @@ const {
getArchivedPosts,
getAccountAnalytics
} = require('../../../controllers/application/profile/accountController')
const {
requestMobileOtp,
verifyMobileOtp
} = require('../../../controllers/application/account/registerMobileController')
const {
putMyPublicKey,
getPublicKeyByUserId,
@@ -22,7 +27,7 @@ const {
getDeviceLinkSession,
approveDeviceLink,
pollDeviceLink,
getOrCreateAutoBackupSecret,
verifyPassword,
putKeyBackup,
getKeyBackup,
deleteKeyBackup
@@ -31,6 +36,8 @@ const {
const router = express.Router()
router.get('/status', auth, blockCheck, getAccountStatus)
router.post('/mobile/request', auth, blockCheck, requestMobileOtp)
router.post('/mobile/verify', auth, blockCheck, verifyMobileOtp)
router.get('/visitors', auth, blockCheck, getProfileVisitors)
router.patch('/privacy', auth, blockCheck, updatePrivacySettings)
router.post('/reset-algorithm', auth, blockCheck, resetAlgorithm)
@@ -48,7 +55,7 @@ router.post('/e2ee/device-link', auth, blockCheck, startDeviceLink)
router.get('/e2ee/device-link/:linkId', auth, blockCheck, getDeviceLinkSession)
router.post('/e2ee/device-link/:linkId/approve', auth, blockCheck, approveDeviceLink)
router.get('/e2ee/device-link/:linkId/poll', auth, blockCheck, pollDeviceLink)
router.get('/e2ee/auto-backup-secret', auth, blockCheck, getOrCreateAutoBackupSecret)
router.post('/e2ee/verify-password', auth, blockCheck, authRateLimit, verifyPassword)
router.put('/e2ee/key-backup', auth, blockCheck, putKeyBackup)
router.get('/e2ee/key-backup', auth, blockCheck, getKeyBackup)
router.delete('/e2ee/key-backup', auth, blockCheck, deleteKeyBackup)

View File

@@ -2,6 +2,7 @@ const express = require('express')
const blockCheck = require('../../../middlewares/blockCheck')
const { createAdvertisingValidationRules, createAdvertising, getAdvertisingTypes, getAdvertisingCategory, getSingleAdvertising, getAdvertisingFeatures, getAdvertisings, toggleLike, addComment, getComments, rateAdvertising, getUserAdvertisings, getAdvertisingAnalytics, editAdvertising, getUserSingleAdvertising, getUserAdvertisingProfile, updateUserAdvertisingProfile, getAdvertisingsWeb, getSingleAdvertisingWeb, getUserAdvertisingProfileWeb, recordAdvertisingView, getUserLikedAdvertisings, getUserAdvertisingComments } = require('../../../controllers/application/advertising/advertisingController')
const auth = require('../../../middlewares/auth')
const requireSubUserPermission = require('../../../middlewares/requireSubUserPermission')
const { initiateAdvertisingPayment, handleAdvertisingPaymentCallback, handleAdvertisingRepublishPaymentCallback, republishAdvertisingPayment } = require('../../../controllers/application/payment/paymentController')
const { initiateAdvertisingPaymentWeb, handleAdvertisingPaymentCallbackWeb, republishAdvertisingPaymentWeb, handleAdvertisingRepublishPaymentCallbackWeb } = require('../../../controllers/application/payment/paymentControllerWeb')
const router = express.Router()
@@ -14,7 +15,7 @@ router.get('/user-advertising', [auth], getUserAdvertisings)
router.get('/analytics', [auth], getAdvertisingAnalytics)
router.get('/user-liked', [auth], getUserLikedAdvertisings)
router.get('/user-comments', [auth], getUserAdvertisingComments)
router.post('/create', [auth], [blockCheck], createAdvertisingValidationRules(), createAdvertising)
router.post('/create', [auth], [blockCheck], requireSubUserPermission('billboards'), createAdvertisingValidationRules(), createAdvertising)
router.get('/types', [auth], getAdvertisingTypes)
router.get('/categories', getAdvertisingCategory)
router.get('/features', [auth], getAdvertisingFeatures)
@@ -34,6 +35,6 @@ router.post('/like', [blockCheck], [auth], toggleLike)
router.post('/comment', [blockCheck], [auth], addComment)
router.get('/comments', getComments)
router.post('/rate', [blockCheck], [auth], rateAdvertising)
router.post('/edit/:id', [blockCheck], [auth], editAdvertising)
router.post('/edit/:id', [blockCheck], [auth], requireSubUserPermission('billboards'), editAdvertising)
module.exports = router

View File

@@ -21,11 +21,12 @@ const {
handleBookingPaymentCallback
} = require('../../../controllers/application/bookings/bookingPaymentController')
const blockCheck = require('../../../middlewares/blockCheck')
const requireSubUserPermission = require('../../../middlewares/requireSubUserPermission')
const router = express.Router()
// --- Booking-config wizard (provider setup) ---
router.post('/draft', [blockCheck], createDraftBookingConfig)
router.post('/draft', [blockCheck], requireSubUserPermission('bookings'), createDraftBookingConfig)
router.get('/mine', [blockCheck], getMyBookingConfigs)
// --- Reservations (buyer booking + provider's received bookings) ---
@@ -42,9 +43,9 @@ router.get('/payment-callback/web', handleBookingPaymentCallback)
// --- Booking-config wizard (continued) ---
router.get('/:id', [blockCheck], getBookingConfigById)
router.patch('/:id/services', [blockCheck], updateBookingConfigServices)
router.patch('/:id/schedule', [blockCheck], updateBookingConfigSchedule)
router.patch('/:id/status', [blockCheck], updateBookingConfigStatus)
router.post('/:id/submit', [blockCheck], submitBookingConfig)
router.patch('/:id/services', [blockCheck], requireSubUserPermission('bookings'), updateBookingConfigServices)
router.patch('/:id/schedule', [blockCheck], requireSubUserPermission('bookings'), updateBookingConfigSchedule)
router.patch('/:id/status', [blockCheck], requireSubUserPermission('bookings'), updateBookingConfigStatus)
router.post('/:id/submit', [blockCheck], requireSubUserPermission('bookings'), submitBookingConfig)
module.exports = router

View File

@@ -11,6 +11,7 @@ const {
getRoomE2eeKeys,
putRoomE2eeKeys
} = require('../../../controllers/application/chatRooms/chatRoomController')
const requireSubUserPermission = require('../../../middlewares/requireSubUserPermission')
const router = express.Router()
@@ -22,7 +23,7 @@ router.post('/:roomId/members', addRoomMembers)
router.get('/:roomId/e2ee-keys', getRoomE2eeKeys)
router.put('/:roomId/e2ee-keys', putRoomE2eeKeys)
router.get('/:roomId/messages', getRoomMessages)
router.post('/:roomId/messages', sendRoomMessage)
router.post('/:roomId/messages', requireSubUserPermission('reply_dm'), sendRoomMessage)
router.post('/:roomId/messages/reaction', reactToRoomMessage)
module.exports = router

View File

@@ -11,14 +11,15 @@ const {
reportOrder,
createReturnRequest
} = require('../../../controllers/application/orders/orderController')
const requireSubUserPermission = require('../../../middlewares/requireSubUserPermission')
const router = express.Router()
router.post('/draft', createOrderDraft)
router.get('/', getOrders)
router.get('/:id', getOrderById)
router.patch('/:id/status', updateOrderStatus)
router.patch('/:id/tracking-code', updateTrackingCode)
router.patch('/:id/status', requireSubUserPermission('order_status'), updateOrderStatus)
router.patch('/:id/tracking-code', requireSubUserPermission('order_status'), updateTrackingCode)
router.patch('/:id/confirm-receipt', confirmReceipt)
router.patch('/:id/cancel', cancelOrder)
router.post('/:id/rate', rateOrder)

View File

@@ -1,5 +1,5 @@
const express = require('express');
const { createPostValidationRules, createPost, updatePost, getUserPosts, getUserPostsWeb, getPostByIdWeb } = require('../../../controllers/application/posts/postController');
const { createPostValidationRules, createPost, updatePost, deletePost, getUserPosts, getUserPostsWeb, getPostByIdWeb } = require('../../../controllers/application/posts/postController');
const { toggleLike } = require('../../../controllers/application/posts/likeController');
const {
toggleBookmark,
@@ -15,6 +15,7 @@ const { reportPost } = require('../../../controllers/application/posts/reportCon
const blockCheck = require('../../../middlewares/blockCheck');
const auth = require('../../../middlewares/auth');
const requirePostExpertise = require('../../../middlewares/requirePostExpertise');
const requireSubUserPermission = require('../../../middlewares/requireSubUserPermission');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
@@ -121,6 +122,7 @@ router.post(
next();
},
blockCheck,
requireSubUserPermission('posts_stories'),
(req, res, next) => {
console.log('DEBUG: reached requirePostExpertise');
next();
@@ -153,6 +155,7 @@ router.post(
'/create-base64',
auth,
blockCheck,
requireSubUserPermission('posts_stories'),
requirePostExpertise,
createPostValidationRules(),
async (req, res) => {
@@ -254,6 +257,7 @@ async function handleUpdateBase64(req, res) {
}
}
router.delete('/:postId', auth, blockCheck, deletePost);
router.post('/update-base64', auth, blockCheck, handleUpdateBase64);
// مسیر جایگزین برای سرورهایی که روت قبلی را کش کرده‌اند
router.put('/update-base64', auth, blockCheck, handleUpdateBase64);

View File

@@ -8,6 +8,7 @@ const { requestProjectValidationRules, requestProject, acceptProject, editReques
const { editProject, doneProject, cancleProject, editProjectValidationRules, setRateProject, doneProjectWeb } = require('../../../controllers/application/projects/updateProjectController')
const blockCheck = require('../../../middlewares/blockCheck')
const isBasicRegister = require('../../../middlewares/isBasicRegister')
const requireSubUserPermission = require('../../../middlewares/requireSubUserPermission')
const {
initiatePaymentWeb,
handlePaymentCallbackWeb,
@@ -25,8 +26,8 @@ router.get('/get/:projectId', [auth], getSingleProjects)
router.get('/get/web/:projectId', getSingleProjectsWeb)
router.get('/payment', handlePaymentCallback)
router.get('/payment-web', handlePaymentCallbackWeb)
router.post('/create', [blockCheck], [auth], createProjectValidationRules(), createProject)
router.post('/edit', [blockCheck], [auth], editProjectValidationRules(), editProject)
router.post('/create', [blockCheck], [auth], requireSubUserPermission('projects'), createProjectValidationRules(), createProject)
router.post('/edit', [blockCheck], [auth], requireSubUserPermission('projects'), editProjectValidationRules(), editProject)
router.post('/rate', [blockCheck], [auth], [isBasicRegister], setRateProject)
router.post('/initiate-payment', [blockCheck], [isBasicRegister], [auth], initiatePayment)
router.post('/initiate-payment-web', [blockCheck], [isBasicRegister], [auth], initiatePaymentWeb)
@@ -41,9 +42,9 @@ router.get('/payment-request-step-one-callback/web', paymentRequestAcceptWebCall
router.get('/payment-request-step-two-callback', paymentRequestStepTwoCallback)
router.get('/payment-request-step-all-callback', paymentRequestStepAllCallback)
router.post('/accept', [isBasicRegister], [blockCheck], [auth], acceptProject)
router.post('/done', [isBasicRegister], [auth], doneProject)
router.post('/done/web', [isBasicRegister], [auth], doneProjectWeb)
router.post('/cancle', [isBasicRegister], [blockCheck], [auth], cancleProject)
router.post('/done', [isBasicRegister], [auth], requireSubUserPermission('projects'), doneProject)
router.post('/done/web', [isBasicRegister], [auth], requireSubUserPermission('projects'), doneProjectWeb)
router.post('/cancle', [isBasicRegister], [blockCheck], [auth], requireSubUserPermission('projects'), cancleProject)
router.post('/initiate-completion-payment/web', [isBasicRegister], [blockCheck], [auth], initiateProjectCompletionPaymentWeb)
router.get('/payment-completion-callback/web', handleProjectCompletionPaymentCallbackWeb)

View File

@@ -7,8 +7,10 @@ const { setUserFullName, setUserFullNameValidationRules } = require('../../../co
const { setUserType } = require('../../../controllers/application/register/userTypeController')
const { setRegisterPassword, setRegisterPasswordValidationRules } = require('../../../controllers/application/register/passwordController')
const { getRegisterStatus } = require('../../../controllers/application/register/statusController')
const { completeSocialSignup } = require('../../../controllers/application/register/completeSocialSignupController')
const auth = require('../../../middlewares/auth')
const authRateLimit = require('../../../middlewares/authRateLimit')
const blockCheck = require('../../../middlewares/blockCheck')
router.post('/', authRateLimit, registerValidationRules(), registerUser)
router.post('/verify', authRateLimit, verifyUser)
@@ -18,5 +20,6 @@ router.patch('/username', updateUserName)
router.post('/password', setRegisterPasswordValidationRules(), setRegisterPassword)
router.post('/fullname', setUserFullNameValidationRules(), setUserFullName)
router.post('/user_type', setUserType)
router.post('/complete-social-signup', [blockCheck], completeSocialSignup)
module.exports = router

View File

@@ -1,5 +1,6 @@
const express = require('express')
const blockCheck = require('../../../middlewares/blockCheck')
const requireSubUserPermission = require('../../../middlewares/requireSubUserPermission')
const {
searchCatalog,
createShopProduct,
@@ -24,8 +25,8 @@ router.get('/:id', getShopProductById)
router.get('/:id/comments', getShopProductComments)
// Seller-only
router.post('/', [blockCheck], createShopProduct)
router.patch('/:id', [blockCheck], updateShopProduct)
router.post('/', [blockCheck], requireSubUserPermission('shop_products'), createShopProduct)
router.patch('/:id', [blockCheck], requireSubUserPermission('shop_products'), updateShopProduct)
// Authenticated buyer actions
router.post('/:id/comments', [blockCheck], createShopProductComment)

View File

@@ -25,10 +25,11 @@ const {
toggleFavorite
} = require('../../../controllers/application/shops/favoriteController')
const blockCheck = require('../../../middlewares/blockCheck')
const requireSubUserPermission = require('../../../middlewares/requireSubUserPermission')
const router = express.Router()
router.post('/draft', [blockCheck], createDraftShop)
router.post('/draft', [blockCheck], requireSubUserPermission('create_shop'), createDraftShop)
router.get('/mine', [blockCheck], getMyShops)
router.get('/analytics', [blockCheck], getShopAnalytics)
@@ -42,13 +43,13 @@ router.post('/favorites/toggle', [blockCheck], toggleFavorite)
router.get('/public/:id', getShopPublic)
router.get('/:id', [blockCheck], getShopById)
router.patch('/:id/basic', [blockCheck], updateShopBasic)
router.patch('/:id/logo', [blockCheck], updateShopLogo)
router.patch('/:id/shipping', [blockCheck], updateShopShipping)
router.patch('/:id/location', [blockCheck], updateShopLocation)
router.patch('/:id/contact', [blockCheck], updateShopContact)
router.patch('/:id/basic', [blockCheck], requireSubUserPermission('create_shop'), updateShopBasic)
router.patch('/:id/logo', [blockCheck], requireSubUserPermission('create_shop'), updateShopLogo)
router.patch('/:id/shipping', [blockCheck], requireSubUserPermission('create_shop'), updateShopShipping)
router.patch('/:id/location', [blockCheck], requireSubUserPermission('create_shop'), updateShopLocation)
router.patch('/:id/contact', [blockCheck], requireSubUserPermission('create_shop'), updateShopContact)
router.post('/:id/follow', [blockCheck], toggleShopFollow)
router.post('/:id/submit', [blockCheck], submitShop)
router.post('/:id/submit', [blockCheck], requireSubUserPermission('create_shop'), submitShop)
router.post('/:id/send', [blockCheck], sendShopToUsers)
module.exports = router

View File

@@ -5,6 +5,7 @@ const multer = require('multer')
const auth = require('../../../middlewares/auth')
const blockCheck = require('../../../middlewares/blockCheck')
const isHalfRegister = require('../../../middlewares/isHalfRegister')
const requireSubUserPermission = require('../../../middlewares/requireSubUserPermission')
const {
applyPhotoFilter,
createStoryBase64,
@@ -45,7 +46,7 @@ const storyUpload = multer({
router.get('/feed', getStoriesFeed)
router.post('/apply-filter', auth, blockCheck, isHalfRegister, applyPhotoFilter)
router.post('/create-base64', auth, blockCheck, isHalfRegister, createStoryBase64)
router.post('/create-base64', auth, blockCheck, isHalfRegister, requireSubUserPermission('posts_stories'), createStoryBase64)
// عین /posts/create چندبخشی — برای فایل‌های حجیم (مخصوصاً ویدیو) که تبدیل به base64 توی
// حافظه‌ی JS موبایل باعث خطای OOM می‌شد؛ فایل مستقیم از دیسک استریم می‌شه
router.post(
@@ -53,6 +54,7 @@ router.post(
auth,
blockCheck,
isHalfRegister,
requireSubUserPermission('posts_stories'),
(req, res, next) => {
storyUpload(req, res, (err) => {
if (err) return res.status(400).json({ error: true, message: err.message })

View File

@@ -0,0 +1,32 @@
const express = require('express')
const router = express.Router()
const auth = require('../../../middlewares/auth')
const blockCheck = require('../../../middlewares/blockCheck')
const {
createSubUser,
listSubUsers,
getSubUser,
updateSubUser,
setSubUserActive
} = require('../../../controllers/application/subUsers/subUserController')
// یه کاربرِ زیرمجموعه هیچ‌وقت نباید بتونه خودش زیرمجموعه بسازه/مدیریت کنه —
// فقط صاحبِ اصلیِ اکانت (req.actingSubUser خالیه)
function ownerOnly (req, res, next) {
if (req.actingSubUser) {
return res.status(403).json({
error: true,
message: 'کاربر زیرمجموعه اجازه‌ی مدیریت کاربران زیرمجموعه را ندارد'
})
}
next()
}
router.post('/', [auth, blockCheck, ownerOnly], createSubUser)
router.get('/', [auth, blockCheck, ownerOnly], listSubUsers)
router.get('/:id', [auth, blockCheck, ownerOnly], getSubUser)
router.patch('/:id', [auth, blockCheck, ownerOnly], updateSubUser)
router.patch('/:id/active', [auth, blockCheck, ownerOnly], setSubUserActive)
module.exports = router

View File

@@ -12,6 +12,7 @@ const {
const blockCheck = require('../../../middlewares/blockCheck')
const isRegister = require('../../../middlewares/isRegister')
const auth = require('../../../middlewares/auth')
const requireSubUserPermission = require('../../../middlewares/requireSubUserPermission')
const router = express.Router()
router.get('/follow/stats', auth, getFollowStats)
router.get('/follow/followers', auth, getFollowers)
@@ -31,7 +32,7 @@ router.get('/get/web', getSingleUserWeb)
router.post('/block', [auth], [isRegister], [blockCheck], blockUser)
router.post('/unblock', [auth], [isRegister], [blockCheck], unblockUser)
router.get('/comments', [auth], getComments)
router.post('/comments', [auth], [isRegister], [blockCheck], createUserComment)
router.post('/comments', [auth], [isRegister], [blockCheck], requireSubUserPermission('reply_comments'), createUserComment)
router.post('/comments/like', [auth], [isRegister], [blockCheck], toggleCommentLike)
router.post('/explore/interaction', [auth], recordExploreInteraction)
router.get('/invite-employer', [auth], [isRegister], [blockCheck], inviteEmployer)

View File

@@ -38,6 +38,7 @@ const accountRouter = require('./application/account')
const giftsRouter = require('./application/gifts')
const chatRoomsRouter = require('./application/chat-rooms')
const suggestionsRouter = require('./application/suggestions')
const subUsersRouter = require('./application/sub-users')
const analyticsRouter = require('./application/analytics')
const geoRouter = require('./application/geo')
@@ -117,6 +118,7 @@ module.exports = (app) => {
app.use('/api/v1/geo', geoRouter)
app.use('/api/v1/academy', academyRoute)
app.use('/api/v1/stories', storiesRouter)
app.use('/api/v1/sub-users', subUsersRouter)
// Panel
// فقط متدهای نوشتاری (POST/PUT/PATCH/DELETE) رو خودکار توی AuditLog ثبت می‌کنه —

View File

View File

@@ -12,6 +12,11 @@ function isSocialUser (user) {
}
function isBasicRegistrationComplete (user) {
// کاربرِ زیرمجموعه همون لحظه‌ی ساخت توسطِ صاحبِ اکانت کامل ساخته می‌شه (نام،
// نام‌کاربری، رمز) — user_type براش معنا نداره، پس نباید وارد ویزاردِ ثبت‌نام بشه
if (user?.is_sub_user) {
return Boolean(user?.user_name && user?.password && user?.first_name)
}
const hasPassword = isSocialUser(user) || Boolean(user?.password)
return Boolean(
user?.user_name &&
@@ -22,6 +27,7 @@ function isBasicRegistrationComplete (user) {
}
function getRegistrationStep (user) {
if (user?.is_sub_user) return 'complete'
if (!user?.user_name) return 'user_name'
if (!isSocialUser(user) && !user.password) return 'password'
if (!user.first_name) return 'first_name'
@@ -33,6 +39,14 @@ function isRegistrationIncomplete (user) {
return !isBasicRegistrationComplete(user)
}
/**
* کاربر گوگلی/اینستاگرامی که هنوز نام‌کاربری یا رمز عبور ندارد — قبل از اولین
* پیام یا ثبت درخواست همکاری باید تکمیلش کند (مودال ست‌کردن نام‌کاربری+رمز).
*/
function needsUsernameAndPassword (user) {
return isSocialUser(user) && (!user?.user_name || !user?.password)
}
function buildAuthResponse (user, token) {
const step = getRegistrationStep(user)
const incomplete = isRegistrationIncomplete(user)
@@ -49,6 +63,8 @@ function buildAuthResponse (user, token) {
page: incomplete ? 'auth-page' : 'home',
user_type: incomplete ? null : user.user_type,
registrationComplete: !incomplete,
is_sub_user: Boolean(user.is_sub_user),
sub_user_permissions: user.is_sub_user ? (user.sub_user_permissions || []) : undefined,
}
}
@@ -59,5 +75,6 @@ module.exports = {
isBasicRegistrationComplete,
getRegistrationStep,
isRegistrationIncomplete,
needsUsernameAndPassword,
buildAuthResponse
}

View File

@@ -0,0 +1,34 @@
/**
* این پسورد پایه‌ی رمزنگاری E2EE هم هست (کلید با آن wrap می‌شود)، برای همین
* قانون قوی‌تری از پسورد معمولیِ ثبت‌نام/تغییر رمز (که فقط حرف+عدد می‌خواهد) دارد.
*/
const STRONG_PASSWORD_MIN_LENGTH = 8
const STRONG_PASSWORD_MAX_LENGTH = 64
const STRONG_PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,64}$/
const validateStrongPassword = (password) => {
if (!password || typeof password !== 'string') {
return 'رمز عبور الزامی است'
}
if (password.length < STRONG_PASSWORD_MIN_LENGTH) {
return `رمز عبور باید حداقل ${STRONG_PASSWORD_MIN_LENGTH} کاراکتر باشد`
}
if (password.length > STRONG_PASSWORD_MAX_LENGTH) {
return `رمز عبور نمی‌تواند بیشتر از ${STRONG_PASSWORD_MAX_LENGTH} کاراکتر باشد`
}
if (!STRONG_PASSWORD_REGEX.test(password)) {
return 'رمز عبور باید شامل حروف بزرگ، حروف کوچک، عدد و یک کاراکتر خاص باشد'
}
return null
}
module.exports = {
STRONG_PASSWORD_REGEX,
STRONG_PASSWORD_MIN_LENGTH,
STRONG_PASSWORD_MAX_LENGTH,
validateStrongPassword
}

40
utils/subUserAuth.js Normal file
View File

@@ -0,0 +1,40 @@
const UserModel = require('../models/UserModel')
// وقتی توکن مالِ یه کاربرِ زیرمجموعه‌ست (is_sub_user)، req.user باید اکانتِ اصلی
// (parent_account_id) باشه — نه خودِ سندِ زیرمجموعه — تا همه‌ی کنترلرهای فعلی
// (پست/کامنت/فروشگاه/...) بدونِ هیچ تغییری محتوا رو به اکانتِ اصلی نسبت بدن.
// req.actingSubUser فقط برای چکِ پرمیژن (requireSubUserPermission) استفاده می‌شه.
// چون این لوکاپ هر بار از دیتابیس تازه انجام می‌شه (نه از خودِ JWT)، غیرفعال‌کردنِ
// یه زیرمجموعه بلافاصله از درخواستِ بعدیش جلوگیری می‌کنه.
async function resolveActingUser (userId) {
const loaded = await UserModel.findById(userId)
if (!loaded) {
return { error: 'NOT_FOUND' }
}
if (!loaded.is_sub_user) {
return { user: loaded, actingSubUser: null, rawUser: loaded }
}
if (loaded.sub_user_active === false) {
return { error: 'SUB_USER_DISABLED' }
}
const parent = loaded.parent_account_id
? await UserModel.findById(loaded.parent_account_id)
: null
if (!parent) {
return { error: 'NOT_FOUND' }
}
return {
user: parent,
rawUser: loaded,
actingSubUser: {
_id: loaded._id,
permissions: loaded.sub_user_permissions || []
}
}
}
module.exports = { resolveActingUser }

View File

@@ -0,0 +1,27 @@
// کاتالوگِ پرمیژن‌هایی که صاحبِ اکانت می‌تونه به کاربرِ زیرمجموعه بده — مطابقِ
// utils/panelPermissions.js (همون الگو برای پنلِ ادمین)
const SUB_USER_PERMISSIONS = [
{ key: 'posts_stories', label: 'ثبت پست و استوری' },
{ key: 'reply_dm', label: 'جواب دادن به دایرکت' },
{ key: 'reply_comments', label: 'جواب دادن به کامنت' },
{ key: 'create_shop', label: 'ساخت فروشگاه' },
{ key: 'shop_products', label: 'ثبت و ویرایش کالا' },
{ key: 'order_status', label: 'تغییر وضعیت سفارشات' },
{ key: 'billboards', label: 'ساخت و ویرایش بیلبورد' },
{ key: 'projects', label: 'ساخت و مدیریت پروژه' },
{ key: 'academy_packages', label: 'ساخت و مدیریت پکیج آموزشی' },
{ key: 'bookings', label: 'ساخت رزرو آنلاین' }
]
const SUB_USER_PERMISSION_KEYS = new Set(SUB_USER_PERMISSIONS.map((p) => p.key))
function sanitizeSubUserPermissions (input) {
if (!Array.isArray(input)) return []
return [...new Set(input.filter((key) => SUB_USER_PERMISSION_KEYS.has(key)))]
}
module.exports = {
SUB_USER_PERMISSIONS,
SUB_USER_PERMISSION_KEYS,
sanitizeSubUserPermissions
}

View File

@@ -7,7 +7,10 @@ function isVerifiedBadge (badge) {
}
function computeVerifyBadge ({ is_verified, license_verified, gold_badge }) {
if (gold_badge) return 'gold'
// تیک طلایی فقط زمانی نمایش داده می‌شه که کاربر از قبل مدارک هویتش (تیک آبی)
// هم تایید شده باشه — وگرنه فعال بودنِ gold_badge (مثلاً به‌اشتباه یا قبل از
// تایید مدارک) نباید تیک آبیِ تازه‌تاییدشده رو بپوشونه
if (gold_badge && is_verified === 'verified') return 'gold'
if (license_verified) return 'green'
if (is_verified === 'verified') return 'blue'
if (is_verified === 'pending') return 'gray'