371 lines
10 KiB
JavaScript
371 lines
10 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 userInclude = {
|
|
roles_users_role_idToroles: { 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 sanitizeUser = (user) => {
|
|
if (!user) return null;
|
|
const {
|
|
roles_users_role_idToroles,
|
|
plants_users_plant_idToplants,
|
|
users_users_reporting_toTousers,
|
|
departments,
|
|
designations,
|
|
...rest
|
|
} = user;
|
|
delete rest.password_hash;
|
|
delete rest.mobile_index;
|
|
|
|
return {
|
|
...rest,
|
|
mobile: rest.mobile ? decrypt(rest.mobile) : null,
|
|
role: roles_users_role_idToroles || null,
|
|
department: departments || null,
|
|
designation: designations || null,
|
|
plant: plants_users_plant_idToplants || 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 ? { 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,
|
|
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 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');
|
|
}
|
|
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) {
|
|
const row = await prisma.plants.findFirst({
|
|
where: { id: BigInt(payload.plant_id), deleted_at: null },
|
|
});
|
|
if (!row) throw new ApiError(422, 'Invalid 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 };
|
|
|
|
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 ['role_id', 'department_id', 'designation_id', 'plant_id', 'reporting_to']) {
|
|
if (data[key] !== undefined && data[key] !== null) data[key] = BigInt(data[key]);
|
|
}
|
|
|
|
return data;
|
|
};
|
|
|
|
const createUser = async (payload, userId, requestId) => {
|
|
await assertFk(payload);
|
|
|
|
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.users.create({ data, include: userInclude });
|
|
|
|
await auditLog({
|
|
tableName: 'users',
|
|
recordId: created.id,
|
|
action: 'CREATE',
|
|
oldValue: null,
|
|
newValue: sanitizeUser(created),
|
|
userId,
|
|
requestId,
|
|
});
|
|
|
|
return sanitizeUser(created);
|
|
};
|
|
|
|
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 escapeCsv = (value) => {
|
|
const text = String(value ?? '');
|
|
if (/[",\n]/.test(text)) return `"${text.replace(/"/g, '""')}"`;
|
|
return text;
|
|
};
|
|
|
|
const exportUsers = async (query) => {
|
|
const where = buildUsersWhere(query);
|
|
const rows = await prisma.users.findMany({
|
|
where,
|
|
include: userInclude,
|
|
orderBy: { full_name: 'asc' },
|
|
});
|
|
|
|
const header = [
|
|
'Full Name',
|
|
'Email',
|
|
'Employee Code',
|
|
'Role',
|
|
'Department',
|
|
'Plant',
|
|
'Last Login',
|
|
'Status',
|
|
];
|
|
|
|
const lines = rows.map((user) => {
|
|
const item = toUserListItem(user);
|
|
return [
|
|
item.full_name,
|
|
item.email,
|
|
item.employee_code,
|
|
item.role?.name || '',
|
|
item.department?.name || '',
|
|
item.plant?.name || '',
|
|
item.last_login_at ? new Date(item.last_login_at).toISOString() : '',
|
|
item.status,
|
|
]
|
|
.map(escapeCsv)
|
|
.join(',');
|
|
});
|
|
|
|
return [header.join(','), ...lines].join('\n');
|
|
};
|
|
|
|
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 updated = await prisma.users.update({
|
|
where: { id: BigInt(id) },
|
|
data,
|
|
include: userInclude,
|
|
});
|
|
|
|
await auditLog({
|
|
tableName: 'users',
|
|
recordId: id,
|
|
action: 'UPDATE',
|
|
oldValue: sanitizeUser(existing),
|
|
newValue: sanitizeUser(updated),
|
|
userId,
|
|
requestId,
|
|
});
|
|
|
|
return sanitizeUser(updated);
|
|
};
|
|
|
|
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,
|
|
};
|