mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
Adjusted
This commit is contained in:
@@ -2,50 +2,38 @@
|
||||
* File Name: users.controller.js (admin)
|
||||
* Type of Program: Controller
|
||||
* Description: Admin-level user management — full CRUD on any user.
|
||||
* Admins can: list all users, view any user, update acc_type,
|
||||
* activate/deactivate, hard-delete, manage groups.
|
||||
*
|
||||
* Endpoints (require authenticate → requireAdmin()):
|
||||
* GET /api/admin/users → all users (paginated)
|
||||
* GET /api/admin/users/:id → any user
|
||||
* PUT /api/admin/users/:id → update acc_type, is_active, personal_info
|
||||
* DELETE /api/admin/users/:id → hard delete
|
||||
* POST /api/admin/users/:id/groups/:gid → add user to group
|
||||
* DELETE /api/admin/users/:id/groups/:gid → remove user from group
|
||||
* GET /api/admin/groups → list groups
|
||||
* POST /api/admin/groups → create group
|
||||
* DELETE /api/admin/groups/:gid → delete group
|
||||
*
|
||||
* Author: rgrgogu
|
||||
* Date Created: Oct. 6, 2025
|
||||
***********************************************************************************************************************************************************************/
|
||||
const sequelize = require('../../config/db.config')
|
||||
const { Op, Sequelize } = require('sequelize')
|
||||
const bcrypt = require('bcryptjs')
|
||||
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_Users = require('../../models/users/users.mdl');
|
||||
const mdl_UserSessions = require('../../models/users/user_sessions.mdl');
|
||||
const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/user_groups.mdl');
|
||||
|
||||
const sendEmail = require('../../services/email.service');
|
||||
const R = require('../../utils/response.util');
|
||||
const { paginate, auditInclude } = require('../../utils/paginate.util');
|
||||
const R = require('../../utils/response.util');
|
||||
const { paginate } = require('../../utils/paginate.util');
|
||||
const { enrichPersonalInfo } = require('../../utils/personalInfo.util');
|
||||
|
||||
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas, computedAttributes: usersComputed } = require('../../models/users/users.attributes');
|
||||
const { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas, computedAttributes: groupComputed } = require('../../models/users/user_groups.attributes');
|
||||
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes');
|
||||
|
||||
const EXCLUDED = ['password', 'otp_code', 'otp_expires_at', 'must_change_password', 'password_expires_at'];
|
||||
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',
|
||||
auditOptions: { mdl_Users, parentAlias: 'User' },
|
||||
jsonbSchemas: usersSchemas,
|
||||
jsonbColumn: 'personal_info',
|
||||
auditOptions: { mdl_Users, parentAlias: 'User' },
|
||||
});
|
||||
|
||||
return R.success(res, 'Users retrieved.', result);
|
||||
@@ -55,29 +43,35 @@ exports.getUsers = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ─── GET single user ───────────────────────────────────────────────────────────
|
||||
// ─── GET ONE ──────────────────────────────────────────────────────────────────
|
||||
|
||||
exports.getUser = async (req, res) => {
|
||||
try {
|
||||
const user = await mdl_Users.findByPk(req.params.id, {
|
||||
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', // ← matches the association alias
|
||||
through: { attributes: [] }
|
||||
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);
|
||||
console.error('[ADMIN][GET USER]', err);
|
||||
return R.error(res, 'Could not retrieve user.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── POST add staff user ───────────────────────────────────────────────────────
|
||||
// ─── ADD STAFF ────────────────────────────────────────────────────────────────
|
||||
|
||||
exports.addStaffUser = async (req, res) => {
|
||||
try {
|
||||
const { email, personal_info = {} } = req.body; // ← password removed from body
|
||||
const { email, personal_info = {} } = req.body;
|
||||
|
||||
if (!email)
|
||||
return R.error(res, 'Email is required.', 400);
|
||||
@@ -86,44 +80,36 @@ exports.addStaffUser = async (req, res) => {
|
||||
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);
|
||||
if (existing) return R.error(res, 'Email is already in use.', 409);
|
||||
|
||||
// ── Auto-generate a temporary password ────────────────────────────────────
|
||||
const plainPassword = crypto.randomBytes(8).toString('base64url').slice(0, 12);
|
||||
const hashed = await bcrypt.hash(plainPassword, 12);
|
||||
|
||||
// ── Set password expiry (24 hours from now) ───────────────────────────────
|
||||
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 enriched = enrichPersonalInfo(personal_info);
|
||||
const fullName = enriched?.name?.full_name ?? email;
|
||||
|
||||
const user = await mdl_Users.create({
|
||||
email,
|
||||
password: hashed,
|
||||
password_expires_at: passwordExpiresAt, // ← requires column in DB
|
||||
must_change_password: true, // ← force change on first login
|
||||
acc_type: 'staff',
|
||||
reg_type: 'system',
|
||||
is_active: true,
|
||||
is_verified: true,
|
||||
createdBy: req.user.user_id,
|
||||
personal_info: enriched,
|
||||
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,
|
||||
}
|
||||
to: email, type: 'ADD_STAFF', data: {
|
||||
name: fullName, email, password: plainPassword, expiryHours: 24,
|
||||
},
|
||||
});
|
||||
|
||||
return R.success(res, 'Staff user created successfully.', {
|
||||
user_id: user.user_id,
|
||||
email: user.email,
|
||||
user_id: user.user_id,
|
||||
email: user.email,
|
||||
acc_type: user.acc_type,
|
||||
}, 201);
|
||||
} catch (err) {
|
||||
@@ -132,7 +118,8 @@ exports.addStaffUser = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ─── PUT update any user ───────────────────────────────────────────────────────
|
||||
// ─── UPDATE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
exports.updateUser = async (req, res) => {
|
||||
try {
|
||||
const user = await mdl_Users.findByPk(req.params.id);
|
||||
@@ -145,23 +132,22 @@ exports.updateUser = async (req, res) => {
|
||||
if (Number(req.params.id) === req.user.user_id && updates.acc_type !== undefined)
|
||||
return R.error(res, 'Admins cannot change their own role.', 400);
|
||||
|
||||
// Enrich personal_info before saving
|
||||
if (updates.personal_info) {
|
||||
if (updates.personal_info)
|
||||
updates.personal_info = enrichPersonalInfo(updates.personal_info);
|
||||
}
|
||||
|
||||
updates.updatedBy = req.user.user_id; // ← who updated
|
||||
updates.updatedBy = req.user.user_id;
|
||||
|
||||
await user.update(updates);
|
||||
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);
|
||||
console.error('[ADMIN][UPDATE USER]', err);
|
||||
return R.error(res, 'Could not update user.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── DEACTIVATE user (soft delete) ────────────────────────────────────────────
|
||||
// ─── DEACTIVATE (soft delete) ─────────────────────────────────────────────────
|
||||
|
||||
exports.deactivateUser = async (req, res) => {
|
||||
try {
|
||||
if (Number(req.params.id) === req.user.user_id)
|
||||
@@ -172,11 +158,7 @@ exports.deactivateUser = async (req, res) => {
|
||||
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, // ← who deactivated
|
||||
});
|
||||
|
||||
await user.update({ is_active: false, deletedBy: req.user.user_id });
|
||||
await user.destroy();
|
||||
|
||||
await mdl_UserSessions.update(
|
||||
@@ -186,121 +168,89 @@ exports.deactivateUser = async (req, res) => {
|
||||
|
||||
return R.success(res, 'User deactivated successfully.');
|
||||
} catch (err) {
|
||||
console.error("[ADMIN][DEACTIVATE USER]", err);
|
||||
console.error('[ADMIN][DEACTIVATE USER]', err);
|
||||
return R.error(res, 'Could not deactivate user.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── BULK DEACTIVATE users ────────────────────────────────────────────────────
|
||||
// ─── BULK DEACTIVATE ──────────────────────────────────────────────────────────
|
||||
|
||||
exports.bulkDeactivateUsers = async (req, res) => {
|
||||
try {
|
||||
const { ids } = req.body; // expects { ids: [1, 2, 3] }
|
||||
|
||||
if (!Array.isArray(ids) || ids.length === 0)
|
||||
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 },
|
||||
});
|
||||
const users = await mdl_Users.findAll({ where: { user_id: ids } });
|
||||
if (!users.length) return R.error(res, 'No users found.', 404);
|
||||
|
||||
if (!users.length)
|
||||
return R.error(res, 'No users found.', 404);
|
||||
|
||||
// Filter only currently active users — skip already-deactivated ones
|
||||
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);
|
||||
|
||||
// Soft-delete + mark who deactivated them
|
||||
await mdl_Users.update(
|
||||
{ is_active: false, deletedBy: req.user.user_id },
|
||||
{ where: { user_id: activeIds } }
|
||||
);
|
||||
|
||||
// Sequelize paranoid bulk destroy (sets deletedAt)
|
||||
await mdl_Users.destroy({
|
||||
where: { user_id: activeIds },
|
||||
});
|
||||
|
||||
// Force logout all their active sessions
|
||||
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,
|
||||
},
|
||||
},
|
||||
{ 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,
|
||||
// Inform caller if some were already inactive and got skipped
|
||||
skipped_ids: ids.filter((id) => !activeIds.includes(id)),
|
||||
skipped_ids: ids.filter((id) => !activeIds.includes(id)),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[ADMIN][BULK DEACTIVATE USERS]", err);
|
||||
console.error('[ADMIN][BULK DEACTIVATE USERS]', err);
|
||||
return R.error(res, 'Could not deactivate users.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── RESTORE user ──────────────────────────────────────────────────────────────
|
||||
// ─── RESTORE ──────────────────────────────────────────────────────────────────
|
||||
|
||||
exports.restoreUser = async (req, res) => {
|
||||
try {
|
||||
const user = await mdl_Users.findOne({
|
||||
where: { user_id: req.params.id },
|
||||
paranoid: false,
|
||||
where: { user_id: req.params.id }, paranoid: false,
|
||||
});
|
||||
if (!user) return R.error(res, 'User not found.', 404);
|
||||
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, // ← who restored
|
||||
deletedBy: null
|
||||
});
|
||||
await user.update({ is_active: true, updatedBy: req.user.user_id, deletedBy: null });
|
||||
|
||||
return R.success(res, 'User restored successfully.');
|
||||
} catch (err) {
|
||||
console.error("[ADMIN][RESTORE USER]", err);
|
||||
console.error('[ADMIN][RESTORE USER]', err);
|
||||
return R.error(res, 'Could not restore user.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── BULK RESTORE users ────────────────────────────────────────────────────────
|
||||
// ─── BULK RESTORE ─────────────────────────────────────────────────────────────
|
||||
|
||||
exports.bulkRestoreUsers = async (req, res) => {
|
||||
try {
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!Array.isArray(ids) || ids.length === 0)
|
||||
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,
|
||||
});
|
||||
const users = await mdl_Users.findAll({ where: { user_id: ids }, paranoid: false });
|
||||
if (!users.length) return R.error(res, 'No users found.', 404);
|
||||
|
||||
if (!users.length)
|
||||
return R.error(res, 'No users found.', 404);
|
||||
|
||||
// Filter only soft-deleted users — skip already-active ones
|
||||
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 }
|
||||
@@ -308,278 +258,26 @@ exports.bulkRestoreUsers = async (req, res) => {
|
||||
|
||||
return R.success(res, `${deletedIds.length} user(s) restored successfully.`, {
|
||||
restored_ids: deletedIds,
|
||||
skipped_ids: ids.filter((id) => !deletedIds.includes(id)),
|
||||
skipped_ids: ids.filter((id) => !deletedIds.includes(id)),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[ADMIN][BULK RESTORE USERS]", err);
|
||||
console.error('[ADMIN][BULK RESTORE USERS]', err);
|
||||
return R.error(res, 'Could not restore users.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getGroups = async (req, res) => {
|
||||
try {
|
||||
const result = await paginate(mdl_UserGroups, req, {
|
||||
excludeAttributes: groupExclude,
|
||||
jsonbSchemas: groupSchemas,
|
||||
computedAttributes: groupComputed, // ← just pass it, paginate handles the rest
|
||||
auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
|
||||
});
|
||||
// ─── SESSIONS ─────────────────────────────────────────────────────────────────
|
||||
|
||||
return R.success(res, 'Groups retrieved.', result);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][GET GROUPS]', err);
|
||||
return R.error(res, 'Could not retrieve groups.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getGroup = async (req, res) => {
|
||||
try {
|
||||
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
||||
if (!group) return R.error(res, 'Group not found.', 404);
|
||||
|
||||
const members = await paginate(mdl_Users, req, {
|
||||
excludeAttributes: groupExclude,
|
||||
jsonbSchemas: groupSchemas,
|
||||
computedAttributes: groupComputed,
|
||||
auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
|
||||
findOptions: {
|
||||
paranoid: false,
|
||||
where: {
|
||||
deletedAt: { [Op.ne]: null },
|
||||
is_active: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return R.success(res, 'Group retrieved.', { group, members });
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][GET GROUP]', err);
|
||||
return R.error(res, 'Could not retrieve group.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.createGroup = async (req, res) => {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
if (!name) return R.error(res, 'Group name is required.', 400);
|
||||
|
||||
const group = await mdl_UserGroups.create({
|
||||
name,
|
||||
description,
|
||||
createdBy: req.user.user_id,
|
||||
});
|
||||
|
||||
return R.success(res, 'Group created.', group, 201);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][CREATE GROUP]', err);
|
||||
return R.error(res, 'Could not create group.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateGroup = async (req, res) => {
|
||||
try {
|
||||
const { name, description } = req.body;
|
||||
|
||||
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
||||
if (!group) return R.error(res, 'Group not found.', 404);
|
||||
|
||||
if (name !== undefined) group.name = name;
|
||||
if (description !== undefined) group.description = description;
|
||||
group.updatedBy = req.user.user_id;
|
||||
await group.save();
|
||||
|
||||
return R.success(res, 'Group updated.', group);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][UPDATE GROUP]', err);
|
||||
return R.error(res, 'Could not update group.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.deactivateGroup = async (req, res) => {
|
||||
try {
|
||||
const group = await mdl_UserGroups.findByPk(req.params.gid);
|
||||
if (!group) return R.error(res, 'Group not found.', 404);
|
||||
if (!group.is_active) return R.error(res, 'Group is already deactivated.', 400);
|
||||
|
||||
await group.update({
|
||||
is_active: false,
|
||||
updatedBy: req.user.user_id,
|
||||
deletedBy: req.user.user_id,
|
||||
});
|
||||
await group.destroy();
|
||||
|
||||
return R.success(res, 'Group deactivated.');
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][DEACTIVATE GROUP]', err);
|
||||
return R.error(res, 'Could not deactivate group.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.restoreGroup = async (req, res) => {
|
||||
try {
|
||||
const group = await mdl_UserGroups.findOne({
|
||||
where: { group_id: req.params.gid },
|
||||
paranoid: false, // ← needed to find soft-deleted rows
|
||||
});
|
||||
if (!group) return R.error(res, 'Group not found.', 404);
|
||||
if (group.is_active) return R.error(res, 'Group is already active.', 400);
|
||||
|
||||
await group.restore();
|
||||
await group.update({
|
||||
is_active: true,
|
||||
updatedBy: req.user.user_id,
|
||||
deletedBy: null,
|
||||
});
|
||||
|
||||
return R.success(res, 'Group restored.');
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][RESTORE GROUP]', err);
|
||||
return R.error(res, 'Could not restore group.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Group membership ──────────────────────────────────────────────────────────
|
||||
// Get List of Members in a Group, to be added
|
||||
exports.getUsersNotInGroup = async (req, res) => {
|
||||
try {
|
||||
const { gid: group_id } = req.params;
|
||||
|
||||
const members = await mdl_UserGroupMembers.findAll({
|
||||
where: { group_id },
|
||||
attributes: ['user_id'],
|
||||
});
|
||||
|
||||
const memberIds = members.map(m => m.user_id);
|
||||
|
||||
const users = await mdl_Users.findAll({
|
||||
where: {
|
||||
user_id: { [Op.notIn]: memberIds.length ? memberIds : [0] },
|
||||
},
|
||||
attributes: [
|
||||
'user_id',
|
||||
[Sequelize.literal(`("User"."personal_info"->'name'->>'full_name')`), 'full_name'],
|
||||
],
|
||||
});
|
||||
|
||||
return R.success(res, 'Users fetched.', users);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][GET USERS NOT IN GROUP]', err);
|
||||
return R.error(res, 'Could not fetch users.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// Get List of Members in a Group, to be removed
|
||||
exports.getUsersInGroup = async (req, res) => {
|
||||
try {
|
||||
const { gid: group_id } = req.params;
|
||||
|
||||
const group = await mdl_UserGroups.findByPk(group_id, {
|
||||
include: [{
|
||||
model: mdl_Users,
|
||||
as: 'members',
|
||||
through: { attributes: [] },
|
||||
attributes: [
|
||||
'user_id',
|
||||
[Sequelize.literal(`("members"."personal_info"->'name'->>'full_name')`), 'full_name'],
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
if (!group) return R.error(res, 'Group not found.', 404);
|
||||
|
||||
return R.success(res, 'Group members fetched.', group.members);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][GET USERS IN GROUP]', err);
|
||||
return R.error(res, 'Could not fetch group members.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.addUserToGroup = async (req, res) => {
|
||||
try {
|
||||
const { gid: group_id } = req.params;
|
||||
const { user_ids } = req.body;
|
||||
|
||||
if (!Array.isArray(user_ids) || user_ids.length === 0)
|
||||
return R.error(res, 'No users provided.', 400);
|
||||
|
||||
const existingUsers = await mdl_Users.findAll({
|
||||
where: { user_id: user_ids },
|
||||
attributes: ['user_id'],
|
||||
});
|
||||
|
||||
const existingUserIds = existingUsers.map(u => u.user_id);
|
||||
const notFound = user_ids.filter(id => !existingUserIds.includes(id));
|
||||
|
||||
if (notFound.length > 0)
|
||||
return R.error(res, `Users not found: ${notFound.join(', ')}`, 404);
|
||||
|
||||
// Restore soft-deleted rows
|
||||
await mdl_UserGroupMembers.restore({
|
||||
where: { user_id: user_ids, group_id },
|
||||
});
|
||||
|
||||
await mdl_UserGroupMembers.update(
|
||||
{ deletedBy: null, updatedBy: req.user.user_id },
|
||||
{ where: { user_id: user_ids, group_id }, paranoid: false }
|
||||
);
|
||||
|
||||
// Insert any that didn't exist at all
|
||||
await mdl_UserGroupMembers.bulkCreate(
|
||||
user_ids.map(user_id => ({ user_id, group_id, createdBy: req.user.user_id })),
|
||||
{ ignoreDuplicates: true }
|
||||
);
|
||||
|
||||
return R.success(res, 'Users added to group.');
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][ADD USERS TO GROUP]', err);
|
||||
return R.error(res, 'Could not add users to group.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.removeUserFromGroup = async (req, res) => {
|
||||
try {
|
||||
const { gid: group_id } = req.params;
|
||||
const { user_ids } = req.body;
|
||||
|
||||
if (!Array.isArray(user_ids) || user_ids.length === 0)
|
||||
return R.error(res, 'No users provided.', 400);
|
||||
|
||||
const existingMembers = await mdl_UserGroupMembers.findAll({
|
||||
where: { user_id: user_ids, group_id },
|
||||
attributes: ['user_id'],
|
||||
});
|
||||
|
||||
const existingMemberIds = existingMembers.map(m => m.user_id);
|
||||
const notFound = user_ids.filter(id => !existingMemberIds.includes(id));
|
||||
|
||||
if (notFound.length > 0)
|
||||
return R.error(res, `Memberships not found for users: ${notFound.join(', ')}`, 404);
|
||||
|
||||
await mdl_UserGroupMembers.update(
|
||||
{ deletedBy: req.user.user_id },
|
||||
{ where: { user_id: user_ids, group_id } }
|
||||
);
|
||||
await mdl_UserGroupMembers.destroy({
|
||||
where: { user_id: user_ids, group_id },
|
||||
});
|
||||
|
||||
return R.success(res, 'Users removed from group.');
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][REMOVE USERS FROM GROUP]', err);
|
||||
return R.error(res, 'Could not remove users from group.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Admin sessions management ─────────────────────────────────────────────────
|
||||
exports.getUserSessions = async (req, res) => {
|
||||
try {
|
||||
const sessions = await mdl_UserSessions.findAll({
|
||||
where: { user_id: req.params.id },
|
||||
where: { user_id: req.params.id },
|
||||
attributes: { exclude: ['refresh_token_hash'] },
|
||||
order: [['createdAt', 'DESC']],
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -588,105 +286,98 @@ 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 } });
|
||||
|
||||
await session.update({
|
||||
is_active: false,
|
||||
logout_info: { date: new Date().toISOString(), forced_by: req.user.user_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 = async (req, res) => {
|
||||
try {
|
||||
const { field } = req.query;
|
||||
if (!field) return R.error(res, 'Field is required.', 400);
|
||||
|
||||
const allowedFields = Object.keys(mdl_Users.rawAttributes).filter(
|
||||
f => !['password', 'otp_code', 'otp_expires_at', 'personal_info'].includes(f)
|
||||
(f) => !['password', 'otp_code', 'otp_expires_at', 'personal_info'].includes(f)
|
||||
);
|
||||
|
||||
const dateFields = ['createdAt', 'updatedAt', 'deletedAt', 'modifiedAt'];
|
||||
|
||||
if (!field.includes('.')) {
|
||||
if (!allowedFields.includes(field))
|
||||
return R.error(res, 'Invalid or restricted field.', 400);
|
||||
|
||||
// ─── Audit By fields — return names instead of IDs ───────────────────
|
||||
if (auditByFields.includes(field)) {
|
||||
const [rows] = await sequelize.query(`
|
||||
SELECT DISTINCT u2."personal_info"->'name'->>'full_name' AS value
|
||||
FROM users u1
|
||||
JOIN users u2 ON u2.user_id = u1."${field}"
|
||||
WHERE u1."${field}" IS NOT NULL
|
||||
AND u2."personal_info"->'name'->>'full_name' IS NOT NULL
|
||||
ORDER BY value ASC
|
||||
SELECT DISTINCT u2."personal_info"->'name'->>'full_name' AS value
|
||||
FROM users u1
|
||||
JOIN users u2 ON u2.user_id = u1."${field}"
|
||||
WHERE u1."${field}" IS NOT NULL
|
||||
AND u2."personal_info"->'name'->>'full_name' IS NOT NULL
|
||||
ORDER BY value ASC
|
||||
`);
|
||||
|
||||
const values = rows.map(r => r.value).filter(Boolean);
|
||||
return R.success(res, 'Field values retrieved.', values);
|
||||
return R.success(res, 'Field values retrieved.', rows.map((r) => r.value).filter(Boolean));
|
||||
}
|
||||
|
||||
// ─── Date fields ──────────────────────────────────────────────────────
|
||||
if (dateFields.includes(field)) {
|
||||
const results = await mdl_Users.findAll({
|
||||
attributes: [
|
||||
[Sequelize.fn('DISTINCT', Sequelize.fn('DATE', Sequelize.col(field))), 'value'],
|
||||
],
|
||||
attributes: [[Sequelize.fn('DISTINCT', Sequelize.fn('DATE', Sequelize.col(field))), 'value']],
|
||||
where: { [field]: { [Op.ne]: null } },
|
||||
order: [[Sequelize.fn('DATE', Sequelize.col(field)), 'DESC']],
|
||||
raw: true,
|
||||
raw: true,
|
||||
});
|
||||
|
||||
const values = results.map(r => r.value).filter(Boolean);
|
||||
return R.success(res, 'Field values retrieved.', values);
|
||||
return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean));
|
||||
}
|
||||
|
||||
// ─── Regular fields ───────────────────────────────────────────────────
|
||||
const results = await mdl_Users.findAll({
|
||||
attributes: [[Sequelize.fn('DISTINCT', Sequelize.col(field)), 'value']],
|
||||
where: { [field]: { [Op.ne]: null } },
|
||||
raw: true,
|
||||
raw: true,
|
||||
});
|
||||
|
||||
const values = results.map(r => r.value).filter(Boolean).sort();
|
||||
return R.success(res, 'Field values retrieved.', values);
|
||||
return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean).sort());
|
||||
}
|
||||
|
||||
// ─── JSONB dot-notation fields ─────────────────────────────────────────
|
||||
// ─── JSONB dot-notation ───────────────────────────────────────────────
|
||||
const [column, ...pathParts] = field.split('.');
|
||||
const keys = [...pathParts];
|
||||
const keys = [...pathParts];
|
||||
const lastKey = keys.pop();
|
||||
const jsonbPath = keys.length
|
||||
? `"${column}"->${keys.map(k => `'${k}'`).join('->')}->>'${lastKey}'`
|
||||
? `"${column}"->${keys.map((k) => `'${k}'`).join('->')}->>'${lastKey}'`
|
||||
: `"${column}"->>'${lastKey}'`;
|
||||
|
||||
const results = await mdl_Users.findAll({
|
||||
attributes: [[Sequelize.literal(`DISTINCT ${jsonbPath}`), 'value']],
|
||||
where: Sequelize.literal(`${jsonbPath} IS NOT NULL`),
|
||||
raw: true,
|
||||
raw: true,
|
||||
});
|
||||
|
||||
const values = results.map(r => r.value).filter(Boolean).sort();
|
||||
return R.success(res, 'Field values retrieved.', values);
|
||||
|
||||
return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean).sort());
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][GET USER FIELD VALUES]', err);
|
||||
return R.error(res, 'Could not retrieve field values.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── ARCHIVED ─────────────────────────────────────────────────────────────────
|
||||
|
||||
exports.getArchivedUsers = async (req, res) => {
|
||||
try {
|
||||
const result = await paginate(mdl_Users, req, {
|
||||
excludeAttributes: usersExclude,
|
||||
jsonbSchemas: usersSchemas,
|
||||
jsonbColumn: 'personal_info',
|
||||
auditOptions: { mdl_Users, parentAlias: 'User' },
|
||||
jsonbSchemas: usersSchemas,
|
||||
jsonbColumn: 'personal_info',
|
||||
auditOptions: { mdl_Users, parentAlias: 'User' },
|
||||
findOptions: {
|
||||
paranoid: false, // ← include soft-deleted rows
|
||||
where: {
|
||||
deletedAt: { [Op.ne]: null }, // ← only archived ones
|
||||
is_active: false,
|
||||
},
|
||||
paranoid: false,
|
||||
where: { deletedAt: { [Op.ne]: null }, is_active: false },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -695,175 +386,4 @@ exports.getArchivedUsers = async (req, res) => {
|
||||
console.error('[ADMIN][GET ARCHIVED USERS]', err);
|
||||
return R.error(res, 'Could not retrieve archived users.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── BULK DEACTIVATE groups ───────────────────────────────────────────────────
|
||||
exports.bulkDeactivateGroups = async (req, res) => {
|
||||
try {
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!Array.isArray(ids) || ids.length === 0)
|
||||
return R.error(res, 'No group IDs provided.', 400);
|
||||
|
||||
const groups = await mdl_UserGroups.findAll({
|
||||
where: { group_id: ids },
|
||||
});
|
||||
|
||||
if (!groups.length)
|
||||
return R.error(res, 'No groups found.', 404);
|
||||
|
||||
const activeGroups = groups.filter((g) => g.is_active && !g.deletedAt);
|
||||
|
||||
if (!activeGroups.length)
|
||||
return R.error(res, 'All selected groups are already deactivated.', 400);
|
||||
|
||||
const activeIds = activeGroups.map((g) => g.group_id);
|
||||
|
||||
await mdl_UserGroups.update(
|
||||
{ is_active: false, deletedBy: req.user.user_id },
|
||||
{ where: { group_id: activeIds } }
|
||||
);
|
||||
|
||||
await mdl_UserGroups.destroy({
|
||||
where: { group_id: activeIds },
|
||||
});
|
||||
|
||||
return R.success(res, `${activeIds.length} group(s) deactivated successfully.`, {
|
||||
deactivated_ids: activeIds,
|
||||
skipped_ids: ids.filter((id) => !activeIds.includes(id)),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][BULK DEACTIVATE GROUPS]', err);
|
||||
return R.error(res, 'Could not deactivate groups.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── BULK RESTORE groups ──────────────────────────────────────────────────────
|
||||
exports.bulkRestoreGroups = async (req, res) => {
|
||||
try {
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!Array.isArray(ids) || ids.length === 0)
|
||||
return R.error(res, 'No group IDs provided.', 400);
|
||||
|
||||
const groups = await mdl_UserGroups.findAll({
|
||||
where: { group_id: ids },
|
||||
paranoid: false,
|
||||
});
|
||||
|
||||
if (!groups.length)
|
||||
return R.error(res, 'No groups found.', 404);
|
||||
|
||||
const deletedGroups = groups.filter((g) => g.deletedAt);
|
||||
|
||||
if (!deletedGroups.length)
|
||||
return R.error(res, 'All selected groups are already active.', 400);
|
||||
|
||||
const deletedIds = deletedGroups.map((g) => g.group_id);
|
||||
|
||||
await mdl_UserGroups.restore({ where: { group_id: deletedIds } });
|
||||
|
||||
await mdl_UserGroups.update(
|
||||
{ is_active: true, updatedBy: req.user.user_id, deletedBy: null },
|
||||
{ where: { group_id: deletedIds }, paranoid: false }
|
||||
);
|
||||
|
||||
return R.success(res, `${deletedIds.length} group(s) restored successfully.`, {
|
||||
restored_ids: deletedIds,
|
||||
skipped_ids: ids.filter((id) => !deletedIds.includes(id)),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][BULK RESTORE GROUPS]', err);
|
||||
return R.error(res, 'Could not restore groups.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getArchivedGroups = async (req, res) => {
|
||||
try {
|
||||
|
||||
const result = await paginate(mdl_UserGroups, req, {
|
||||
excludeAttributes: groupExclude,
|
||||
jsonbSchemas: groupSchemas,
|
||||
computedAttributes: groupComputed, // ← just pass it, paginate handles the rest
|
||||
auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
|
||||
findOptions: {
|
||||
paranoid: false,
|
||||
where: {
|
||||
deletedAt: { [Op.ne]: null },
|
||||
is_active: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return R.success(res, 'Archived groups retrieved.', result);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][GET ARCHIVED GROUPS]', err);
|
||||
return R.error(res, 'Could not retrieve archived groups.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── GET group field values ───────────────────────────────────────────────────
|
||||
exports.getGroupFieldValues = async (req, res) => {
|
||||
try {
|
||||
const { field } = req.query;
|
||||
if (!field) return R.error(res, 'Field is required.', 400);
|
||||
|
||||
const allowedFields = Object.keys(mdl_UserGroups.rawAttributes).filter(
|
||||
(f) => !['deletedAt'].includes(f)
|
||||
);
|
||||
|
||||
const dateFields = ['createdAt', 'updatedAt', 'deletedAt'];
|
||||
|
||||
if (!field.includes('.')) {
|
||||
if (!allowedFields.includes(field))
|
||||
return R.error(res, 'Invalid or restricted field.', 400);
|
||||
|
||||
// ─── Audit By fields — return names instead of IDs ───────────────────
|
||||
if (auditByFields.includes(field)) {
|
||||
const [rows] = await sequelize.query(`
|
||||
SELECT DISTINCT u."personal_info"->'name'->>'full_name' AS value
|
||||
FROM user_groups g
|
||||
JOIN users u ON u.user_id = g."${field}"
|
||||
WHERE g."${field}" IS NOT NULL
|
||||
AND u."personal_info"->'name'->>'full_name' IS NOT NULL
|
||||
ORDER BY value ASC
|
||||
`);
|
||||
|
||||
const values = rows.map((r) => r.value).filter(Boolean);
|
||||
return R.success(res, 'Field values retrieved.', values);
|
||||
}
|
||||
|
||||
// ─── Date fields ──────────────────────────────────────────────────────
|
||||
if (dateFields.includes(field)) {
|
||||
const results = await mdl_UserGroups.findAll({
|
||||
attributes: [
|
||||
[Sequelize.fn('DISTINCT', Sequelize.fn('DATE', Sequelize.col(field))), 'value'],
|
||||
],
|
||||
where: { [field]: { [Op.ne]: null } },
|
||||
order: [[Sequelize.fn('DATE', Sequelize.col(field)), 'DESC']],
|
||||
paranoid: false,
|
||||
raw: true,
|
||||
});
|
||||
|
||||
const values = results.map((r) => r.value).filter(Boolean);
|
||||
return R.success(res, 'Field values retrieved.', values);
|
||||
}
|
||||
|
||||
// ─── Regular fields ───────────────────────────────────────────────────
|
||||
const results = await mdl_UserGroups.findAll({
|
||||
attributes: [[Sequelize.fn('DISTINCT', Sequelize.col(field)), 'value']],
|
||||
where: { [field]: { [Op.ne]: null } },
|
||||
paranoid: false,
|
||||
raw: true,
|
||||
});
|
||||
|
||||
const values = results.map((r) => r.value).filter(Boolean).sort();
|
||||
return R.success(res, 'Field values retrieved.', values);
|
||||
}
|
||||
|
||||
return R.error(res, 'JSONB fields are not supported for groups.', 400);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][GET GROUP FIELD VALUES]', err);
|
||||
return R.error(res, 'Could not retrieve field values.', 500);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user