GWM : handled multiple role for user
This commit is contained in:
parent
5eb853bcfe
commit
c4d2878487
@ -131,8 +131,8 @@ Base path: `/api/v1` · Auth: `Authorization: Bearer <accessToken>` (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 |
|
||||
|
||||
---
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
|
||||
19
scripts/patch-user-roles.sql
Normal file
19
scripts/patch-user-roles.sql
Normal file
@ -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;
|
||||
10
scripts/patch-users-drop-role-id.sql
Normal file
10
scripts/patch-users-drop-role-id.sql
Normal file
@ -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;
|
||||
@ -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 }
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}`));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
}));
|
||||
};
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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(),
|
||||
|
||||
30
src/utils/userPermissions.js
Normal file
30
src/utils/userPermissions.js
Normal file
@ -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,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user