This commit is contained in:
rgrgogu
2026-05-06 14:15:04 +08:00
parent a8f10a25d7
commit d5fcc574a4
12 changed files with 389 additions and 219 deletions
+91 -10
View File
@@ -19,6 +19,7 @@
* Author: rgrgogu
* Date Created: Oct. 6, 2025
***********************************************************************************************************************************************************************/
const sequelize = require('../../config/db.config')
const { Op, Sequelize } = require('sequelize')
const mdl_Users = require('../../models/users/users.mdl');
const mdl_UserSessions = require('../../models/users/user_sessions.mdl');
@@ -32,14 +33,15 @@ const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require(
const { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas } = require('../../models/users/user_groups.attributes');
const EXCLUDED = ['password', 'otp_code', 'otp_expires_at'];
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy'];
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);
@@ -154,8 +156,8 @@ exports.getGroups = async (req, res) => {
try {
const result = await paginate(mdl_UserGroups, req, {
excludeAttributes: groupExclude,
jsonbSchemas: groupSchemas,
auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
jsonbSchemas: groupSchemas,
auditOptions: { mdl_Users, parentAlias: 'UserGroup' },
findOptions: {
attributes: {
include: [
@@ -186,14 +188,14 @@ exports.getGroup = async (req, res) => {
const members = 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: {
include: [
{
model: mdl_UserGroupMembers,
where: { group_id: req.params.gid },
model: mdl_UserGroupMembers,
where: { group_id: req.params.gid },
attributes: [],
required: true,
},
@@ -443,4 +445,83 @@ exports.terminateSession = async (req, res) => {
} catch (err) {
return R.error(res, 'Could not terminate session.', 500);
}
};
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)
);
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
`);
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_Users.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']],
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_Users.findAll({
attributes: [[Sequelize.fn('DISTINCT', Sequelize.col(field)), 'value']],
where: { [field]: { [Op.ne]: null } },
raw: true,
});
const values = results.map(r => r.value).filter(Boolean).sort();
return R.success(res, 'Field values retrieved.', values);
}
// ─── JSONB dot-notation fields ─────────────────────────────────────────
const [column, ...pathParts] = field.split('.');
const keys = [...pathParts];
const lastKey = keys.pop();
const jsonbPath = keys.length
? `"${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,
});
const values = results.map(r => r.value).filter(Boolean).sort();
return R.success(res, 'Field values retrieved.', values);
} catch (err) {
console.error('[ADMIN][GET USER FIELD VALUES]', err);
return R.error(res, 'Could not retrieve field values.', 500);
}
};
+12 -15
View File
@@ -28,7 +28,7 @@ const bcrypt = require('bcryptjs');
const sequelize = require('../config/db.config')
const mdl_Users = require('../models/users/users.mdl');
const mdl_UserSessions = require('../models/users/user_sessions.mdl');
const { generateTokens, verifyRefreshToken, hashToken } = require('../utils/token.util');
const { generateTokens, verifyRefreshToken, hashToken, shouldRotateRefreshToken } = require('../utils/token.util');
const { generateOTP, getOTPExpiry, isOTPExpired } = require('../utils/otp.util');
const sendEmail = require('../services/email.service');
const R = require('../utils/response.util');
@@ -124,7 +124,7 @@ exports.verifyOTP = async (req, res) => {
sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax', // ← CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
return R.success(res, 'Email verified successfully. You are now logged in.', {
accessToken,
refreshToken,
@@ -234,13 +234,9 @@ exports.refreshToken = async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return R.error(res, 'Refresh token is required.', 400);
console.log('[REFRESH] called, token tail:', refreshToken?.slice(-10))
console.log('[REFRESH] hash:', hashToken(refreshToken))
const decoded = verifyRefreshToken(refreshToken);
const tokenHash = hashToken(refreshToken);
console.log({ user_id: decoded.user_id, refresh_token_hash: tokenHash, is_active: true })
const session = await mdl_UserSessions.findOne({
where: { user_id: decoded.user_id, refresh_token_hash: tokenHash, is_active: true },
});
@@ -249,20 +245,21 @@ exports.refreshToken = async (req, res) => {
const user = await mdl_Users.findByPk(decoded.user_id);
if (!user || !user.is_active) return R.error(res, 'User not found or deactivated.', 401);
const tokens = generateTokens(user);
// Check if refresh token is expired
if (!shouldRotateRefreshToken(decoded)) {
const { accessToken } = generateTokens(user);
return R.success(res, 'Token refreshed.', { accessToken, user: safeUser(user) });
}
// Rotate refresh token
// ─── Rotate refresh token ───────────────────────────────────────────────────
const tokens = generateTokens(user);
await session.update({ refresh_token_hash: hashToken(tokens.refreshToken) });
console.log('[REFRESH] old token tail:', refreshToken?.slice(-10))
console.log('[REFRESH] new token tail:', tokens.refreshToken?.slice(-10))
console.log('[REFRESH] are they different:', refreshToken !== tokens.refreshToken)
res.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true, // ← JS cannot read this
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax', // ← CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
return R.success(res, 'Token refreshed.', { ...tokens, user: safeUser(user) });