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

@@ -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
}