reels
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
129
controllers/application/account/registerMobileController.js
Normal file
129
controllers/application/account/registerMobileController.js
Normal 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
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
// کاملاً از کیف پول پوشش داده شد — اصلاً نیازی به درگاه نیست
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 {
|
||||
|
||||
220
controllers/application/subUsers/subUserController.js
Normal file
220
controllers/application/subUsers/subUserController.js
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
33
index.js
33
index.js
@@ -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' })
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
18
middlewares/requireSubUserPermission.js
Normal file
18
middlewares/requireSubUserPermission.js
Normal 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: 'شما دسترسی انجام این عملیات را ندارید'
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
32
routes/application/sub-users/index.js
Normal file
32
routes/application/sub-users/index.js
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ثبت میکنه —
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
34
utils/strongPasswordValidation.js
Normal file
34
utils/strongPasswordValidation.js
Normal 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
40
utils/subUserAuth.js
Normal 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 }
|
||||
27
utils/subUserPermissions.js
Normal file
27
utils/subUserPermissions.js
Normal 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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user