mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
386 lines
16 KiB
JavaScript
386 lines
16 KiB
JavaScript
/***********************************************************************************************************************************************************************
|
|
* File Name: users.controller.js (admin)
|
|
* Type of Program: Controller
|
|
* Description: Admin-level user management — full CRUD on any user.
|
|
*
|
|
* Author: rgrgogu
|
|
* Date Created: Oct. 6, 2025
|
|
***********************************************************************************************************************************************************************/
|
|
const sequelize = require('../../config/db.config');
|
|
const { Op, Sequelize } = require('sequelize');
|
|
const bcrypt = require('bcryptjs');
|
|
const crypto = require('crypto');
|
|
|
|
const mdl_Users = require('../../models/users/users.mdl');
|
|
const mdl_UserSessions = require('../../models/users/user_sessions.mdl');
|
|
const mdl_Achievements = require('../../models/users/achievements.mdl');
|
|
const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl');
|
|
|
|
const { sendEmail } = require('../../services/email.service');
|
|
const logActivity = require('../../utils/logActivity.util');
|
|
const R = require('../../utils/response.util');
|
|
const { paginate } = require('../../utils/paginate.util');
|
|
const { enrichPersonalInfo } = require('../../utils/personalInfo.util');
|
|
const { getFieldValues } = require("../../utils/fieldValues.util");
|
|
|
|
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas, computedAttributes: userComputed } = require('../../models/users/users.attributes');
|
|
|
|
const EXCLUDED = ['password', 'otp_code', 'otp_expires_at', 'must_change_password', 'password_expires_at'];
|
|
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy'];
|
|
|
|
// ─── GET ALL ──────────────────────────────────────────────────────────────────
|
|
|
|
exports.getUsers = async (req, res) => {
|
|
try {
|
|
const result = await paginate(mdl_Users, req, {
|
|
excludeAttributes: usersExclude,
|
|
jsonbSchemas: usersSchemas,
|
|
jsonbColumn: 'personal_info',
|
|
computedAttributes: userComputed,
|
|
context: "list",
|
|
auditOptions: { mdl_Users, parentAlias: 'User' },
|
|
findOptions: {
|
|
include: [{
|
|
model: mdl_UserGroups,
|
|
as: 'groups',
|
|
through: { attributes: [] }, // hide junction columns
|
|
attributes: ['group_id', 'name', 'group_code'],
|
|
required: false, // LEFT JOIN — users with no group still appear
|
|
}],
|
|
},
|
|
});
|
|
|
|
return R.success(res, 'Users retrieved.', result);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET ALL USERS]', err);
|
|
return R.error(res, 'Could not retrieve users.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── GET ONE ──────────────────────────────────────────────────────────────────
|
|
|
|
exports.getUser = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
if (!id || id === 'undefined') return R.error(res, 'Invalid User ID.', 400);
|
|
|
|
const user = await mdl_Users.findByPk(id, {
|
|
attributes: { exclude: EXCLUDED },
|
|
include: [{
|
|
model: mdl_UserGroups,
|
|
as: 'groups',
|
|
through: { attributes: [] },
|
|
}],
|
|
});
|
|
|
|
if (!user) return R.error(res, 'User not found.', 404);
|
|
return R.success(res, 'User retrieved.', user);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET USER]', err);
|
|
return R.error(res, 'Could not retrieve user.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── ADD STAFF ────────────────────────────────────────────────────────────────
|
|
|
|
exports.addStaffUser = async (req, res) => {
|
|
try {
|
|
const { email, personal_info = {} } = req.body;
|
|
|
|
if (!email)
|
|
return R.error(res, 'Email is required.', 400);
|
|
|
|
if (!personal_info?.name?.given_name || !personal_info?.name?.last_name)
|
|
return R.error(res, 'First name and last name are required.', 400);
|
|
|
|
const existing = await mdl_Users.findOne({ where: { email } });
|
|
if (existing) return R.error(res, 'Email is already in use.', 409);
|
|
|
|
const plainPassword = crypto.randomBytes(8).toString('base64url').slice(0, 12);
|
|
const hashed = await bcrypt.hash(plainPassword, 12);
|
|
const passwordExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
|
const enriched = enrichPersonalInfo(personal_info);
|
|
const fullName = enriched?.name?.full_name ?? email;
|
|
|
|
const user = await mdl_Users.create({
|
|
email,
|
|
password: hashed,
|
|
password_expires_at: passwordExpiresAt,
|
|
must_change_password: true,
|
|
acc_type: 'staff',
|
|
reg_type: 'system',
|
|
is_active: true,
|
|
is_verified: true,
|
|
createdBy: req.user.user_id,
|
|
personal_info: enriched,
|
|
});
|
|
|
|
await sendEmail({
|
|
to: email, type: 'ADD_STAFF', data: {
|
|
name: fullName, email, password: plainPassword, expiryHours: 24,
|
|
},
|
|
});
|
|
|
|
logActivity(req.user.user_id, 'create_staff', { entityType: 'user', entityId: user.user_id, details: { email } });
|
|
|
|
return R.success(res, 'Staff user created successfully.', {
|
|
user_id: user.user_id,
|
|
email: user.email,
|
|
acc_type: user.acc_type,
|
|
}, 201);
|
|
} catch (err) {
|
|
console.error('[ADMIN][ADD STAFF USER]', err);
|
|
return R.error(res, 'Could not create staff user.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── UPDATE ───────────────────────────────────────────────────────────────────
|
|
|
|
exports.updateUser = async (req, res) => {
|
|
try {
|
|
const user = await mdl_Users.findByPk(req.params.id);
|
|
if (!user) return R.error(res, 'User not found.', 404);
|
|
|
|
const allowed = ['acc_type', 'is_active', 'personal_info'];
|
|
const updates = {};
|
|
allowed.forEach((k) => { if (req.body[k] !== undefined) updates[k] = req.body[k]; });
|
|
|
|
if (Number(req.params.id) === req.user.user_id && updates.acc_type !== undefined)
|
|
return R.error(res, 'Admins cannot change their own role.', 400);
|
|
|
|
if (updates.personal_info)
|
|
updates.personal_info = enrichPersonalInfo(updates.personal_info);
|
|
|
|
updates.updatedBy = req.user.user_id;
|
|
|
|
await user.update(updates);
|
|
|
|
logActivity(req.user.user_id, 'update_user', { entityType: 'user', entityId: Number(req.params.id) });
|
|
|
|
const updated = await mdl_Users.findByPk(req.params.id, { attributes: { exclude: EXCLUDED } });
|
|
return R.success(res, 'User updated.', updated);
|
|
} catch (err) {
|
|
console.error('[ADMIN][UPDATE USER]', err);
|
|
return R.error(res, 'Could not update user.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── DEACTIVATE (soft delete) ─────────────────────────────────────────────────
|
|
|
|
exports.deactivateUser = async (req, res) => {
|
|
try {
|
|
if (Number(req.params.id) === req.user.user_id)
|
|
return R.error(res, 'You cannot deactivate your own account.', 400);
|
|
|
|
const user = await mdl_Users.findByPk(req.params.id);
|
|
if (!user) return R.error(res, 'User not found.', 404);
|
|
if (!user.is_active && user.deletedAt)
|
|
return R.error(res, 'User is already deactivated.', 400);
|
|
|
|
await user.update({ is_active: false, deletedBy: req.user.user_id });
|
|
await user.destroy();
|
|
|
|
await mdl_UserSessions.update(
|
|
{ is_active: false, logout_info: { date: new Date().toISOString(), forced_by: req.user.user_id } },
|
|
{ where: { user_id: req.params.id } }
|
|
);
|
|
|
|
logActivity(req.user.user_id, 'deactivate_user', { entityType: 'user', entityId: Number(req.params.id) });
|
|
|
|
return R.success(res, 'User deactivated successfully.');
|
|
} catch (err) {
|
|
console.error('[ADMIN][DEACTIVATE USER]', err);
|
|
return R.error(res, 'Could not deactivate user.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── BULK DEACTIVATE ──────────────────────────────────────────────────────────
|
|
|
|
exports.bulkDeactivateUsers = async (req, res) => {
|
|
try {
|
|
const { ids } = req.body;
|
|
if (!Array.isArray(ids) || !ids.length)
|
|
return R.error(res, 'No user IDs provided.', 400);
|
|
|
|
if (ids.includes(req.user.user_id))
|
|
return R.error(res, 'You cannot deactivate your own account.', 400);
|
|
|
|
const users = await mdl_Users.findAll({ where: { user_id: ids } });
|
|
if (!users.length) return R.error(res, 'No users found.', 404);
|
|
|
|
const activeUsers = users.filter((u) => u.is_active && !u.deletedAt);
|
|
if (!activeUsers.length)
|
|
return R.error(res, 'All selected users are already deactivated.', 400);
|
|
|
|
const activeIds = activeUsers.map((u) => u.user_id);
|
|
|
|
await mdl_Users.update(
|
|
{ is_active: false, deletedBy: req.user.user_id },
|
|
{ where: { user_id: activeIds } }
|
|
);
|
|
await mdl_Users.destroy({ where: { user_id: activeIds } });
|
|
await mdl_UserSessions.update(
|
|
{ is_active: false, logout_info: { date: new Date().toISOString(), forced_by: req.user.user_id } },
|
|
{ where: { user_id: activeIds } }
|
|
);
|
|
|
|
return R.success(res, `${activeIds.length} user(s) deactivated successfully.`, {
|
|
deactivated_ids: activeIds,
|
|
skipped_ids: ids.filter((id) => !activeIds.includes(id)),
|
|
});
|
|
} catch (err) {
|
|
console.error('[ADMIN][BULK DEACTIVATE USERS]', err);
|
|
return R.error(res, 'Could not deactivate users.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── RESTORE ──────────────────────────────────────────────────────────────────
|
|
|
|
exports.restoreUser = async (req, res) => {
|
|
try {
|
|
const user = await mdl_Users.findOne({
|
|
where: { user_id: req.params.id }, paranoid: false,
|
|
});
|
|
if (!user) return R.error(res, 'User not found.', 404);
|
|
if (!user.deletedAt) return R.error(res, 'User is not deactivated.', 400);
|
|
|
|
await user.restore();
|
|
await user.update({ is_active: true, updatedBy: req.user.user_id, deletedBy: null });
|
|
|
|
logActivity(req.user.user_id, 'restore_user', { entityType: 'user', entityId: Number(req.params.id) });
|
|
|
|
return R.success(res, 'User restored successfully.');
|
|
} catch (err) {
|
|
console.error('[ADMIN][RESTORE USER]', err);
|
|
return R.error(res, 'Could not restore user.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── BULK RESTORE ─────────────────────────────────────────────────────────────
|
|
|
|
exports.bulkRestoreUsers = async (req, res) => {
|
|
try {
|
|
const { ids } = req.body;
|
|
if (!Array.isArray(ids) || !ids.length)
|
|
return R.error(res, 'No user IDs provided.', 400);
|
|
|
|
const users = await mdl_Users.findAll({ where: { user_id: ids }, paranoid: false });
|
|
if (!users.length) return R.error(res, 'No users found.', 404);
|
|
|
|
const deletedUsers = users.filter((u) => u.deletedAt);
|
|
if (!deletedUsers.length)
|
|
return R.error(res, 'All selected users are already active.', 400);
|
|
|
|
const deletedIds = deletedUsers.map((u) => u.user_id);
|
|
|
|
await mdl_Users.restore({ where: { user_id: deletedIds } });
|
|
await mdl_Users.update(
|
|
{ is_active: true, updatedBy: req.user.user_id, deletedBy: null },
|
|
{ where: { user_id: deletedIds }, paranoid: false }
|
|
);
|
|
|
|
return R.success(res, `${deletedIds.length} user(s) restored successfully.`, {
|
|
restored_ids: deletedIds,
|
|
skipped_ids: ids.filter((id) => !deletedIds.includes(id)),
|
|
});
|
|
} catch (err) {
|
|
console.error('[ADMIN][BULK RESTORE USERS]', err);
|
|
return R.error(res, 'Could not restore users.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── SESSIONS ─────────────────────────────────────────────────────────────────
|
|
|
|
exports.getUserSessions = async (req, res) => {
|
|
try {
|
|
const sessions = await mdl_UserSessions.findAll({
|
|
where: { user_id: req.params.id },
|
|
attributes: { exclude: ['refresh_token_hash'] },
|
|
order: [['createdAt', 'DESC']],
|
|
});
|
|
return R.success(res, 'Sessions retrieved.', sessions);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET USER SESSIONS]', err);
|
|
return R.error(res, 'Could not retrieve sessions.', 500);
|
|
}
|
|
};
|
|
|
|
exports.terminateSession = async (req, res) => {
|
|
try {
|
|
const session = await mdl_UserSessions.findByPk(req.params.sid);
|
|
if (!session) return R.error(res, 'Session not found.', 404);
|
|
|
|
await session.update({
|
|
is_active: false,
|
|
logout_info: { date: new Date().toISOString(), forced_by: req.user.user_id },
|
|
});
|
|
|
|
logActivity(req.user.user_id, 'terminate_session', { entityType: 'session', entityId: session.session_id });
|
|
|
|
return R.success(res, 'Session terminated.');
|
|
} catch (err) {
|
|
console.error('[ADMIN][TERMINATE SESSION]', err);
|
|
return R.error(res, 'Could not terminate session.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── FIELD VALUES ─────────────────────────────────────────────────────────────
|
|
|
|
exports.getUserFieldValues = getFieldValues(mdl_Users, "USER", {
|
|
blockedFields: ["password", "otp_code", "otp_expires_at", "personal_info"],
|
|
extraDateFields: ["modifiedAt"],
|
|
allowJsonb: true,
|
|
selfJoin: true,
|
|
});
|
|
|
|
// ─── ARCHIVED ─────────────────────────────────────────────────────────────────
|
|
|
|
// ─── GET ARCHIVED USERS ───────────────────────────────────────────────────────
|
|
exports.getArchivedUsers = async (req, res) => {
|
|
try {
|
|
const result = await paginate(mdl_Users, req, {
|
|
excludeAttributes: usersExclude,
|
|
jsonbSchemas: usersSchemas,
|
|
jsonbColumn: 'personal_info',
|
|
computedAttributes: userComputed,
|
|
context: 'archived',
|
|
auditOptions: { mdl_Users, parentAlias: 'User' },
|
|
findOptions: {
|
|
paranoid: false,
|
|
where: { deletedAt: { [Op.ne]: null }, is_active: false },
|
|
include: [{
|
|
model: mdl_UserGroups,
|
|
as: 'groups',
|
|
through: { attributes: [] },
|
|
attributes: ['group_id', 'name', 'group_code'],
|
|
required: false,
|
|
}],
|
|
},
|
|
});
|
|
|
|
return R.success(res, 'Archived users retrieved.', result);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET ARCHIVED USERS]', err);
|
|
return R.error(res, 'Could not retrieve archived users.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── GET USER ACHIEVEMENTS ────────────────────────────────────────────────────
|
|
|
|
exports.getUserAchievements = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const user = await mdl_Users.findByPk(id, { attributes: ['user_id'] });
|
|
if (!user) return R.error(res, 'User not found.', 404);
|
|
|
|
const achievements = await mdl_Achievements.findAll({
|
|
where: { user_id: id },
|
|
order: [['granted_at', 'DESC']],
|
|
});
|
|
|
|
return R.success(res, 'Achievements retrieved.', achievements);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET USER ACHIEVEMENTS]', err);
|
|
return R.error(res, 'Could not retrieve achievements.', 500);
|
|
}
|
|
}; |