436 lines
12 KiB
JavaScript
436 lines
12 KiB
JavaScript
const bcrypt = require('bcrypt');
|
|
const prisma = require('../../config/prisma');
|
|
const env = require('../../config/env');
|
|
const ApiError = require('../../utils/ApiError');
|
|
const auditLog = require('../../utils/auditLog');
|
|
const { getPagination } = require('../../utils/pagination');
|
|
const { encrypt, decrypt, blindIndex } = require('../../utils/encryption');
|
|
const { assertPlant } = require('../../utils/locations');
|
|
const { extractUserRoles } = require('../../utils/userPermissions');
|
|
|
|
const userInclude = {
|
|
user_roles: {
|
|
include: { roles: { select: { id: true, name: true } } },
|
|
},
|
|
departments: { select: { id: true, name: true } },
|
|
designations: { select: { id: true, name: true } },
|
|
plant_location: { 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 {
|
|
user_roles,
|
|
plant_location,
|
|
users_users_reporting_toTousers,
|
|
departments,
|
|
designations,
|
|
...rest
|
|
} = user;
|
|
delete rest.password_hash;
|
|
delete rest.mobile_index;
|
|
|
|
const roles = mapUserRoles(user);
|
|
|
|
return {
|
|
...rest,
|
|
mobile: rest.mobile ? decrypt(rest.mobile) : null,
|
|
roles,
|
|
role: roles[0] || null,
|
|
department: departments || null,
|
|
designation: designations || null,
|
|
plant: plant_location || null,
|
|
reporting_manager: users_users_reporting_toTousers || null,
|
|
};
|
|
};
|
|
|
|
const buildUsersWhere = (query) => ({
|
|
deleted_at: null,
|
|
...(query.status ? { status: query.status } : {}),
|
|
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
|
...(query.role_id
|
|
? {
|
|
user_roles: {
|
|
some: { role_id: BigInt(query.role_id) },
|
|
},
|
|
}
|
|
: {}),
|
|
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
|
|
...(query.search
|
|
? {
|
|
OR: [
|
|
{ full_name: { contains: query.search, mode: 'insensitive' } },
|
|
{ email: { contains: query.search, mode: 'insensitive' } },
|
|
{ employee_code: { contains: query.search, mode: 'insensitive' } },
|
|
],
|
|
}
|
|
: {}),
|
|
});
|
|
|
|
const toInitials = (name) => {
|
|
if (!name) return '';
|
|
return name
|
|
.trim()
|
|
.split(/\s+/)
|
|
.slice(0, 2)
|
|
.map((part) => part[0]?.toUpperCase() || '')
|
|
.join('');
|
|
};
|
|
|
|
const toUserListItem = (user) => {
|
|
const row = sanitizeUser(user);
|
|
return {
|
|
id: row.id,
|
|
full_name: row.full_name,
|
|
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,
|
|
last_login_at: row.last_login_at,
|
|
status: row.status,
|
|
is_active: row.is_active,
|
|
};
|
|
};
|
|
|
|
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 },
|
|
});
|
|
if (!row) throw new ApiError(422, 'Invalid department_id');
|
|
}
|
|
if (payload.designation_id) {
|
|
const row = await prisma.designations.findFirst({
|
|
where: { id: BigInt(payload.designation_id), deleted_at: null },
|
|
});
|
|
if (!row) throw new ApiError(422, 'Invalid designation_id');
|
|
}
|
|
if (payload.plant_id) {
|
|
await assertPlant(payload.plant_id, 'plant_id');
|
|
}
|
|
if (payload.reporting_to) {
|
|
const row = await prisma.users.findFirst({
|
|
where: { id: BigInt(payload.reporting_to), deleted_at: null },
|
|
});
|
|
if (!row) throw new ApiError(422, 'Invalid reporting_to');
|
|
}
|
|
};
|
|
|
|
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);
|
|
delete data.password;
|
|
} else {
|
|
delete data.password;
|
|
}
|
|
|
|
if (Object.prototype.hasOwnProperty.call(data, 'mobile')) {
|
|
if (data.mobile) {
|
|
data.mobile_index = blindIndex(data.mobile);
|
|
data.mobile = encrypt(data.mobile);
|
|
} else {
|
|
data.mobile = null;
|
|
data.mobile_index = null;
|
|
}
|
|
}
|
|
|
|
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: {
|
|
deleted_at: null,
|
|
OR: [{ email: payload.email }, { employee_code: payload.employee_code }],
|
|
},
|
|
});
|
|
if (existing) throw new ApiError(409, 'User with this email or employee code already exists');
|
|
|
|
const data = await buildUserData(payload, { hashPassword: true });
|
|
data.created_by = userId ? BigInt(userId) : null;
|
|
data.updated_by = userId ? BigInt(userId) : null;
|
|
|
|
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: sanitized,
|
|
userId,
|
|
requestId,
|
|
});
|
|
|
|
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 { page, limit, skip } = getPagination(query);
|
|
const where = buildUsersWhere(query);
|
|
|
|
const [rows, total] = await Promise.all([
|
|
prisma.users.findMany({
|
|
where,
|
|
include: userInclude,
|
|
orderBy: { created_at: 'desc' },
|
|
skip,
|
|
take: limit,
|
|
}),
|
|
prisma.users.count({ where }),
|
|
]);
|
|
|
|
return { data: rows.map(toUserListItem), meta: { page, limit, total } };
|
|
};
|
|
|
|
const getScreenSummary = async () => {
|
|
const baseWhere = { deleted_at: null };
|
|
|
|
const [total, active, inactive, locked, rolesTotal] = await Promise.all([
|
|
prisma.users.count({ where: baseWhere }),
|
|
prisma.users.count({ where: { ...baseWhere, status: 'active' } }),
|
|
prisma.users.count({ where: { ...baseWhere, status: 'inactive' } }),
|
|
prisma.users.count({ where: { ...baseWhere, status: 'locked' } }),
|
|
prisma.roles.count({ where: { deleted_at: null } }),
|
|
]);
|
|
|
|
return {
|
|
users: { total, active, inactive, locked },
|
|
roles: { total: rolesTotal },
|
|
tabs: { users: total, roles: rolesTotal },
|
|
};
|
|
};
|
|
|
|
const getFilterOptions = async () => {
|
|
const [roles, departments] = await Promise.all([
|
|
prisma.roles.findMany({
|
|
where: { deleted_at: null, is_active: true },
|
|
select: { id: true, name: true },
|
|
orderBy: { name: 'asc' },
|
|
}),
|
|
prisma.departments.findMany({
|
|
where: { deleted_at: null, is_active: true },
|
|
select: { id: true, name: true },
|
|
orderBy: { name: 'asc' },
|
|
}),
|
|
]);
|
|
|
|
return {
|
|
roles,
|
|
departments,
|
|
statuses: [
|
|
{ value: 'active', label: 'Active' },
|
|
{ value: 'inactive', label: 'Inactive' },
|
|
{ value: 'locked', label: 'Locked' },
|
|
],
|
|
};
|
|
};
|
|
|
|
const { rowsToCsv } = require('../../utils/csv');
|
|
|
|
const exportUsers = async (query) => {
|
|
const where = buildUsersWhere(query);
|
|
const rows = await prisma.users.findMany({
|
|
where,
|
|
include: userInclude,
|
|
orderBy: { full_name: 'asc' },
|
|
});
|
|
|
|
return rowsToCsv(
|
|
[
|
|
{ key: 'full_name', header: 'Full Name' },
|
|
{ key: 'email', header: 'Email' },
|
|
{ key: 'employee_code', header: 'Employee Code' },
|
|
{ key: (row) => row.roles.map((role) => role.name).join('; '), header: 'Roles' },
|
|
{ key: (row) => row.department?.name || '', header: 'Department' },
|
|
{ key: (row) => row.plant?.name || '', header: 'Plant' },
|
|
{ key: 'last_login_at', header: 'Last Login', type: 'datetime' },
|
|
{ key: 'status', header: 'Status' },
|
|
],
|
|
rows.map(toUserListItem)
|
|
);
|
|
};
|
|
|
|
const getUserById = async (id) => {
|
|
const user = await prisma.users.findFirst({
|
|
where: { id: BigInt(id), deleted_at: null },
|
|
include: userInclude,
|
|
});
|
|
if (!user) throw new ApiError(404, 'User not found');
|
|
return sanitizeUser(user);
|
|
};
|
|
|
|
const updateUser = async (id, payload, userId, requestId) => {
|
|
const existing = await prisma.users.findFirst({
|
|
where: { id: BigInt(id), deleted_at: null },
|
|
include: userInclude,
|
|
});
|
|
if (!existing) throw new ApiError(404, 'User not found');
|
|
|
|
await assertFk(payload);
|
|
|
|
if (payload.email || payload.employee_code) {
|
|
const duplicate = await prisma.users.findFirst({
|
|
where: {
|
|
deleted_at: null,
|
|
id: { not: BigInt(id) },
|
|
OR: [
|
|
...(payload.email ? [{ email: payload.email }] : []),
|
|
...(payload.employee_code ? [{ employee_code: payload.employee_code }] : []),
|
|
],
|
|
},
|
|
});
|
|
if (duplicate) throw new ApiError(409, 'User with this email or employee code already exists');
|
|
}
|
|
|
|
const data = await buildUserData(payload, { hashPassword: Boolean(payload.password) });
|
|
data.updated_by = userId ? BigInt(userId) : null;
|
|
|
|
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: sanitized,
|
|
userId,
|
|
requestId,
|
|
});
|
|
|
|
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 existing = await prisma.users.findFirst({
|
|
where: { id: BigInt(id), deleted_at: null },
|
|
include: userInclude,
|
|
});
|
|
if (!existing) throw new ApiError(404, 'User not found');
|
|
|
|
if (userId && String(existing.id) === String(userId)) {
|
|
throw new ApiError(400, 'You cannot delete your own account');
|
|
}
|
|
|
|
await prisma.users.update({
|
|
where: { id: BigInt(id) },
|
|
data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null, is_active: false },
|
|
});
|
|
|
|
await auditLog({
|
|
tableName: 'users',
|
|
recordId: id,
|
|
action: 'DELETE',
|
|
oldValue: sanitizeUser(existing),
|
|
newValue: { deleted_at: new Date() },
|
|
userId,
|
|
requestId,
|
|
});
|
|
};
|
|
|
|
module.exports = {
|
|
createUser,
|
|
listUsers,
|
|
getUserById,
|
|
updateUser,
|
|
deleteUser,
|
|
getScreenSummary,
|
|
getFilterOptions,
|
|
exportUsers,
|
|
};
|