From c4d28784877fba9287b862f53725c1a52c7e829f Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 22 Jun 2026 11:39:28 +0530 Subject: [PATCH] GWM : handled multiple role for user --- BACKEND_TASKS.md | 4 +- prisma/schema.prisma | 20 +++- prisma/seed.js | 18 +++- scripts/patch-user-roles.sql | 19 ++++ scripts/patch-users-drop-role-id.sql | 10 ++ src/docs/completed-routes.yaml | 14 ++- src/middlewares/auth.middleware.js | 25 +++-- src/middlewares/rbac.middleware.js | 9 +- src/modules/auth/auth.service.js | 44 ++++----- src/modules/roles/roles.service.js | 16 ++-- src/modules/users/users.service.js | 130 +++++++++++++++++++++----- src/modules/users/users.validation.js | 6 +- src/utils/userPermissions.js | 30 ++++++ 13 files changed, 264 insertions(+), 81 deletions(-) create mode 100644 scripts/patch-user-roles.sql create mode 100644 scripts/patch-users-drop-role-id.sql create mode 100644 src/utils/userPermissions.js diff --git a/BACKEND_TASKS.md b/BACKEND_TASKS.md index 1c674b9..64d583f 100644 --- a/BACKEND_TASKS.md +++ b/BACKEND_TASKS.md @@ -131,8 +131,8 @@ Base path: `/api/v1` ยท Auth: `Authorization: Bearer ` (except publ | [x] | GET | `/users/export` | export | CSV export (same filters as list) | | [x] | GET | `/users` | view | Paginated list (`search`, `status`, `role_id`, `department_id`) | | [x] | GET | `/users/:id` | view | User detail | -| [x] | POST | `/users` | create | Create user (bcrypt password, mobile encrypted) | -| [x] | PUT | `/users/:id` | edit | Update user | +| [x] | POST | `/users` | create | Create user with `role_ids[]` (bcrypt password, mobile encrypted) | +| [x] | PUT | `/users/:id` | edit | Update user; optional `role_ids[]` replaces assigned roles | | [x] | DELETE | `/users/:id` | delete | Soft delete | --- diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d008a30..c9201f8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -739,6 +739,19 @@ model role_permissions { @@unique([role_id, permission_id]) } +model user_roles { + id BigInt @id @default(autoincrement()) + user_id BigInt + role_id BigInt + created_at DateTime @default(now()) @db.Timestamptz(6) + roles roles @relation(fields: [role_id], references: [id], onDelete: Restrict, onUpdate: NoAction) + users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction) + + @@unique([user_id, role_id]) + @@index([user_id], map: "idx_user_roles_user_id") + @@index([role_id], map: "idx_user_roles_role_id") +} + model roles { id BigInt @id @default(autoincrement()) name String @unique @db.VarChar(100) @@ -751,9 +764,9 @@ model roles { deleted_at DateTime? @db.Timestamptz(6) po_approvals po_approvals[] role_permissions role_permissions[] + user_roles user_roles[] users_roles_created_byTousers users? @relation("roles_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction, map: "fk_roles_created_by") users_roles_updated_byTousers users? @relation("roles_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction, map: "fk_roles_updated_by") - users_users_role_idToroles users[] @relation("users_role_idToroles") } model uom { @@ -782,7 +795,6 @@ model users { mobile String? @db.VarChar(255) mobile_index String? @db.VarChar(64) password_hash String @db.VarChar(255) - role_id BigInt? department_id BigInt? designation_id BigInt? plant_id BigInt? @@ -841,6 +853,7 @@ model users { purchase_orders_purchase_orders_created_byTousers purchase_orders[] @relation("purchase_orders_created_byTousers") purchase_orders_purchase_orders_updated_byTousers purchase_orders[] @relation("purchase_orders_updated_byTousers") refresh_tokens refresh_tokens[] + user_roles user_roles[] roles_roles_created_byTousers roles[] @relation("roles_created_byTousers") roles_roles_updated_byTousers roles[] @relation("roles_updated_byTousers") uom_uom_created_byTousers uom[] @relation("uom_created_byTousers") @@ -852,7 +865,6 @@ model users { plants_users_plant_idToplants plants? @relation("users_plant_idToplants", fields: [plant_id], references: [id], onUpdate: NoAction) users_users_reporting_toTousers users? @relation("users_reporting_toTousers", fields: [reporting_to], references: [id], onUpdate: NoAction) other_users_users_reporting_toTousers users[] @relation("users_reporting_toTousers") - roles_users_role_idToroles roles? @relation("users_role_idToroles", fields: [role_id], references: [id], onDelete: Restrict, onUpdate: NoAction) users_users_updated_byTousers users? @relation("users_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction) other_users_users_updated_byTousers users[] @relation("users_updated_byTousers") vendor_addresses_vendor_addresses_created_byTousers vendor_addresses[] @relation("vendor_addresses_created_byTousers") @@ -867,8 +879,6 @@ model users { vendors_vendors_updated_byTousers vendors[] @relation("vendors_updated_byTousers") warehouses_warehouses_created_byTousers warehouses[] @relation("warehouses_created_byTousers") warehouses_warehouses_updated_byTousers warehouses[] @relation("warehouses_updated_byTousers") - - @@index([role_id], map: "idx_users_role_id") } /// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info. diff --git a/prisma/seed.js b/prisma/seed.js index 6f60223..c17473f 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -15,10 +15,9 @@ async function main() { const passwordHash = await bcrypt.hash('Admin@123', 12); - await prisma.users.upsert({ + const user = await prisma.users.upsert({ where: { email: 'admin@bharaterp.com' }, update: { - role_id: superAdminRole.id, status: 'active', is_active: true, deleted_at: null, @@ -28,12 +27,25 @@ async function main() { full_name: 'Super Admin', email: 'admin@bharaterp.com', password_hash: passwordHash, - role_id: superAdminRole.id, status: 'active', is_active: true, }, }); + await prisma.user_roles.upsert({ + where: { + user_id_role_id: { + user_id: user.id, + role_id: superAdminRole.id, + }, + }, + update: {}, + create: { + user_id: user.id, + role_id: superAdminRole.id, + }, + }); + console.log('Bootstrap Super Admin ready: admin@bharaterp.com / Admin@123'); } diff --git a/scripts/patch-user-roles.sql b/scripts/patch-user-roles.sql new file mode 100644 index 0000000..3b07a4e --- /dev/null +++ b/scripts/patch-user-roles.sql @@ -0,0 +1,19 @@ +-- Multi-role support: user_roles junction table + backfill from users.role_id + +CREATE TABLE IF NOT EXISTS user_roles ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role_id BIGINT NOT NULL REFERENCES roles(id) ON DELETE RESTRICT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (user_id, role_id) +); + +CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id); +CREATE INDEX IF NOT EXISTS idx_user_roles_role_id ON user_roles(role_id); + +INSERT INTO user_roles (user_id, role_id) +SELECT id, role_id +FROM users +WHERE role_id IS NOT NULL + AND deleted_at IS NULL +ON CONFLICT (user_id, role_id) DO NOTHING; diff --git a/scripts/patch-users-drop-role-id.sql b/scripts/patch-users-drop-role-id.sql new file mode 100644 index 0000000..fb88387 --- /dev/null +++ b/scripts/patch-users-drop-role-id.sql @@ -0,0 +1,10 @@ +-- Remove legacy users.role_id after roles are stored in user_roles + +INSERT INTO user_roles (user_id, role_id) +SELECT id, role_id +FROM users +WHERE role_id IS NOT NULL + AND deleted_at IS NULL +ON CONFLICT (user_id, role_id) DO NOTHING; + +ALTER TABLE users DROP COLUMN IF EXISTS role_id; diff --git a/src/docs/completed-routes.yaml b/src/docs/completed-routes.yaml index 0ba8cfe..e054960 100644 --- a/src/docs/completed-routes.yaml +++ b/src/docs/completed-routes.yaml @@ -324,14 +324,18 @@ components: UsersCreateBody: type: object - required: [employee_code, full_name, email, password, role_id] + required: [employee_code, full_name, email, password, role_ids] properties: employee_code: { type: string, example: 'EMP002' } full_name: { type: string, example: 'Jane Doe' } email: { type: string, format: email, example: 'jane@bharaterp.com' } password: { type: string, minLength: 8, example: 'Admin@123' } mobile: { type: string, example: '9876543210' } - role_id: { type: integer, example: 2 } + role_ids: + type: array + minItems: 1 + items: { type: integer } + example: [2, 5] department_id: { type: integer, example: 1 } designation_id: { type: integer, example: 1 } plant_id: { type: integer, example: 1 } @@ -347,7 +351,11 @@ components: email: { type: string, format: email, example: 'jane@bharaterp.com' } password: { type: string, minLength: 8, example: 'NewPass@123' } mobile: { type: string, example: '9876543210' } - role_id: { type: integer, example: 2 } + role_ids: + type: array + minItems: 1 + items: { type: integer } + example: [2, 5] department_id: { type: integer, example: 1 } designation_id: { type: integer, example: 1 } plant_id: { type: integer, example: 1 } diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index 24dfe77..753b2e5 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -2,6 +2,19 @@ const jwt = require('jsonwebtoken'); const ApiError = require('../utils/ApiError'); const env = require('../config/env'); const prisma = require('../config/prisma'); +const { extractUserRoles } = require('../utils/userPermissions'); + +const rolePermissionsInclude = { + user_roles: { + include: { + roles: { + include: { + role_permissions: { include: { permissions: { include: { modules: true } } } }, + }, + }, + }, + }, +}; module.exports = async (req, _res, next) => { try { @@ -15,20 +28,16 @@ module.exports = async (req, _res, next) => { const user = await prisma.users.findFirst({ where: { id: BigInt(payload.sub), deleted_at: null }, - include: { - roles_users_role_idToroles: { - include: { - role_permissions: { include: { permissions: { include: { modules: true } } } }, - }, - }, - }, + include: rolePermissionsInclude, }); if (!user || !user.is_active || user.status !== 'active') { throw new ApiError(401, 'Invalid or inactive user'); } - user.role = user.roles_users_role_idToroles; + const roles = extractUserRoles(user); + user.roles = roles; + user.role = roles[0] || null; req.user = user; return next(); } catch (err) { diff --git a/src/middlewares/rbac.middleware.js b/src/middlewares/rbac.middleware.js index c53ad41..d32b201 100644 --- a/src/middlewares/rbac.middleware.js +++ b/src/middlewares/rbac.middleware.js @@ -1,9 +1,12 @@ const ApiError = require('../utils/ApiError'); const authorize = (moduleCode, action) => (req, _res, next) => { - const permissions = req.user?.role?.role_permissions || []; - const allowed = permissions.some( - (rp) => rp.permissions.modules.code === moduleCode && rp.permissions.action === action + const roles = req.user?.roles || (req.user?.role ? [req.user.role] : []); + const allowed = roles.some((role) => + (role.role_permissions || []).some( + (rp) => + rp.permissions?.modules?.code === moduleCode && rp.permissions?.action === action + ) ); if (!allowed) return next(new ApiError(403, `Forbidden: requires ${moduleCode}:${action}`)); diff --git a/src/modules/auth/auth.service.js b/src/modules/auth/auth.service.js index cf50155..0227029 100644 --- a/src/modules/auth/auth.service.js +++ b/src/modules/auth/auth.service.js @@ -5,15 +5,14 @@ const prisma = require('../../config/prisma'); const ApiError = require('../../utils/ApiError'); const env = require('../../config/env'); const { decrypt } = require('../../utils/encryption'); +const { collectUserPermissions, extractUserRoles } = require('../../utils/userPermissions'); const hashToken = (token) => crypto.createHash('sha256').update(token).digest('hex'); const issueTokens = async (user) => { - const accessToken = jwt.sign( - { sub: user.id.toString(), role_id: user.role_id?.toString() }, - env.JWT_ACCESS_SECRET, - { expiresIn: env.JWT_ACCESS_EXPIRY } - ); + const accessToken = jwt.sign({ sub: user.id.toString() }, env.JWT_ACCESS_SECRET, { + expiresIn: env.JWT_ACCESS_EXPIRY, + }); const refreshToken = crypto.randomBytes(40).toString('hex'); await prisma.refresh_tokens.create({ @@ -92,29 +91,19 @@ const logout = async (refreshToken) => { }); }; -const buildPermissionList = (rolePermissions = []) => { - const codes = rolePermissions - .filter( - (rp) => - rp.permissions?.is_active && - rp.permissions?.modules?.is_active && - rp.permissions?.modules?.code && - rp.permissions?.action - ) - .map((rp) => `${rp.permissions.modules.code}:${rp.permissions.action}`); - - return [...new Set(codes)]; -}; - const getMe = async (userId) => { const user = await prisma.users.findFirst({ where: { id: BigInt(userId), deleted_at: null }, include: { - roles_users_role_idToroles: { + user_roles: { include: { - role_permissions: { + roles: { include: { - permissions: { include: { modules: true } }, + role_permissions: { + include: { + permissions: { include: { modules: true } }, + }, + }, }, }, }, @@ -129,8 +118,12 @@ const getMe = async (userId) => { throw new ApiError(401, 'Invalid or inactive user'); } - const role = user.roles_users_role_idToroles; - const permissions = buildPermissionList(role?.role_permissions); + const roles = extractUserRoles(user).map((role) => ({ + id: role.id, + name: role.name, + description: role.description, + })); + const permissions = collectUserPermissions(user); return { id: user.id, @@ -141,7 +134,8 @@ const getMe = async (userId) => { status: user.status, is_active: user.is_active, last_login_at: user.last_login_at, - role: role ? { id: role.id, name: role.name, description: role.description } : null, + roles, + role: roles[0] || null, department: user.departments, designation: user.designations, plant: user.plants_users_plant_idToplants, diff --git a/src/modules/roles/roles.service.js b/src/modules/roles/roles.service.js index 99e9bef..a929ddc 100644 --- a/src/modules/roles/roles.service.js +++ b/src/modules/roles/roles.service.js @@ -157,7 +157,7 @@ const listRoles = async (query) => { where, include: { role_permissions: { include: permissionSelect }, - _count: { select: { users_users_role_idToroles: true } }, + _count: { select: { user_roles: true } }, }, orderBy: { created_at: 'desc' }, skip, @@ -168,7 +168,7 @@ const listRoles = async (query) => { const data = rows.map((role) => ({ ...sanitizeRole(role), - user_count: role._count.users_users_role_idToroles, + user_count: role._count.user_roles, })); return { data, meta: { page, limit, total } }; @@ -179,11 +179,11 @@ const getRoleById = async (id) => { where: { id: BigInt(id), deleted_at: null }, include: { role_permissions: { include: permissionSelect }, - _count: { select: { users_users_role_idToroles: true } }, + _count: { select: { user_roles: true } }, }, }); if (!role) throw new ApiError(404, 'Role not found'); - return { ...sanitizeRole(role), user_count: role._count.users_users_role_idToroles }; + return { ...sanitizeRole(role), user_count: role._count.user_roles }; }; const updateRole = async (id, payload, userId, requestId) => { @@ -233,8 +233,8 @@ const deleteRole = async (id, userId, requestId) => { }); if (!existing) throw new ApiError(404, 'Role not found'); - const assignedUsers = await prisma.users.count({ - where: { role_id: BigInt(id), deleted_at: null }, + const assignedUsers = await prisma.user_roles.count({ + where: { role_id: BigInt(id), users: { deleted_at: null } }, }); if (assignedUsers > 0) { throw new ApiError(400, 'Cannot delete role assigned to active users'); @@ -322,7 +322,7 @@ const listRoleCards = async () => { where: { deleted_at: null }, include: { role_permissions: { include: permissionSelect }, - _count: { select: { users_users_role_idToroles: true } }, + _count: { select: { user_roles: true } }, }, orderBy: { created_at: 'asc' }, }); @@ -332,7 +332,7 @@ const listRoleCards = async () => { name: role.name, description: role.description, is_active: role.is_active, - user_count: role._count.users_users_role_idToroles, + user_count: role._count.user_roles, permission_count: role.role_permissions.length, })); }; diff --git a/src/modules/users/users.service.js b/src/modules/users/users.service.js index 9e8571d..cf14d09 100644 --- a/src/modules/users/users.service.js +++ b/src/modules/users/users.service.js @@ -5,19 +5,27 @@ const ApiError = require('../../utils/ApiError'); const auditLog = require('../../utils/auditLog'); const { getPagination } = require('../../utils/pagination'); const { encrypt, decrypt, blindIndex } = require('../../utils/encryption'); +const { extractUserRoles } = require('../../utils/userPermissions'); const userInclude = { - roles_users_role_idToroles: { select: { id: true, name: true } }, + user_roles: { + include: { roles: { select: { id: true, name: true } } }, + }, departments: { select: { id: true, name: true } }, designations: { select: { id: true, name: true } }, plants_users_plant_idToplants: { select: { id: true, code: true, name: true } }, users_users_reporting_toTousers: { select: { id: true, full_name: true, employee_code: true } }, }; +const mapUserRoles = (user) => + extractUserRoles(user) + .map((role) => ({ id: role.id, name: role.name })) + .sort((a, b) => a.name.localeCompare(b.name)); + const sanitizeUser = (user) => { if (!user) return null; const { - roles_users_role_idToroles, + user_roles, plants_users_plant_idToplants, users_users_reporting_toTousers, departments, @@ -27,10 +35,13 @@ const sanitizeUser = (user) => { delete rest.password_hash; delete rest.mobile_index; + const roles = mapUserRoles(user); + return { ...rest, mobile: rest.mobile ? decrypt(rest.mobile) : null, - role: roles_users_role_idToroles || null, + roles, + role: roles[0] || null, department: departments || null, designation: designations || null, plant: plants_users_plant_idToplants || null, @@ -42,7 +53,13 @@ const buildUsersWhere = (query) => ({ deleted_at: null, ...(query.status ? { status: query.status } : {}), ...(query.is_active !== undefined ? { is_active: query.is_active } : {}), - ...(query.role_id ? { role_id: BigInt(query.role_id) } : {}), + ...(query.role_id + ? { + user_roles: { + some: { role_id: BigInt(query.role_id) }, + }, + } + : {}), ...(query.department_id ? { department_id: BigInt(query.department_id) } : {}), ...(query.search ? { @@ -73,6 +90,7 @@ const toUserListItem = (user) => { email: row.email, initials: toInitials(row.full_name), employee_code: row.employee_code, + roles: row.roles, role: row.role, department: row.department, plant: row.plant, @@ -82,13 +100,22 @@ const toUserListItem = (user) => { }; }; -const assertFk = async (payload) => { - if (payload.role_id) { - const role = await prisma.roles.findFirst({ - where: { id: BigInt(payload.role_id), deleted_at: null, is_active: true }, - }); - if (!role) throw new ApiError(422, 'Invalid role_id'); +const assertRoleIds = async (roleIds) => { + const uniqueIds = [...new Set((roleIds || []).map((id) => BigInt(id)))]; + if (uniqueIds.length === 0) throw new ApiError(422, 'At least one role is required'); + + const roles = await prisma.roles.findMany({ + where: { id: { in: uniqueIds }, deleted_at: null, is_active: true }, + }); + + if (roles.length !== uniqueIds.length) { + throw new ApiError(422, 'Invalid role_ids'); } + + return uniqueIds; +}; + +const assertFk = async (payload) => { if (payload.department_id) { const row = await prisma.departments.findFirst({ where: { id: BigInt(payload.department_id), deleted_at: null }, @@ -117,6 +144,7 @@ const assertFk = async (payload) => { const buildUserData = async (payload, { hashPassword = false } = {}) => { const data = { ...payload }; + delete data.role_ids; if (hashPassword && data.password) { data.password_hash = await bcrypt.hash(data.password, env.BCRYPT_SALT_ROUNDS); @@ -135,15 +163,27 @@ const buildUserData = async (payload, { hashPassword = false } = {}) => { } } - for (const key of ['role_id', 'department_id', 'designation_id', 'plant_id', 'reporting_to']) { + for (const key of ['department_id', 'designation_id', 'plant_id', 'reporting_to']) { if (data[key] !== undefined && data[key] !== null) data[key] = BigInt(data[key]); } return data; }; +const syncUserRoles = async (tx, userId, roleIds) => { + const uniqueRoleIds = await assertRoleIds(roleIds); + + await tx.user_roles.deleteMany({ where: { user_id: userId } }); + await tx.user_roles.createMany({ + data: uniqueRoleIds.map((role_id) => ({ user_id: userId, role_id })), + }); + + return uniqueRoleIds; +}; + const createUser = async (payload, userId, requestId) => { await assertFk(payload); + const roleIds = await assertRoleIds(payload.role_ids); const existing = await prisma.users.findFirst({ where: { @@ -157,19 +197,41 @@ const createUser = async (payload, userId, requestId) => { data.created_by = userId ? BigInt(userId) : null; data.updated_by = userId ? BigInt(userId) : null; - const created = await prisma.users.create({ data, include: userInclude }); + const created = await prisma.$transaction(async (tx) => { + const user = await tx.users.create({ data }); + await tx.user_roles.createMany({ + data: roleIds.map((role_id) => ({ user_id: user.id, role_id })), + }); + + return tx.users.findFirst({ + where: { id: user.id }, + include: userInclude, + }); + }); + + const sanitized = sanitizeUser(created); await auditLog({ tableName: 'users', recordId: created.id, action: 'CREATE', oldValue: null, - newValue: sanitizeUser(created), + newValue: sanitized, userId, requestId, }); - return sanitizeUser(created); + await auditLog({ + tableName: 'user_roles', + recordId: created.id, + action: 'CREATE', + oldValue: null, + newValue: roleIds.map((id) => id.toString()), + userId, + requestId, + }); + + return sanitized; }; const listUsers = async (query) => { @@ -251,7 +313,7 @@ const exportUsers = async (query) => { 'Full Name', 'Email', 'Employee Code', - 'Role', + 'Roles', 'Department', 'Plant', 'Last Login', @@ -264,7 +326,7 @@ const exportUsers = async (query) => { item.full_name, item.email, item.employee_code, - item.role?.name || '', + item.roles.map((role) => role.name).join('; '), item.department?.name || '', item.plant?.name || '', item.last_login_at ? new Date(item.last_login_at).toISOString() : '', @@ -312,23 +374,47 @@ const updateUser = async (id, payload, userId, requestId) => { const data = await buildUserData(payload, { hashPassword: Boolean(payload.password) }); data.updated_by = userId ? BigInt(userId) : null; - const updated = await prisma.users.update({ - where: { id: BigInt(id) }, - data, - include: userInclude, + const userIdBigInt = BigInt(id); + const previousRoleIds = (existing.user_roles || []).map((entry) => entry.role_id.toString()); + + const updated = await prisma.$transaction(async (tx) => { + if (payload.role_ids) { + await syncUserRoles(tx, userIdBigInt, payload.role_ids); + } + + return tx.users.update({ + where: { id: userIdBigInt }, + data, + include: userInclude, + }); }); + const sanitized = sanitizeUser(updated); + await auditLog({ tableName: 'users', recordId: id, action: 'UPDATE', oldValue: sanitizeUser(existing), - newValue: sanitizeUser(updated), + newValue: sanitized, userId, requestId, }); - return sanitizeUser(updated); + if (payload.role_ids) { + const newRoleIds = sanitized.roles.map((role) => role.id.toString()); + await auditLog({ + tableName: 'user_roles', + recordId: id, + action: 'UPDATE', + oldValue: previousRoleIds, + newValue: newRoleIds, + userId, + requestId, + }); + } + + return sanitized; }; const deleteUser = async (id, userId, requestId) => { diff --git a/src/modules/users/users.validation.js b/src/modules/users/users.validation.js index 1ea67d1..039fb0c 100644 --- a/src/modules/users/users.validation.js +++ b/src/modules/users/users.validation.js @@ -1,12 +1,14 @@ const Joi = require('joi'); +const roleIdsSchema = Joi.array().items(Joi.number().integer().positive()).min(1); + const createUserSchema = Joi.object({ employee_code: Joi.string().max(30).required(), full_name: Joi.string().max(200).required(), email: Joi.string().email().max(200).required(), password: Joi.string().min(8).required(), mobile: Joi.string().max(20).allow(null, '').optional(), - role_id: Joi.number().integer().positive().required(), + role_ids: roleIdsSchema.required(), department_id: Joi.number().integer().positive().allow(null).optional(), designation_id: Joi.number().integer().positive().allow(null).optional(), plant_id: Joi.number().integer().positive().allow(null).optional(), @@ -21,7 +23,7 @@ const updateUserSchema = Joi.object({ email: Joi.string().email().max(200).optional(), password: Joi.string().min(8).optional(), mobile: Joi.string().max(20).allow(null, '').optional(), - role_id: Joi.number().integer().positive().optional(), + role_ids: roleIdsSchema.optional(), department_id: Joi.number().integer().positive().allow(null).optional(), designation_id: Joi.number().integer().positive().allow(null).optional(), plant_id: Joi.number().integer().positive().allow(null).optional(), diff --git a/src/utils/userPermissions.js b/src/utils/userPermissions.js new file mode 100644 index 0000000..924c868 --- /dev/null +++ b/src/utils/userPermissions.js @@ -0,0 +1,30 @@ +const buildPermissionList = (rolePermissions = []) => { + const codes = rolePermissions + .filter( + (rp) => + rp.permissions?.is_active && + rp.permissions?.modules?.is_active && + rp.permissions?.modules?.code && + rp.permissions?.action + ) + .map((rp) => `${rp.permissions.modules.code}:${rp.permissions.action}`); + + return [...new Set(codes)]; +}; + +const extractUserRoles = (user) => { + if (!user?.user_roles?.length) return []; + return user.user_roles.map((entry) => entry.roles).filter(Boolean); +}; + +const collectUserPermissions = (user) => { + const roles = extractUserRoles(user); + const rolePermissions = roles.flatMap((role) => role.role_permissions || []); + return buildPermissionList(rolePermissions); +}; + +module.exports = { + buildPermissionList, + extractUserRoles, + collectUserPermissions, +};