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/export` | export | CSV export (same filters as list) |
|
||||||
| [x] | GET | `/users` | view | Paginated list (`search`, `status`, `role_id`, `department_id`) |
|
| [x] | GET | `/users` | view | Paginated list (`search`, `status`, `role_id`, `department_id`) |
|
||||||
| [x] | GET | `/users/:id` | view | User detail |
|
| [x] | GET | `/users/:id` | view | User detail |
|
||||||
| [x] | POST | `/users` | create | Create user (bcrypt password, mobile encrypted) |
|
| [x] | POST | `/users` | create | Create user with `role_ids[]` (bcrypt password, mobile encrypted) |
|
||||||
| [x] | PUT | `/users/:id` | edit | Update user |
|
| [x] | PUT | `/users/:id` | edit | Update user; optional `role_ids[]` replaces assigned roles |
|
||||||
| [x] | DELETE | `/users/:id` | delete | Soft delete |
|
| [x] | DELETE | `/users/:id` | delete | Soft delete |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -739,6 +739,19 @@ model role_permissions {
|
|||||||
@@unique([role_id, permission_id])
|
@@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 {
|
model roles {
|
||||||
id BigInt @id @default(autoincrement())
|
id BigInt @id @default(autoincrement())
|
||||||
name String @unique @db.VarChar(100)
|
name String @unique @db.VarChar(100)
|
||||||
@ -751,9 +764,9 @@ model roles {
|
|||||||
deleted_at DateTime? @db.Timestamptz(6)
|
deleted_at DateTime? @db.Timestamptz(6)
|
||||||
po_approvals po_approvals[]
|
po_approvals po_approvals[]
|
||||||
role_permissions role_permissions[]
|
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_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_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 {
|
model uom {
|
||||||
@ -782,7 +795,6 @@ model users {
|
|||||||
mobile String? @db.VarChar(255)
|
mobile String? @db.VarChar(255)
|
||||||
mobile_index String? @db.VarChar(64)
|
mobile_index String? @db.VarChar(64)
|
||||||
password_hash String @db.VarChar(255)
|
password_hash String @db.VarChar(255)
|
||||||
role_id BigInt?
|
|
||||||
department_id BigInt?
|
department_id BigInt?
|
||||||
designation_id BigInt?
|
designation_id BigInt?
|
||||||
plant_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_created_byTousers purchase_orders[] @relation("purchase_orders_created_byTousers")
|
||||||
purchase_orders_purchase_orders_updated_byTousers purchase_orders[] @relation("purchase_orders_updated_byTousers")
|
purchase_orders_purchase_orders_updated_byTousers purchase_orders[] @relation("purchase_orders_updated_byTousers")
|
||||||
refresh_tokens refresh_tokens[]
|
refresh_tokens refresh_tokens[]
|
||||||
|
user_roles user_roles[]
|
||||||
roles_roles_created_byTousers roles[] @relation("roles_created_byTousers")
|
roles_roles_created_byTousers roles[] @relation("roles_created_byTousers")
|
||||||
roles_roles_updated_byTousers roles[] @relation("roles_updated_byTousers")
|
roles_roles_updated_byTousers roles[] @relation("roles_updated_byTousers")
|
||||||
uom_uom_created_byTousers uom[] @relation("uom_created_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)
|
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)
|
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")
|
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)
|
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")
|
other_users_users_updated_byTousers users[] @relation("users_updated_byTousers")
|
||||||
vendor_addresses_vendor_addresses_created_byTousers vendor_addresses[] @relation("vendor_addresses_created_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")
|
vendors_vendors_updated_byTousers vendors[] @relation("vendors_updated_byTousers")
|
||||||
warehouses_warehouses_created_byTousers warehouses[] @relation("warehouses_created_byTousers")
|
warehouses_warehouses_created_byTousers warehouses[] @relation("warehouses_created_byTousers")
|
||||||
warehouses_warehouses_updated_byTousers warehouses[] @relation("warehouses_updated_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.
|
/// 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);
|
const passwordHash = await bcrypt.hash('Admin@123', 12);
|
||||||
|
|
||||||
await prisma.users.upsert({
|
const user = await prisma.users.upsert({
|
||||||
where: { email: 'admin@bharaterp.com' },
|
where: { email: 'admin@bharaterp.com' },
|
||||||
update: {
|
update: {
|
||||||
role_id: superAdminRole.id,
|
|
||||||
status: 'active',
|
status: 'active',
|
||||||
is_active: true,
|
is_active: true,
|
||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
@ -28,12 +27,25 @@ async function main() {
|
|||||||
full_name: 'Super Admin',
|
full_name: 'Super Admin',
|
||||||
email: 'admin@bharaterp.com',
|
email: 'admin@bharaterp.com',
|
||||||
password_hash: passwordHash,
|
password_hash: passwordHash,
|
||||||
role_id: superAdminRole.id,
|
|
||||||
status: 'active',
|
status: 'active',
|
||||||
is_active: true,
|
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');
|
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:
|
UsersCreateBody:
|
||||||
type: object
|
type: object
|
||||||
required: [employee_code, full_name, email, password, role_id]
|
required: [employee_code, full_name, email, password, role_ids]
|
||||||
properties:
|
properties:
|
||||||
employee_code: { type: string, example: 'EMP002' }
|
employee_code: { type: string, example: 'EMP002' }
|
||||||
full_name: { type: string, example: 'Jane Doe' }
|
full_name: { type: string, example: 'Jane Doe' }
|
||||||
email: { type: string, format: email, example: 'jane@bharaterp.com' }
|
email: { type: string, format: email, example: 'jane@bharaterp.com' }
|
||||||
password: { type: string, minLength: 8, example: 'Admin@123' }
|
password: { type: string, minLength: 8, example: 'Admin@123' }
|
||||||
mobile: { type: string, example: '9876543210' }
|
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 }
|
department_id: { type: integer, example: 1 }
|
||||||
designation_id: { type: integer, example: 1 }
|
designation_id: { type: integer, example: 1 }
|
||||||
plant_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' }
|
email: { type: string, format: email, example: 'jane@bharaterp.com' }
|
||||||
password: { type: string, minLength: 8, example: 'NewPass@123' }
|
password: { type: string, minLength: 8, example: 'NewPass@123' }
|
||||||
mobile: { type: string, example: '9876543210' }
|
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 }
|
department_id: { type: integer, example: 1 }
|
||||||
designation_id: { type: integer, example: 1 }
|
designation_id: { type: integer, example: 1 }
|
||||||
plant_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 ApiError = require('../utils/ApiError');
|
||||||
const env = require('../config/env');
|
const env = require('../config/env');
|
||||||
const prisma = require('../config/prisma');
|
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) => {
|
module.exports = async (req, _res, next) => {
|
||||||
try {
|
try {
|
||||||
@ -15,20 +28,16 @@ module.exports = async (req, _res, next) => {
|
|||||||
|
|
||||||
const user = await prisma.users.findFirst({
|
const user = await prisma.users.findFirst({
|
||||||
where: { id: BigInt(payload.sub), deleted_at: null },
|
where: { id: BigInt(payload.sub), deleted_at: null },
|
||||||
include: {
|
include: rolePermissionsInclude,
|
||||||
roles_users_role_idToroles: {
|
|
||||||
include: {
|
|
||||||
role_permissions: { include: { permissions: { include: { modules: true } } } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user || !user.is_active || user.status !== 'active') {
|
if (!user || !user.is_active || user.status !== 'active') {
|
||||||
throw new ApiError(401, 'Invalid or inactive user');
|
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;
|
req.user = user;
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
const ApiError = require('../utils/ApiError');
|
const ApiError = require('../utils/ApiError');
|
||||||
|
|
||||||
const authorize = (moduleCode, action) => (req, _res, next) => {
|
const authorize = (moduleCode, action) => (req, _res, next) => {
|
||||||
const permissions = req.user?.role?.role_permissions || [];
|
const roles = req.user?.roles || (req.user?.role ? [req.user.role] : []);
|
||||||
const allowed = permissions.some(
|
const allowed = roles.some((role) =>
|
||||||
(rp) => rp.permissions.modules.code === moduleCode && rp.permissions.action === action
|
(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}`));
|
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 ApiError = require('../../utils/ApiError');
|
||||||
const env = require('../../config/env');
|
const env = require('../../config/env');
|
||||||
const { decrypt } = require('../../utils/encryption');
|
const { decrypt } = require('../../utils/encryption');
|
||||||
|
const { collectUserPermissions, extractUserRoles } = require('../../utils/userPermissions');
|
||||||
|
|
||||||
const hashToken = (token) => crypto.createHash('sha256').update(token).digest('hex');
|
const hashToken = (token) => crypto.createHash('sha256').update(token).digest('hex');
|
||||||
|
|
||||||
const issueTokens = async (user) => {
|
const issueTokens = async (user) => {
|
||||||
const accessToken = jwt.sign(
|
const accessToken = jwt.sign({ sub: user.id.toString() }, env.JWT_ACCESS_SECRET, {
|
||||||
{ sub: user.id.toString(), role_id: user.role_id?.toString() },
|
expiresIn: env.JWT_ACCESS_EXPIRY,
|
||||||
env.JWT_ACCESS_SECRET,
|
});
|
||||||
{ expiresIn: env.JWT_ACCESS_EXPIRY }
|
|
||||||
);
|
|
||||||
|
|
||||||
const refreshToken = crypto.randomBytes(40).toString('hex');
|
const refreshToken = crypto.randomBytes(40).toString('hex');
|
||||||
await prisma.refresh_tokens.create({
|
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 getMe = async (userId) => {
|
||||||
const user = await prisma.users.findFirst({
|
const user = await prisma.users.findFirst({
|
||||||
where: { id: BigInt(userId), deleted_at: null },
|
where: { id: BigInt(userId), deleted_at: null },
|
||||||
include: {
|
include: {
|
||||||
roles_users_role_idToroles: {
|
user_roles: {
|
||||||
include: {
|
include: {
|
||||||
role_permissions: {
|
roles: {
|
||||||
include: {
|
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');
|
throw new ApiError(401, 'Invalid or inactive user');
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = user.roles_users_role_idToroles;
|
const roles = extractUserRoles(user).map((role) => ({
|
||||||
const permissions = buildPermissionList(role?.role_permissions);
|
id: role.id,
|
||||||
|
name: role.name,
|
||||||
|
description: role.description,
|
||||||
|
}));
|
||||||
|
const permissions = collectUserPermissions(user);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@ -141,7 +134,8 @@ const getMe = async (userId) => {
|
|||||||
status: user.status,
|
status: user.status,
|
||||||
is_active: user.is_active,
|
is_active: user.is_active,
|
||||||
last_login_at: user.last_login_at,
|
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,
|
department: user.departments,
|
||||||
designation: user.designations,
|
designation: user.designations,
|
||||||
plant: user.plants_users_plant_idToplants,
|
plant: user.plants_users_plant_idToplants,
|
||||||
|
|||||||
@ -157,7 +157,7 @@ const listRoles = async (query) => {
|
|||||||
where,
|
where,
|
||||||
include: {
|
include: {
|
||||||
role_permissions: { include: permissionSelect },
|
role_permissions: { include: permissionSelect },
|
||||||
_count: { select: { users_users_role_idToroles: true } },
|
_count: { select: { user_roles: true } },
|
||||||
},
|
},
|
||||||
orderBy: { created_at: 'desc' },
|
orderBy: { created_at: 'desc' },
|
||||||
skip,
|
skip,
|
||||||
@ -168,7 +168,7 @@ const listRoles = async (query) => {
|
|||||||
|
|
||||||
const data = rows.map((role) => ({
|
const data = rows.map((role) => ({
|
||||||
...sanitizeRole(role),
|
...sanitizeRole(role),
|
||||||
user_count: role._count.users_users_role_idToroles,
|
user_count: role._count.user_roles,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return { data, meta: { page, limit, total } };
|
return { data, meta: { page, limit, total } };
|
||||||
@ -179,11 +179,11 @@ const getRoleById = async (id) => {
|
|||||||
where: { id: BigInt(id), deleted_at: null },
|
where: { id: BigInt(id), deleted_at: null },
|
||||||
include: {
|
include: {
|
||||||
role_permissions: { include: permissionSelect },
|
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');
|
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) => {
|
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');
|
if (!existing) throw new ApiError(404, 'Role not found');
|
||||||
|
|
||||||
const assignedUsers = await prisma.users.count({
|
const assignedUsers = await prisma.user_roles.count({
|
||||||
where: { role_id: BigInt(id), deleted_at: null },
|
where: { role_id: BigInt(id), users: { deleted_at: null } },
|
||||||
});
|
});
|
||||||
if (assignedUsers > 0) {
|
if (assignedUsers > 0) {
|
||||||
throw new ApiError(400, 'Cannot delete role assigned to active users');
|
throw new ApiError(400, 'Cannot delete role assigned to active users');
|
||||||
@ -322,7 +322,7 @@ const listRoleCards = async () => {
|
|||||||
where: { deleted_at: null },
|
where: { deleted_at: null },
|
||||||
include: {
|
include: {
|
||||||
role_permissions: { include: permissionSelect },
|
role_permissions: { include: permissionSelect },
|
||||||
_count: { select: { users_users_role_idToroles: true } },
|
_count: { select: { user_roles: true } },
|
||||||
},
|
},
|
||||||
orderBy: { created_at: 'asc' },
|
orderBy: { created_at: 'asc' },
|
||||||
});
|
});
|
||||||
@ -332,7 +332,7 @@ const listRoleCards = async () => {
|
|||||||
name: role.name,
|
name: role.name,
|
||||||
description: role.description,
|
description: role.description,
|
||||||
is_active: role.is_active,
|
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,
|
permission_count: role.role_permissions.length,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,19 +5,27 @@ const ApiError = require('../../utils/ApiError');
|
|||||||
const auditLog = require('../../utils/auditLog');
|
const auditLog = require('../../utils/auditLog');
|
||||||
const { getPagination } = require('../../utils/pagination');
|
const { getPagination } = require('../../utils/pagination');
|
||||||
const { encrypt, decrypt, blindIndex } = require('../../utils/encryption');
|
const { encrypt, decrypt, blindIndex } = require('../../utils/encryption');
|
||||||
|
const { extractUserRoles } = require('../../utils/userPermissions');
|
||||||
|
|
||||||
const userInclude = {
|
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 } },
|
departments: { select: { id: true, name: true } },
|
||||||
designations: { select: { id: true, name: true } },
|
designations: { select: { id: true, name: true } },
|
||||||
plants_users_plant_idToplants: { select: { id: true, code: 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 } },
|
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) => {
|
const sanitizeUser = (user) => {
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
const {
|
const {
|
||||||
roles_users_role_idToroles,
|
user_roles,
|
||||||
plants_users_plant_idToplants,
|
plants_users_plant_idToplants,
|
||||||
users_users_reporting_toTousers,
|
users_users_reporting_toTousers,
|
||||||
departments,
|
departments,
|
||||||
@ -27,10 +35,13 @@ const sanitizeUser = (user) => {
|
|||||||
delete rest.password_hash;
|
delete rest.password_hash;
|
||||||
delete rest.mobile_index;
|
delete rest.mobile_index;
|
||||||
|
|
||||||
|
const roles = mapUserRoles(user);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
mobile: rest.mobile ? decrypt(rest.mobile) : null,
|
mobile: rest.mobile ? decrypt(rest.mobile) : null,
|
||||||
role: roles_users_role_idToroles || null,
|
roles,
|
||||||
|
role: roles[0] || null,
|
||||||
department: departments || null,
|
department: departments || null,
|
||||||
designation: designations || null,
|
designation: designations || null,
|
||||||
plant: plants_users_plant_idToplants || null,
|
plant: plants_users_plant_idToplants || null,
|
||||||
@ -42,7 +53,13 @@ const buildUsersWhere = (query) => ({
|
|||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
...(query.status ? { status: query.status } : {}),
|
...(query.status ? { status: query.status } : {}),
|
||||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
...(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.department_id ? { department_id: BigInt(query.department_id) } : {}),
|
||||||
...(query.search
|
...(query.search
|
||||||
? {
|
? {
|
||||||
@ -73,6 +90,7 @@ const toUserListItem = (user) => {
|
|||||||
email: row.email,
|
email: row.email,
|
||||||
initials: toInitials(row.full_name),
|
initials: toInitials(row.full_name),
|
||||||
employee_code: row.employee_code,
|
employee_code: row.employee_code,
|
||||||
|
roles: row.roles,
|
||||||
role: row.role,
|
role: row.role,
|
||||||
department: row.department,
|
department: row.department,
|
||||||
plant: row.plant,
|
plant: row.plant,
|
||||||
@ -82,13 +100,22 @@ const toUserListItem = (user) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const assertFk = async (payload) => {
|
const assertRoleIds = async (roleIds) => {
|
||||||
if (payload.role_id) {
|
const uniqueIds = [...new Set((roleIds || []).map((id) => BigInt(id)))];
|
||||||
const role = await prisma.roles.findFirst({
|
if (uniqueIds.length === 0) throw new ApiError(422, 'At least one role is required');
|
||||||
where: { id: BigInt(payload.role_id), deleted_at: null, is_active: true },
|
|
||||||
});
|
const roles = await prisma.roles.findMany({
|
||||||
if (!role) throw new ApiError(422, 'Invalid role_id');
|
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) {
|
if (payload.department_id) {
|
||||||
const row = await prisma.departments.findFirst({
|
const row = await prisma.departments.findFirst({
|
||||||
where: { id: BigInt(payload.department_id), deleted_at: null },
|
where: { id: BigInt(payload.department_id), deleted_at: null },
|
||||||
@ -117,6 +144,7 @@ const assertFk = async (payload) => {
|
|||||||
|
|
||||||
const buildUserData = async (payload, { hashPassword = false } = {}) => {
|
const buildUserData = async (payload, { hashPassword = false } = {}) => {
|
||||||
const data = { ...payload };
|
const data = { ...payload };
|
||||||
|
delete data.role_ids;
|
||||||
|
|
||||||
if (hashPassword && data.password) {
|
if (hashPassword && data.password) {
|
||||||
data.password_hash = await bcrypt.hash(data.password, env.BCRYPT_SALT_ROUNDS);
|
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]);
|
if (data[key] !== undefined && data[key] !== null) data[key] = BigInt(data[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
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) => {
|
const createUser = async (payload, userId, requestId) => {
|
||||||
await assertFk(payload);
|
await assertFk(payload);
|
||||||
|
const roleIds = await assertRoleIds(payload.role_ids);
|
||||||
|
|
||||||
const existing = await prisma.users.findFirst({
|
const existing = await prisma.users.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@ -157,19 +197,41 @@ const createUser = async (payload, userId, requestId) => {
|
|||||||
data.created_by = userId ? BigInt(userId) : null;
|
data.created_by = userId ? BigInt(userId) : null;
|
||||||
data.updated_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({
|
await auditLog({
|
||||||
tableName: 'users',
|
tableName: 'users',
|
||||||
recordId: created.id,
|
recordId: created.id,
|
||||||
action: 'CREATE',
|
action: 'CREATE',
|
||||||
oldValue: null,
|
oldValue: null,
|
||||||
newValue: sanitizeUser(created),
|
newValue: sanitized,
|
||||||
userId,
|
userId,
|
||||||
requestId,
|
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) => {
|
const listUsers = async (query) => {
|
||||||
@ -251,7 +313,7 @@ const exportUsers = async (query) => {
|
|||||||
'Full Name',
|
'Full Name',
|
||||||
'Email',
|
'Email',
|
||||||
'Employee Code',
|
'Employee Code',
|
||||||
'Role',
|
'Roles',
|
||||||
'Department',
|
'Department',
|
||||||
'Plant',
|
'Plant',
|
||||||
'Last Login',
|
'Last Login',
|
||||||
@ -264,7 +326,7 @@ const exportUsers = async (query) => {
|
|||||||
item.full_name,
|
item.full_name,
|
||||||
item.email,
|
item.email,
|
||||||
item.employee_code,
|
item.employee_code,
|
||||||
item.role?.name || '',
|
item.roles.map((role) => role.name).join('; '),
|
||||||
item.department?.name || '',
|
item.department?.name || '',
|
||||||
item.plant?.name || '',
|
item.plant?.name || '',
|
||||||
item.last_login_at ? new Date(item.last_login_at).toISOString() : '',
|
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) });
|
const data = await buildUserData(payload, { hashPassword: Boolean(payload.password) });
|
||||||
data.updated_by = userId ? BigInt(userId) : null;
|
data.updated_by = userId ? BigInt(userId) : null;
|
||||||
|
|
||||||
const updated = await prisma.users.update({
|
const userIdBigInt = BigInt(id);
|
||||||
where: { id: BigInt(id) },
|
const previousRoleIds = (existing.user_roles || []).map((entry) => entry.role_id.toString());
|
||||||
data,
|
|
||||||
include: userInclude,
|
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({
|
await auditLog({
|
||||||
tableName: 'users',
|
tableName: 'users',
|
||||||
recordId: id,
|
recordId: id,
|
||||||
action: 'UPDATE',
|
action: 'UPDATE',
|
||||||
oldValue: sanitizeUser(existing),
|
oldValue: sanitizeUser(existing),
|
||||||
newValue: sanitizeUser(updated),
|
newValue: sanitized,
|
||||||
userId,
|
userId,
|
||||||
requestId,
|
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) => {
|
const deleteUser = async (id, userId, requestId) => {
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
const Joi = require('joi');
|
const Joi = require('joi');
|
||||||
|
|
||||||
|
const roleIdsSchema = Joi.array().items(Joi.number().integer().positive()).min(1);
|
||||||
|
|
||||||
const createUserSchema = Joi.object({
|
const createUserSchema = Joi.object({
|
||||||
employee_code: Joi.string().max(30).required(),
|
employee_code: Joi.string().max(30).required(),
|
||||||
full_name: Joi.string().max(200).required(),
|
full_name: Joi.string().max(200).required(),
|
||||||
email: Joi.string().email().max(200).required(),
|
email: Joi.string().email().max(200).required(),
|
||||||
password: Joi.string().min(8).required(),
|
password: Joi.string().min(8).required(),
|
||||||
mobile: Joi.string().max(20).allow(null, '').optional(),
|
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(),
|
department_id: Joi.number().integer().positive().allow(null).optional(),
|
||||||
designation_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(),
|
plant_id: Joi.number().integer().positive().allow(null).optional(),
|
||||||
@ -21,7 +23,7 @@ const updateUserSchema = Joi.object({
|
|||||||
email: Joi.string().email().max(200).optional(),
|
email: Joi.string().email().max(200).optional(),
|
||||||
password: Joi.string().min(8).optional(),
|
password: Joi.string().min(8).optional(),
|
||||||
mobile: Joi.string().max(20).allow(null, '').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(),
|
department_id: Joi.number().integer().positive().allow(null).optional(),
|
||||||
designation_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(),
|
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