From 2b94fdb03468a1897d7c9c79f2b82e1e4b6be787 Mon Sep 17 00:00:00 2001 From: Kavehhn174 Date: Tue, 18 Aug 2026 00:08:04 +0330 Subject: [PATCH] feat: add env-based super admin authentication and complete role & permission system --- components/permission/permissionList.js | 516 +++++++++++++++++++++ components/permission/permissionModel.js | 2 +- components/permission/permissionSeeder.js | 8 +- components/permission/permissionService.js | 46 +- components/role/roleService.js | 9 +- components/user/userRoutes.js | 12 +- components/user/userService.js | 132 +++++- config/.env | 3 + globalErrorHandler/errors.json | 40 ++ middlewares/permissionMiddleware.js | 106 +++-- setup/databaseConnection.js | 4 + 11 files changed, 814 insertions(+), 64 deletions(-) create mode 100644 components/permission/permissionList.js diff --git a/components/permission/permissionList.js b/components/permission/permissionList.js new file mode 100644 index 0000000..0eac07a --- /dev/null +++ b/components/permission/permissionList.js @@ -0,0 +1,516 @@ +/** + * System-wide Permission Definitions + * Organized by functional category for the Belonging Manager platform. + */ + +const permissionsList = [ + // ─── مدیریت کاربران (User Management) ────────────────────────────────────── + { + englishName: 'View All Users', + nativeName: 'مشاهده لیست کاربران', + key: 'getallusers', + category: 'کاربران', + }, + { + englishName: 'View User Details', + nativeName: 'مشاهده جزئیات کاربر', + key: 'getuser', + category: 'کاربران', + }, + { + englishName: 'Create User', + nativeName: 'ایجاد کاربر جدید', + key: 'createuser', + category: 'کاربران', + }, + { + englishName: 'Update User', + nativeName: 'ویرایش کاربر', + key: 'updateuser', + category: 'کاربران', + }, + { + englishName: 'Delete User', + nativeName: 'حذف کاربر', + key: 'deleteuser', + category: 'کاربران', + }, + + // ─── مدیریت نقش‌ها و دسترسی‌ها (Roles & Permissions) ──────────────────────── + { + englishName: 'View All Roles', + nativeName: 'مشاهده لیست نقش‌ها', + key: 'getallroles', + category: 'نقش‌ها و دسترسی‌ها', + }, + { + englishName: 'View Role Details', + nativeName: 'مشاهده جزئیات نقش', + key: 'getarole', + category: 'نقش‌ها و دسترسی‌ها', + }, + { + englishName: 'Create Role', + nativeName: 'ایجاد نقش جدید', + key: 'createarole', + category: 'نقش‌ها و دسترسی‌ها', + }, + { + englishName: 'Update Role', + nativeName: 'ویرایش نقش و دسترسی‌ها', + key: 'updatearole', + category: 'نقش‌ها و دسترسی‌ها', + }, + { + englishName: 'Delete Role', + nativeName: 'حذف نقش', + key: 'deletearole', + category: 'نقش‌ها و دسترسی‌ها', + }, + { + englishName: 'View All Permissions', + nativeName: 'مشاهده لیست دسترسی‌ها', + key: 'getallpermissions', + category: 'نقش‌ها و دسترسی‌ها', + }, + { + englishName: 'Create Permission', + nativeName: 'ایجاد دسترسی', + key: 'createpermission', + category: 'نقش‌ها و دسترسی‌ها', + }, + { + englishName: 'View Permission', + nativeName: 'مشاهده دسترسی', + key: 'getpermission', + category: 'نقش‌ها و دسترسی‌ها', + }, + { + englishName: 'Update Permission', + nativeName: 'ویرایش دسترسی', + key: 'updatepermission', + category: 'نقش‌ها و دسترسی‌ها', + }, + { + englishName: 'Delete Permission', + nativeName: 'حذف دسترسی', + key: 'deletepermission', + category: 'نقش‌ها و دسترسی‌ها', + }, + + // ─── مدیریت املاک و اموال (Property / Belongings) ───────────────────────── + { + englishName: 'View All Properties', + nativeName: 'مشاهده لیست املاک', + key: 'getallproperties', + category: 'املاک و اموال', + }, + { + englishName: 'View Property Details', + nativeName: 'مشاهده جزئیات ملک', + key: 'getaproperty', + category: 'املاک و اموال', + }, + { + englishName: 'Create Property', + nativeName: 'ایجاد ملک جدید', + key: 'createaproperty', + category: 'املاک و اموال', + }, + { + englishName: 'Update Property', + nativeName: 'ویرایش ملک', + key: 'updateaproperty', + category: 'املاک و اموال', + }, + { + englishName: 'Delete Property', + nativeName: 'حذف ملک', + key: 'deleteaproperty', + category: 'املاک و اموال', + }, + { + englishName: 'Search Properties', + nativeName: 'جستجوی املاک', + key: 'searchproperties', + category: 'املاک و اموال', + }, + { + englishName: 'Index Properties', + nativeName: 'ایندکس‌سازی املاک', + key: 'indexproperties', + category: 'املاک و اموال', + }, + + // Aliases for Belongings + { + englishName: 'View All Belongings', + nativeName: 'مشاهده لیست اموال (سازگاری)', + key: 'getallbelongings', + category: 'املاک و اموال', + }, + { + englishName: 'View Belonging Details', + nativeName: 'مشاهده جزئیات مال (سازگاری)', + key: 'getabelonging', + category: 'املاک و اموال', + }, + { + englishName: 'Create Belonging', + nativeName: 'ایجاد مال (سازگاری)', + key: 'createabelonging', + category: 'املاک و اموال', + }, + { + englishName: 'Update Belonging', + nativeName: 'ویرایش مال (سازگاری)', + key: 'updateabelonging', + category: 'املاک و اموال', + }, + { + englishName: 'Delete Belonging', + nativeName: 'حذف مال (سازگاری)', + key: 'deleteabelonging', + category: 'املاک و اموال', + }, + { + englishName: 'Search Belongings', + nativeName: 'جستجوی اموال (سازگاری)', + key: 'searchbelongings', + category: 'املاک و اموال', + }, + { + englishName: 'Index Belongings', + nativeName: 'ایندکس‌سازی اموال (سازگاری)', + key: 'indexbelongings', + category: 'املاک و اموال', + }, + + // ─── دسته‌بندی‌ها (Categories) ───────────────────────────────────────────── + { + englishName: 'View All Categories', + nativeName: 'مشاهده لیست دسته‌بندی‌ها', + key: 'getallcategories', + category: 'دسته‌بندی‌ها', + }, + { + englishName: 'View Category Tree', + nativeName: 'مشاهده درخت دسته‌بندی‌ها', + key: 'getcategorytree', + category: 'دسته‌بندی‌ها', + }, + { + englishName: 'View Category Details', + nativeName: 'مشاهده جزئیات دسته‌بندی', + key: 'getcategory', + category: 'دسته‌بندی‌ها', + }, + { + englishName: 'Create Category', + nativeName: 'ایجاد دسته‌بندی جدید', + key: 'createcategory', + category: 'دسته‌بندی‌ها', + }, + { + englishName: 'Update Category', + nativeName: 'ویرایش دسته‌بندی', + key: 'updatecategory', + category: 'دسته‌بندی‌ها', + }, + { + englishName: 'Delete Category', + nativeName: 'حذف دسته‌بندی', + key: 'deletecategory', + category: 'دسته‌بندی‌ها', + }, + + // ─── ویژگی‌ها (Features) ────────────────────────────────────────────────── + { + englishName: 'View All Features', + nativeName: 'مشاهده ویژگی‌ها', + key: 'getallfeatures', + category: 'ویژگی‌ها', + }, + { + englishName: 'View Feature Details', + nativeName: 'مشاهده جزئیات ویژگی', + key: 'getafeature', + category: 'ویژگی‌ها', + }, + { + englishName: 'Create Feature', + nativeName: 'ایجاد ویژگی جدید', + key: 'createfeature', + category: 'ویژگی‌ها', + }, + { + englishName: 'Update Feature', + nativeName: 'ویرایش ویژگی', + key: 'updateafeature', + category: 'ویژگی‌ها', + }, + { + englishName: 'Delete Feature', + nativeName: 'حذف ویژگی', + key: 'deleteafeature', + category: 'ویژگی‌ها', + }, + + // ─── وضعیت املاک (Property Statuses) ────────────────────────────────────── + { + englishName: 'View All Property Statuses', + nativeName: 'مشاهده وضعیت‌های ملک', + key: 'getallpropertystatuses', + category: 'وضعیت‌های ملک', + }, + { + englishName: 'View Property Status Details', + nativeName: 'مشاهده جزئیات وضعیت ملک', + key: 'getapropertystatus', + category: 'وضعیت‌های ملک', + }, + { + englishName: 'Create Property Status', + nativeName: 'ایجاد وضعیت ملک جدید', + key: 'createapropertystatus', + category: 'وضعیت‌های ملک', + }, + { + englishName: 'Update Property Status', + nativeName: 'ویرایش وضعیت ملک', + key: 'updateapropertystatus', + category: 'وضعیت‌های ملک', + }, + { + englishName: 'Delete Property Status', + nativeName: 'حذف وضعیت ملک', + key: 'deleteapropertystatus', + category: 'وضعیت‌های ملک', + }, + { + englishName: 'View All Belonging Statuses', + nativeName: 'مشاهده وضعیت‌های اموال (سازگاری)', + key: 'getallbelongingstatuses', + category: 'وضعیت‌های ملک', + }, + { + englishName: 'View Belonging Status Details', + nativeName: 'مشاهده جزئیات وضعیت مال (سازگاری)', + key: 'getabelongingstatus', + category: 'وضعیت‌های ملک', + }, + { + englishName: 'Create Belonging Status', + nativeName: 'ایجاد وضعیت مال (سازگاری)', + key: 'createabelongingstatus', + category: 'وضعیت‌های ملک', + }, + { + englishName: 'Update Belonging Status', + nativeName: 'ویرایش وضعیت مال (سازگاری)', + key: 'updateabelongingstatus', + category: 'وضعیت‌های ملک', + }, + { + englishName: 'Delete Belonging Status', + nativeName: 'حذف وضعیت مال (سازگاری)', + key: 'deleteabelongingstatus', + category: 'وضعیت‌های ملک', + }, + + // ─── مدیریت انبار (Warehouse & Inventory) ────────────────────────────────── + { + englishName: 'View All Warehouses', + nativeName: 'مشاهده انبارها', + key: 'getallwarehouses', + category: 'مدیریت انبار', + }, + { + englishName: 'View Warehouse Details', + nativeName: 'مشاهده جزئیات انبار', + key: 'getwarehouse', + category: 'مدیریت انبار', + }, + { + englishName: 'Create Warehouse', + nativeName: 'ایجاد انبار جدید', + key: 'createwarehouse', + category: 'مدیریت انبار', + }, + { + englishName: 'Update Warehouse', + nativeName: 'ویرایش انبار', + key: 'updatewarehouse', + category: 'مدیریت انبار', + }, + { + englishName: 'Delete Warehouse', + nativeName: 'حذف انبار', + key: 'deletewarehouse', + category: 'مدیریت انبار', + }, + { + englishName: 'View Warehouse Items', + nativeName: 'مشاهده اقلام انبار', + key: 'getallwarehouseitems', + category: 'مدیریت انبار', + }, + { + englishName: 'View Warehouse Item Details', + nativeName: 'مشاهده جزئیات قلم انبار', + key: 'getwarehouseitem', + category: 'مدیریت انبار', + }, + { + englishName: 'Create Warehouse Item', + nativeName: 'ایجاد قلم انبار جدید', + key: 'createwarehouseitem', + category: 'مدیریت انبار', + }, + { + englishName: 'Update Warehouse Item', + nativeName: 'ویرایش قلم انبار', + key: 'updatewarehouseitem', + category: 'مدیریت انبار', + }, + { + englishName: 'Delete Warehouse Item', + nativeName: 'حذف قلم انبار', + key: 'deletewarehouseitem', + category: 'مدیریت انبار', + }, + { + englishName: 'View Warehouse Transactions', + nativeName: 'مشاهده تراکنش‌های انبار', + key: 'getallwarehousetransactions', + category: 'مدیریت انبار', + }, + { + englishName: 'Create Warehouse Transaction', + nativeName: 'ثبت تراکنش انبار', + key: 'createwarehousetransaction', + category: 'مدیریت انبار', + }, + { + englishName: 'Delete Warehouse Transaction', + nativeName: 'حذف تراکنش انبار', + key: 'deletewarehousetransaction', + category: 'مدیریت انبار', + }, + { + englishName: 'View Warehouse Stock', + nativeName: 'مشاهده موجودی انبار', + key: 'getwarehousestock', + category: 'مدیریت انبار', + }, + + // ─── مدیریت فایل‌ها (Files) ──────────────────────────────────────────────── + { + englishName: 'View All Files', + nativeName: 'مشاهده لیست فایل‌ها', + key: 'getallfiles', + category: 'مدیریت فایل‌ها', + }, + { + englishName: 'View File Details', + nativeName: 'مشاهده جزئیات فایل', + key: 'getfile', + category: 'مدیریت فایل‌ها', + }, + { + englishName: 'Create File Record', + nativeName: 'ایجاد رکورد فایل', + key: 'createfile', + category: 'مدیریت فایل‌ها', + }, + { + englishName: 'Update File', + nativeName: 'ویرایش فایل', + key: 'updatefile', + category: 'مدیریت فایل‌ها', + }, + { + englishName: 'Delete File', + nativeName: 'حذف فایل', + key: 'deletefile', + category: 'مدیریت فایل‌ها', + }, + { + englishName: 'Upload Single File', + nativeName: 'آپلود فایل تکی', + key: 'uploadsinglefile', + category: 'مدیریت فایل‌ها', + }, + { + englishName: 'Upload Multiple Files', + nativeName: 'آپلود چندتایی فایل', + key: 'uploadmultifile', + category: 'مدیریت فایل‌ها', + }, + { + englishName: 'Get Bucket Files', + nativeName: 'مشاهده فایل‌های ذخیره‌سازی ابری', + key: 'getbucketfiles', + category: 'مدیریت فایل‌ها', + }, + { + englishName: 'Get Images', + nativeName: 'مشاهده تصاویر', + key: 'getimages', + category: 'مدیریت فایل‌ها', + }, + { + englishName: 'Delete Bucket Files', + nativeName: 'حذف فایل از فضای ابری', + key: 'deletebucketfiles', + category: 'مدیریت فایل‌ها', + }, + + // ─── مدیریت تراکنش‌ها (Transactions) ──────────────────────────────────────── + { + englishName: 'View All Transactions', + nativeName: 'مشاهده تراکنش‌ها', + key: 'getalltransactions', + category: 'تراکنش‌ها', + }, + { + englishName: 'View Transaction Details', + nativeName: 'مشاهده جزئیات تراکنش', + key: 'gettransaction', + category: 'تراکنش‌ها', + }, + { + englishName: 'Create Transaction', + nativeName: 'ثبت تراکنش جدید', + key: 'createtransaction', + category: 'تراکنش‌ها', + }, + { + englishName: 'Update Transaction', + nativeName: 'ویرایش تراکنش', + key: 'updatetransaction', + category: 'تراکنش‌ها', + }, + { + englishName: 'Delete Transaction', + nativeName: 'حذف تراکنش', + key: 'deletetransaction', + category: 'تراکنش‌ها', + }, + + // ─── سیستم و داشبورد (Dashboard & System) ────────────────────────────────── + { + englishName: 'View Dashboard', + nativeName: 'مشاهده داشبورد', + key: 'viewdashboard', + category: 'سیستم و داشبورد', + }, + { + englishName: 'Admin Dashboard Access', + nativeName: 'دسترسی مدیریت داشبورد', + key: 'admindashboard', + category: 'سیستم و داشبورد', + }, +] + +module.exports = permissionsList diff --git a/components/permission/permissionModel.js b/components/permission/permissionModel.js index 4ec037e..f5e39da 100644 --- a/components/permission/permissionModel.js +++ b/components/permission/permissionModel.js @@ -25,7 +25,7 @@ const permissionSchema = new mongoose.Schema({ }) permissionSchema.pre(/^find/, function (next) { - this.find({ active: { $ne: false } }) + this.find({ isActive: { $ne: false } }) next() }) diff --git a/components/permission/permissionSeeder.js b/components/permission/permissionSeeder.js index eb00d54..4f43752 100644 --- a/components/permission/permissionSeeder.js +++ b/components/permission/permissionSeeder.js @@ -1,12 +1,16 @@ const Permission = require('./permissionModel') +const permissionsList = require('./permissionList') const chalk = require('chalk') const runSeeder = async () => { - const permissions = [] - await Permission.deleteMany({}) console.log(chalk.red('Deleted all permission records.')) + const permissions = permissionsList.map((p) => ({ + ...p, + isActive: true, + })) + await Permission.insertMany(permissions, null) console.log( chalk.green.bold( diff --git a/components/permission/permissionService.js b/components/permission/permissionService.js index 69a71ad..e847105 100644 --- a/components/permission/permissionService.js +++ b/components/permission/permissionService.js @@ -1,8 +1,52 @@ const Permission = require('./permissionModel') +const permissionsList = require('./permissionList') const AppError = require('../../globalErrorHandler/AppError') +module.exports.syncPermissions = async () => { + try { + let createdCount = 0 + let updatedCount = 0 + + for (const item of permissionsList) { + const existing = await Permission.findOne({ key: item.key }) + if (!existing) { + await Permission.create({ + englishName: item.englishName, + nativeName: item.nativeName, + key: item.key, + category: item.category, + isActive: true, + }) + createdCount++ + } else { + let changed = false + if (existing.englishName !== item.englishName) { + existing.englishName = item.englishName + changed = true + } + if (existing.nativeName !== item.nativeName) { + existing.nativeName = item.nativeName + changed = true + } + if (existing.category !== item.category) { + existing.category = item.category + changed = true + } + if (changed) { + await existing.save() + updatedCount++ + } + } + } + + console.log(`[Permissions] Sync complete: ${createdCount} created, ${updatedCount} updated.`) + } catch (error) { + console.error('[Permissions] Error during sync:', error.message) + } +} + module.exports.getAllPermissions = async () => { - return Permission.find({}) + return Permission.find({}).sort({ category: 1, englishName: 1 }) } module.exports.createPermission = async (data) => { diff --git a/components/role/roleService.js b/components/role/roleService.js index 8b6ab87..f1a94d1 100644 --- a/components/role/roleService.js +++ b/components/role/roleService.js @@ -2,15 +2,16 @@ const Role = require('./roleModel') const AppError = require('../../globalErrorHandler/AppError') module.exports.getAllRoles = async () => { - return Role.find({}, null, null) + return Role.find({}).populate('permissions').sort({ createdAt: -1 }) } module.exports.createRole = async (data) => { - return await Role.create(data) + const role = await Role.create(data) + return Role.findById(role._id).populate('permissions') } module.exports.getRole = async (roleId) => { - const role = await Role.findById(roleId) + const role = await Role.findById(roleId).populate('permissions') if (!role) { throw new AppError('NoRoleFound', 404, {}) @@ -20,7 +21,7 @@ module.exports.getRole = async (roleId) => { } module.exports.updateRole = async (roleId, data) => { - const role = await Role.findOneAndUpdate({ _id: roleId }, data, { new: true }) + const role = await Role.findOneAndUpdate({ _id: roleId }, data, { new: true }).populate('permissions') if (!role) { throw new AppError('NoRoleFound', 404, {}) diff --git a/components/user/userRoutes.js b/components/user/userRoutes.js index a626bcb..f585e32 100644 --- a/components/user/userRoutes.js +++ b/components/user/userRoutes.js @@ -2,6 +2,7 @@ const express = require('express') const userController = require('./userController') const loginCheck = require('../../middlewares/loginCheck') +const perm = require('../../middlewares/permissionMiddleware') const router = new express.Router() @@ -12,18 +13,17 @@ router.post('/login-email', userController.loginUser) router.post('/logout', userController.logoutUser) router.use(loginCheck.loginProtect) -// router.use(loginCheck.adminProtect) router.get('/user/get-self', userController.userGetSelf) -router.get('/', userController.getAllUsers) +router.get('/', perm.requires('getallusers'), userController.getAllUsers) -router.get('/:userId', userController.getUser) +router.get('/:userId', perm.requires('getuser'), userController.getUser) -router.post('/', userController.createUser) +router.post('/', perm.requires('createuser'), userController.createUser) -router.patch('/:userId', userController.updateUser) +router.patch('/:userId', perm.requires('updateuser'), userController.updateUser) -router.delete('/:userId', userController.deleteUser) +router.delete('/:userId', perm.requires('deleteuser'), userController.deleteUser) module.exports = router diff --git a/components/user/userService.js b/components/user/userService.js index 41da0b2..26171b7 100644 --- a/components/user/userService.js +++ b/components/user/userService.js @@ -1,5 +1,6 @@ // userService.js const User = require('./userModel') +const Role = require('../role/roleModel') const AppError = require('../../globalErrorHandler/AppError') const LoginAttempt = require('./loginAttemptModel') const jwt = require('jsonwebtoken') @@ -13,19 +14,19 @@ const signToken = (id) => { } const signup = async (name, email, password, passwordConfirm) => { - let userExist = await User.findOne({ email }, null, null) + let userExist = await User.findOne({ email: email.toLowerCase().trim() }, null, null) if (userExist) { - throw new AppError('UserAlreadyExist', 500, {}) + throw new AppError('UserAlreadyExist', 400, {}) } - if (password != passwordConfirm) throw new AppError('PasswordIncorrect', 401, {}) + if (password !== passwordConfirm) throw new AppError('PasswordIncorrect', 401, {}) const hashedPassword = bcrypt.hashSync(password, 10) const user = await createUser({ name, - email, + email: email.toLowerCase().trim(), password: hashedPassword, passwordConfirm: undefined, }) @@ -66,7 +67,6 @@ const loginAttempt = async (phrase) => { return } - // if user phrase(email or phone) exist return error with expire time left for login if (attemptRecord.attempts >= limit) { const limitTime = Math.max(0, Math.ceil((attemptRecord.expiresAt.getTime() - Date.now()) / 1000)) const minutes = (limitTime / 60).toFixed(0) @@ -85,56 +85,150 @@ const loginUser = async (email, password) => { throw new AppError('ProvideEmailAndPassword', 400) } - const user = await User.findOne({ email }, null, null).select('+password') + const inputPhrase = email.toLowerCase().trim() - if (email) { - await loginAttempt(email) + // ─── 1) Environment-based Super Admin Authentication ──────────────────── + const superAdminEnabled = process.env.SUPER_ADMIN_ENABLED !== 'false' + const superAdminEmail = (process.env.SUPER_ADMIN_EMAIL || process.env.SUPER_ADMIN_USERNAME || 'superadmin@mbmanager.ir').toLowerCase().trim() + const superAdminUsername = (process.env.SUPER_ADMIN_USERNAME || 'superadmin').toLowerCase().trim() + const superAdminPassword = process.env.SUPER_ADMIN_PASSWORD + + if ( + superAdminEnabled && + superAdminPassword && + (inputPhrase === superAdminEmail || inputPhrase === superAdminUsername || inputPhrase === 'superadmin') && + password === superAdminPassword + ) { + const superAdminRoleId = process.env.SUPER_ADMIN_ROLE_ID || '66a2da5b9b989a8909948848' + + // Ensure SuperAdmin role exists + let superAdminRole = await Role.findById(superAdminRoleId) + if (!superAdminRole) { + superAdminRole = await Role.findOne({ englishName: 'SuperAdmin' }) + } + if (!superAdminRole) { + superAdminRole = await Role.create({ + _id: superAdminRoleId, + englishName: 'SuperAdmin', + nativeName: 'ادمین اصلی', + permissions: [], + isActive: true, + }) + } + + // Ensure SuperAdmin user exists in DB + let superAdminUser = await User.findOne({ + $or: [{ email: superAdminEmail }, { _id: '66ddb52302f30f105de39e61' }], + }) + + if (!superAdminUser) { + superAdminUser = await User.create({ + _id: '66ddb52302f30f105de39e61', + name: 'Super Admin', + email: superAdminEmail, + password: bcrypt.hashSync(superAdminPassword, 10), + role: superAdminRole._id, + isAdmin: true, + isActive: true, + }) + } else { + superAdminUser.isAdmin = true + superAdminUser.role = superAdminRole._id + superAdminUser.isActive = true + await superAdminUser.save() + } + + const populatedUser = await User.findById(superAdminUser._id).populate('role') + return { token: signToken(populatedUser._id), user: populatedUser } } + // ─── 2) Standard Database User Authentication ──────────────────────────── + await loginAttempt(inputPhrase) + + const user = await User.findOne({ email: inputPhrase }, null, null).select('+password').populate('role') + if (!user || !(await bcrypt.compare(password, user.password))) { throw new AppError('IncorrectEmailOrPassword', 401, {}) } - // set user password to undefined before sending data + if (user.isActive === false) { + throw new AppError('UserIsInactive', 403, {}) + } + user.password = undefined return { token: signToken(user._id), user } } const getAllUsers = async () => { - const users = await User.find({}, null, null) - - return users + return User.find({}).populate('role').sort({ createdAt: -1 }) } const getUser = async (userId) => { - const user = await User.findById({ _id: userId }, null, null) + const user = await User.findById(userId).populate('role') if (!user) { - throw AppError('NoUserFound', 404, {}) + throw new AppError('NoUserFound', 404, {}) } return user } const createUser = async (data) => { - return User.create(data, null) + if (!data.email || !data.password || !data.role) { + throw new AppError('ProvideEmailPasswordRole', 400, {}) + } + + const email = data.email.toLowerCase().trim() + const existing = await User.findOne({ email }) + if (existing) { + throw new AppError('UserAlreadyExist', 400, {}) + } + + const hashedPassword = bcrypt.hashSync(data.password, 10) + + const user = await User.create({ + name: data.name, + email, + password: hashedPassword, + role: data.role, + isAdmin: data.isAdmin || false, + isActive: data.isActive !== undefined ? data.isActive : true, + }) + + return User.findById(user._id).populate('role') } const updateUser = async (userId, data) => { - const user = await User.findOneAndUpdate({ _id: userId }, data, { new: true }) + const updateData = { ...data } + + if (updateData.email) { + updateData.email = updateData.email.toLowerCase().trim() + const existing = await User.findOne({ email: updateData.email, _id: { $ne: userId } }) + if (existing) { + throw new AppError('UserAlreadyExist', 400, {}) + } + } + + if (updateData.password && updateData.password.trim().length > 0) { + updateData.password = bcrypt.hashSync(updateData.password, 10) + } else { + delete updateData.password + } + + const user = await User.findOneAndUpdate({ _id: userId }, updateData, { new: true }).populate('role') if (!user) { - throw AppError('NoUserFound', 404, {}) + throw new AppError('NoUserFound', 404, {}) } return user } const deleteUser = async (userId) => { - const user = await User.findOneAndDelete({ _id: userId }) + const user = await User.findByIdAndUpdate(userId, { isActive: false }, { new: true }) if (!user) { - throw AppError('NoUserFound', 404, {}) + throw new AppError('NoUserFound', 404, {}) } return null diff --git a/config/.env b/config/.env index 2f0f2b8..37b2b10 100644 --- a/config/.env +++ b/config/.env @@ -4,6 +4,9 @@ PORT=3000 JWT_SECRET=Y@RJ$WT!@K0NS@MPL! LOGIN_ATTEMPT_LIMIT=900 SUPER_ADMIN_ROLE_ID=66a2da5b9b989a8909948848 +SUPER_ADMIN_ENABLED=true +SUPER_ADMIN_EMAIL=superadmin@mbmanager.ir +SUPER_ADMIN_PASSWORD=SuperAdmin@123456! TYPE_SENSE_URL=localhost:8109 TYPE_SENSE_PROTOCOL=http TYPE_SENSE_API_KEY=vYQq7pTgl6ACoRy diff --git a/globalErrorHandler/errors.json b/globalErrorHandler/errors.json index ab94630..22a5e1a 100644 --- a/globalErrorHandler/errors.json +++ b/globalErrorHandler/errors.json @@ -28,5 +28,45 @@ "code": "6", "english": "You need to log in first.", "persian": "لطفا ابتدا وارد شوید." + }, + "UserAlreadyExist": { + "code": "7", + "english": "A user with this email already exists.", + "persian": "کاربری با این آدرس ایمیل قبلاً ثبت شده است." + }, + "NoPermissionAccess": { + "code": "8", + "english": "You do not have permission to perform this action.", + "persian": "شما دسترسی لازم برای انجام این عملیات را ندارید." + }, + "ProvideEmailPasswordRole": { + "code": "9", + "english": "Please provide email, password, and role.", + "persian": "لطفاً ایمیل، رمز عبور و نقش کاربر را مشخص کنید." + }, + "ProvideEmailAndPassword": { + "code": "10", + "english": "Please provide email and password.", + "persian": "لطفاً ایمیل و رمز عبور را وارد کنید." + }, + "IncorrectEmailOrPassword": { + "code": "11", + "english": "Incorrect email or password.", + "persian": "ایمیل یا رمز عبور اشتباه است." + }, + "UserIsInactive": { + "code": "12", + "english": "Your account has been deactivated.", + "persian": "حساب کاربری شما غیرفعال شده است." + }, + "NoRoleFound": { + "code": "13", + "english": "No role found with that ID.", + "persian": "نقش مورد نظر یافت نشد." + }, + "NoPermissionFound": { + "code": "14", + "english": "No permission found with that ID.", + "persian": "دسترسی مورد نظر یافت نشد." } } \ No newline at end of file diff --git a/middlewares/permissionMiddleware.js b/middlewares/permissionMiddleware.js index 0561fb4..47e7403 100644 --- a/middlewares/permissionMiddleware.js +++ b/middlewares/permissionMiddleware.js @@ -1,30 +1,73 @@ const Permission = require('../components/permission/permissionModel') const Role = require('../components/role/roleModel') const AppError = require('../globalErrorHandler/AppError') -const catchAsync = require('../globalErrorHandler/catchAsync') const User = require('../components/user/userModel') const ErrorLocalization = require('../globalErrorHandler/errorLocalization') const el = new ErrorLocalization() -let checker = (arr, target) => arr.includes(target) +let checker = (arr, target) => { + if (!Array.isArray(arr)) return false + return arr.some((item) => { + const id = (item?._id || item)?.toString() + const key = item?.key + return id === target?.toString() || (key && key === target) + }) +} const requires = (...permissions) => { return async (req, res, next) => { - const role = await Role.findById(req.user.role) - const perm = await Permission.findOne({ key: permissions[0] }) - const rolePerm = role?.permissions || [] - const userPerm = req.user.extraPermissions || [] + try { + if (!req.user) { + return next(new AppError('NotLoggedIn', 401, {})) + } - // some features: permission denied when all roles are not same --> we want to access user with either roles - if ( - !checker(rolePerm, perm?._id) && - !checker(userPerm, perm?._id) && - role._id.toString() !== process.env.SUPER_ADMIN_ROLE_ID - ) { - throw new AppError('NoPermissionAccess', 403, {}) + const superAdminRoleId = process.env.SUPER_ADMIN_ROLE_ID || '66a2da5b9b989a8909948848' + + // Super Admin bypass: full access to all endpoints + if ( + req.user.isAdmin === true || + req.user.role?.englishName === 'SuperAdmin' || + req.user.role?._id?.toString() === superAdminRoleId || + req.user.role?.toString() === superAdminRoleId + ) { + return next() + } + + // Retrieve role and its permissions + let role = req.user.role + if (role && (typeof role === 'string' || !role.permissions || !role.englishName)) { + role = await Role.findById(role._id || role).populate('permissions') + } + + const rolePermissions = role?.permissions || [] + const userExtraPermissions = req.user.extraPermissions || [] + + // Find target permissions from DB + const targetPerms = await Permission.find({ key: { $in: permissions } }) + const targetIds = targetPerms.map((p) => p._id.toString()) + const targetKeys = permissions + + // Check if any required permission is present in role or user extra permissions + const hasPermission = + rolePermissions.some((rp) => { + const rpId = (rp._id || rp).toString() + const rpKey = rp.key + return targetIds.includes(rpId) || targetKeys.includes(rpKey) + }) || + userExtraPermissions.some((up) => { + const upId = (up._id || up).toString() + const upKey = up.key + return targetIds.includes(upId) || targetKeys.includes(upKey) + }) + + if (!hasPermission) { + return next(new AppError('NoPermissionAccess', 403, {})) + } + + next() + } catch (err) { + next(err) } - - next() } } @@ -36,23 +79,26 @@ const adminCheck = () => { }).populate('role') if (!user) { - throw new AppError('UserDoesNotExist', 403, {}) + return next(new AppError('UserDoesNotExist', 403, {})) } - const perm = await Permission.findOne( - { key: 'admindashboard' }, - null, - null - ) + const superAdminRoleId = process.env.SUPER_ADMIN_ROLE_ID || '66a2da5b9b989a8909948848' + + if ( + user.isAdmin === true || + user.role?.englishName === 'SuperAdmin' || + user.role?._id?.toString() === superAdminRoleId || + user.role?.toString() === superAdminRoleId + ) { + return next() + } + + const perm = await Permission.findOne({ key: 'admindashboard' }) let rolePerm = user.role?.permissions || [] let userPerm = user.extraPermissions || [] - // some features: permission denied when all roles are not same --> we want to access user with either roles - if ( - !checker(rolePerm, perm?._id) && - !checker(userPerm, perm?._id) && - user.role?._id.toString() !== process.env.SUPER_ADMIN_ROLE_ID - ) { - throw new AppError('NoPermissionAccess', 403, {}) + + if (!checker(rolePerm, perm?._id) && !checker(userPerm, perm?._id)) { + return next(new AppError('NoPermissionAccess', 403, {})) } next() @@ -61,9 +107,7 @@ const adminCheck = () => { err.message = el.getErrorText(err.name, err.parameters, 'persian') res.status(500).json({ status: 'error', - message: - err.message || - 'An error occurred while checking permissions.', + message: err.message || 'An error occurred while checking permissions.', }) } } diff --git a/setup/databaseConnection.js b/setup/databaseConnection.js index aaee673..d094fb7 100644 --- a/setup/databaseConnection.js +++ b/setup/databaseConnection.js @@ -19,6 +19,10 @@ module.exports = (app) => { // Create a document to force MongoDB to actually create the DB and collection await MyModel.create({ name: 'test' }) + // Auto-sync permissions defined in permissionList.js + const { syncPermissions } = require('../components/permission/permissionService') + await syncPermissions() + app.listen(port, function () { console.log('Server Started On Port ' + port) })