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

View File

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

View File

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

40
utils/subUserAuth.js Normal file
View File

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

View File

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

View File

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