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()
|
||||
|
||||
Reference in New Issue
Block a user