feat: version 4.3.0 -- Sesiones activas y pagina Mi cuenta
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { env } from '../config/env.js';
|
||||
import { pool } from '../config/db.js';
|
||||
import { fail } from '../utils/http.js';
|
||||
import { loadModules } from './modules.js';
|
||||
import { validateSession } from '../modules/auth/sessions-service.js';
|
||||
|
||||
export async function requireAuth(req, res, next) {
|
||||
const header = req.headers.authorization || '';
|
||||
@@ -18,21 +19,30 @@ export async function requireAuth(req, res, next) {
|
||||
|
||||
// Revalidar en cada request que la cuenta siga activa (un JWT firmado no basta).
|
||||
const [rows] = await pool.query(
|
||||
`SELECT u.id, u.name, u.email, u.is_active, u.session_version, u.department_id, r.code AS role_code
|
||||
`SELECT u.id, u.name, u.email, u.is_active, u.session_version, u.department_id, d.name AS department_name, r.code AS role_code, r.name AS role_name
|
||||
FROM users u JOIN roles r ON r.id = u.role_id
|
||||
LEFT JOIN departments d ON d.id = u.department_id
|
||||
WHERE u.id = ? LIMIT 1`,
|
||||
[payload.sub]
|
||||
);
|
||||
const user = rows[0];
|
||||
if (!user || !user.is_active || payload.sv !== user.session_version) return fail(res, 401, 'Sesion invalida o revocada');
|
||||
if (!user || !user.is_active || payload.sv !== user.session_version) return fail(res, 401, 'Sesion invalida o revocada', 'SESSION_REVOKED');
|
||||
// Token emitido antes de las sesiones registradas (sin sid): se trata como vencido, no como cerrado.
|
||||
if (!payload.sid) return fail(res, 401, 'Sesion vencida');
|
||||
const session = await validateSession(payload.sid, user.id);
|
||||
if (!session) return fail(res, 401, 'Sesion cerrada o vencida', 'SESSION_REVOKED');
|
||||
|
||||
req.currentUser = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
roleCode: user.role_code,
|
||||
roleName: user.role_name,
|
||||
departmentId: user.department_id,
|
||||
departmentName: user.department_name,
|
||||
modules: await loadModules(user.id, user.role_code),
|
||||
sessionId: session.id,
|
||||
sessionPublicId: payload.sid,
|
||||
};
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { ok, fail, asyncHandler } from '../../utils/http.js';
|
||||
import * as authService from './service.js';
|
||||
import * as sessions from './sessions-service.js';
|
||||
|
||||
export const postLogin = asyncHandler(async (req, res) => {
|
||||
const { email, password } = req.body || {};
|
||||
if (!email || !password) return fail(res, 400, 'Email y clave son obligatorios');
|
||||
const result = await authService.login(email, password);
|
||||
const result = await authService.login(email, password, req);
|
||||
ok(res, result);
|
||||
});
|
||||
|
||||
export const postLogout = asyncHandler(async (req, res) => {
|
||||
// Stateless JWT: el cliente descarta el token. Se deja el endpoint para uso futuro (blacklist).
|
||||
await sessions.revokeSession(req.currentUser.id, req.currentUser.sessionId, req.currentUser.id, 'logout', req.currentUser.sessionPublicId);
|
||||
ok(res, { loggedOut: true });
|
||||
});
|
||||
|
||||
export const getMe = asyncHandler(async (req, res) => {
|
||||
ok(res, req.currentUser);
|
||||
const { sessionId, sessionPublicId, ...user } = req.currentUser;
|
||||
ok(res, user);
|
||||
});
|
||||
|
||||
export const postRegister = asyncHandler(async (req, res) => {
|
||||
const result = await authService.register(req.body || {});
|
||||
const result = await authService.register(req.body || {}, req);
|
||||
ok(res, result, 201);
|
||||
});
|
||||
|
||||
@@ -33,7 +35,7 @@ export const getInvite = asyncHandler(async (req, res) => {
|
||||
|
||||
export const postAcceptInvite = asyncHandler(async (req, res) => {
|
||||
const { token, name, password } = req.body || {};
|
||||
const result = await authService.acceptInvite(token, name, password);
|
||||
const result = await authService.acceptInvite(token, name, password, req);
|
||||
ok(res, result, 201);
|
||||
});
|
||||
|
||||
@@ -52,6 +54,13 @@ export const postResetPassword = asyncHandler(async (req, res) => {
|
||||
|
||||
export const putChangePassword = asyncHandler(async (req, res) => {
|
||||
const { currentPassword, newPassword } = req.body || {};
|
||||
await authService.changePassword(req.currentUser.id, currentPassword, newPassword);
|
||||
await authService.changePassword(req.currentUser.id, currentPassword, newPassword, req.currentUser.sessionPublicId);
|
||||
ok(res, { changed: true });
|
||||
});
|
||||
|
||||
export const getSessions = asyncHandler(async (req, res) => ok(res,
|
||||
await sessions.listSessions(req.currentUser.id, req.currentUser.sessionPublicId)));
|
||||
export const deleteSession = asyncHandler(async (req, res) => ok(res,
|
||||
await sessions.revokeSession(req.currentUser.id, req.params.id, req.currentUser.id, 'user', req.currentUser.sessionPublicId)));
|
||||
export const postRevokeOthers = asyncHandler(async (req, res) => ok(res,
|
||||
await sessions.revokeAll(req.currentUser.id, req.currentUser.id, 'user', req.currentUser.sessionPublicId)));
|
||||
|
||||
@@ -3,7 +3,7 @@ import rateLimit from 'express-rate-limit';
|
||||
import { requireAuth } from '../../middleware/auth.js';
|
||||
import {
|
||||
postLogin, postLogout, getMe, postRegister, getVerifyEmail, postForgotPassword, postResetPassword,
|
||||
putChangePassword, getInvite, postAcceptInvite,
|
||||
putChangePassword, getInvite, postAcceptInvite, getSessions, deleteSession, postRevokeOthers,
|
||||
} from './controller.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -26,5 +26,8 @@ router.post('/accept-invite', loginLimiter, postAcceptInvite);
|
||||
router.post('/logout', requireAuth, postLogout);
|
||||
router.get('/me', requireAuth, getMe);
|
||||
router.put('/change-password', requireAuth, loginLimiter, putChangePassword);
|
||||
router.get('/sessions', requireAuth, getSessions);
|
||||
router.delete('/sessions/:id', requireAuth, deleteSession);
|
||||
router.post('/sessions/revoke-others', requireAuth, postRevokeOthers);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import crypto from 'node:crypto';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { pool } from '../../config/db.js';
|
||||
import { env } from '../../config/env.js';
|
||||
import { HttpError } from '../../utils/http.js';
|
||||
@@ -9,6 +8,7 @@ import { loadModules } from '../../middleware/modules.js';
|
||||
import { sendMail } from '../../utils/mailer.js';
|
||||
import { createNotification } from '../notifications/service.js';
|
||||
import { verificationEmail, recoveryEmail } from '../../utils/emailHtml.js';
|
||||
import { issueSession, revokeAll } from './sessions-service.js';
|
||||
|
||||
const STATUS_MESSAGES = {
|
||||
pending_verification: 'Debes confirmar tu correo antes de iniciar sesion. Revisa tu bandeja de entrada.',
|
||||
@@ -20,11 +20,12 @@ function newToken() {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
export async function login(email, password) {
|
||||
export async function login(email, password, req) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT u.id, u.name, u.email, u.password_hash, u.is_active, u.status, u.department_id, u.session_version,
|
||||
r.code AS role_code, r.name AS role_name
|
||||
r.code AS role_code, r.name AS role_name, d.name AS department_name
|
||||
FROM users u JOIN roles r ON r.id = u.role_id
|
||||
LEFT JOIN departments d ON d.id = u.department_id
|
||||
WHERE u.email = ? LIMIT 1`,
|
||||
[email]
|
||||
);
|
||||
@@ -37,7 +38,7 @@ export async function login(email, password) {
|
||||
if (STATUS_MESSAGES[user.status]) throw new HttpError(403, STATUS_MESSAGES[user.status]);
|
||||
if (!user.is_active) throw new HttpError(403, 'Cuenta inactiva. Contacta a un administrador.');
|
||||
|
||||
const token = jwt.sign({ sub: user.id, sv: user.session_version }, env.jwtSecret, { expiresIn: env.jwtExpires });
|
||||
const token = await issueSession(user.id, user.session_version, req);
|
||||
|
||||
await logAudit({ userId: user.id, action: 'login', entityType: 'auth', entityId: user.id });
|
||||
|
||||
@@ -50,12 +51,13 @@ export async function login(email, password) {
|
||||
roleCode: user.role_code,
|
||||
roleName: user.role_name,
|
||||
departmentId: user.department_id,
|
||||
departmentName: user.department_name,
|
||||
modules: await loadModules(user.id, user.role_code),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function register({ name, email, password }) {
|
||||
export async function register({ name, email, password }, req) {
|
||||
if (!name || !email || !password || password.length < 8) {
|
||||
throw new HttpError(400, 'Nombre, correo y una clave de al menos 8 caracteres son obligatorios');
|
||||
}
|
||||
@@ -89,7 +91,7 @@ export async function register({ name, email, password }) {
|
||||
} finally { connection.release(); }
|
||||
if (bootstrap) {
|
||||
await logAudit({ userId: result.insertId, action: 'register_bootstrap_superadmin', entityType: 'user', entityId: result.insertId });
|
||||
const token = jwt.sign({ sub: result.insertId, sv: 1 }, env.jwtSecret, { expiresIn: env.jwtExpires });
|
||||
const token = await issueSession(result.insertId, 1, req);
|
||||
return { bootstrap: true, token, user: { id: result.insertId, name, email, roleCode: 'superadmin', roleName: 'Super Administrador', departmentId: null, modules: await loadModules(result.insertId, 'superadmin') } };
|
||||
}
|
||||
await logAudit({ userId: result.insertId, action: 'register', entityType: 'user', entityId: result.insertId });
|
||||
@@ -153,7 +155,7 @@ export async function getInviteInfo(token) {
|
||||
// inviteUser): la persona elige su nombre y clave. A diferencia del autorregistro publico, no
|
||||
// pasa por aprobacion de un admin — quien la invito ya la vetó al agregarla a su proyecto. Devuelve
|
||||
// sesion iniciada de una vez, igual que el bootstrap del primer usuario.
|
||||
export async function acceptInvite(token, name, password) {
|
||||
export async function acceptInvite(token, name, password, req) {
|
||||
if (!token || !name || !password || password.length < 8) {
|
||||
throw new HttpError(400, 'Nombre, token y una clave de al menos 8 caracteres son obligatorios');
|
||||
}
|
||||
@@ -174,16 +176,16 @@ export async function acceptInvite(token, name, password) {
|
||||
await logAudit({ userId: user.id, action: 'accept_invite', entityType: 'user', entityId: user.id });
|
||||
|
||||
const [full] = await pool.query(
|
||||
`SELECT u.id, u.name, u.email, u.department_id, u.session_version, r.code AS role_code, r.name AS role_name
|
||||
FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = ?`,
|
||||
`SELECT u.id, u.name, u.email, u.department_id, u.session_version, r.code AS role_code, r.name AS role_name, d.name AS department_name
|
||||
FROM users u JOIN roles r ON r.id = u.role_id LEFT JOIN departments d ON d.id = u.department_id WHERE u.id = ?`,
|
||||
[user.id]
|
||||
);
|
||||
const jwtToken = jwt.sign({ sub: user.id, sv: full[0].session_version }, env.jwtSecret, { expiresIn: env.jwtExpires });
|
||||
const jwtToken = await issueSession(user.id, full[0].session_version, req);
|
||||
return {
|
||||
token: jwtToken,
|
||||
user: {
|
||||
id: full[0].id, name: full[0].name, email: full[0].email,
|
||||
roleCode: full[0].role_code, roleName: full[0].role_name, departmentId: full[0].department_id,
|
||||
roleCode: full[0].role_code, roleName: full[0].role_name, departmentId: full[0].department_id, departmentName: full[0].department_name,
|
||||
modules: await loadModules(full[0].id, full[0].role_code),
|
||||
},
|
||||
};
|
||||
@@ -227,11 +229,12 @@ export async function resetPassword(token, newPassword) {
|
||||
`UPDATE users SET password_hash = ?, session_version = session_version + 1, reset_token = NULL, reset_token_expires = NULL WHERE id = ?`,
|
||||
[passwordHash, user.id]
|
||||
);
|
||||
await revokeAll(user.id, user.id, 'password_reset');
|
||||
await logAudit({ userId: user.id, action: 'reset_password', entityType: 'user', entityId: user.id });
|
||||
}
|
||||
|
||||
// Cambio de clave desde la app, con sesion activa (distinto del olvide-clave: exige la clave actual).
|
||||
export async function changePassword(userId, currentPassword, newPassword) {
|
||||
export async function changePassword(userId, currentPassword, newPassword, currentSessionId) {
|
||||
if (!currentPassword || !newPassword || newPassword.length < 8) {
|
||||
throw new HttpError(400, 'La clave actual y una clave nueva de al menos 8 caracteres son obligatorias');
|
||||
}
|
||||
@@ -243,6 +246,7 @@ export async function changePassword(userId, currentPassword, newPassword) {
|
||||
if (!validPassword) throw new HttpError(400, 'La clave actual no es correcta');
|
||||
|
||||
const passwordHash = await bcrypt.hash(newPassword, 12);
|
||||
await pool.query('UPDATE users SET password_hash = ?, session_version = session_version + 1 WHERE id = ?', [passwordHash, userId]);
|
||||
await pool.query('UPDATE users SET password_hash = ? WHERE id = ?', [passwordHash, userId]);
|
||||
await revokeAll(userId, userId, 'password_change', currentSessionId);
|
||||
await logAudit({ userId, action: 'change_password', entityType: 'user', entityId: userId });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { pool } from '../../config/db.js';
|
||||
|
||||
export async function create({ publicId, userId, ip, userAgent, lifetimeSeconds }) {
|
||||
await pool.query(
|
||||
`INSERT INTO user_sessions (public_id, user_id, ip, user_agent, expires_at)
|
||||
VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL ? SECOND))`,
|
||||
[publicId, userId, ip, userAgent, lifetimeSeconds]
|
||||
);
|
||||
}
|
||||
|
||||
export async function findValid(publicId, userId) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id FROM user_sessions WHERE public_id = ? AND user_id = ?
|
||||
AND revoked_at IS NULL AND expires_at > NOW() LIMIT 1`, [publicId, userId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function touch(id) {
|
||||
await pool.query(`UPDATE user_sessions SET last_seen_at = NOW()
|
||||
WHERE id = ? AND last_seen_at < DATE_SUB(NOW(), INTERVAL 60 SECOND)`, [id]);
|
||||
}
|
||||
|
||||
export async function list(userId) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, public_id, user_id, ip, user_agent, created_at, last_seen_at, expires_at
|
||||
FROM user_sessions WHERE user_id = ? AND revoked_at IS NULL AND expires_at > NOW()
|
||||
ORDER BY last_seen_at DESC`, [userId]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function findActive(id, userId) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, public_id, user_id, ip, user_agent FROM user_sessions
|
||||
WHERE id = ? AND user_id = ? AND revoked_at IS NULL AND expires_at > NOW() LIMIT 1`, [id, userId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
export async function revoke(id, userId, actorId, reason) {
|
||||
const [result] = await pool.query(
|
||||
`UPDATE user_sessions SET revoked_at = NOW(), revoked_by = ?, revoke_reason = ?
|
||||
WHERE id = ? AND user_id = ? AND revoked_at IS NULL AND expires_at > NOW()`,
|
||||
[actorId, reason, id, userId]
|
||||
);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
export async function cleanup() {
|
||||
await pool.query(`DELETE FROM user_sessions
|
||||
WHERE expires_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
OR revoked_at < DATE_SUB(NOW(), INTERVAL 30 DAY)`);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import crypto from 'node:crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { env } from '../../config/env.js';
|
||||
import { HttpError } from '../../utils/http.js';
|
||||
import { logAudit } from '../../utils/audit.js';
|
||||
import { pool } from '../../config/db.js';
|
||||
import * as repo from './sessions-repository.js';
|
||||
|
||||
function clientName(agent = '') {
|
||||
const browser = /Edg\//.test(agent) ? 'Edge' : /OPR\//.test(agent) ? 'Opera' : /Chrome\//.test(agent) ? 'Chrome' : /Firefox\//.test(agent) ? 'Firefox' : /Safari\//.test(agent) ? 'Safari' : 'unknown';
|
||||
const os = /Windows/.test(agent) ? 'Windows' : /Android/.test(agent) ? 'Android' : /iPhone|iPad/.test(agent) ? 'iOS' : /Mac OS/.test(agent) ? 'macOS' : /Linux/.test(agent) ? 'Linux' : 'unknown';
|
||||
return { browser, os };
|
||||
}
|
||||
|
||||
export async function issueSession(userId, sessionVersion, req) {
|
||||
// Limpieza al iniciar sesion: el backend no tiene un programador de tareas propio.
|
||||
await repo.cleanup();
|
||||
const sid = crypto.randomBytes(16).toString('hex');
|
||||
const token = jwt.sign({ sub: userId, sv: sessionVersion, sid }, env.jwtSecret, { expiresIn: env.jwtExpires });
|
||||
const decoded = jwt.decode(token);
|
||||
await repo.create({ publicId: sid, userId, ip: req.ip?.slice(0, 45) || null,
|
||||
userAgent: req.get('user-agent')?.slice(0, 512) || null, lifetimeSeconds: decoded.exp - decoded.iat });
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function validateSession(sid, userId) {
|
||||
if (typeof sid !== 'string' || !/^[a-f0-9]{32}$/.test(sid)) return null;
|
||||
const session = await repo.findValid(sid, userId);
|
||||
if (session) await repo.touch(session.id);
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function listSessions(userId, currentSessionId = null) {
|
||||
return (await repo.list(userId)).map(({ public_id, user_agent, ...row }) => ({
|
||||
...row, ...clientName(user_agent), current: public_id === currentSessionId,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function revokeSession(userId, sessionId, actorId, reason, currentSessionId = null) {
|
||||
if (!/^[1-9]\d*$/.test(String(sessionId))) throw new HttpError(404, 'Sesion no encontrada');
|
||||
const session = await repo.findActive(sessionId, userId);
|
||||
if (!session) throw new HttpError(404, 'Sesion no encontrada');
|
||||
if (await repo.revoke(session.id, userId, actorId, reason)) {
|
||||
await logAudit({ userId: actorId, action: 'revoke_session', entityType: 'user_session', entityId: session.id,
|
||||
detail: { affectedUserId: userId, ...clientName(session.user_agent), ip: session.ip, reason } });
|
||||
}
|
||||
return { revoked: true, current: session.public_id === currentSessionId };
|
||||
}
|
||||
|
||||
export async function revokeAll(userId, actorId, reason, exceptSessionId = null) {
|
||||
const sessions = await repo.list(userId);
|
||||
for (const session of sessions) {
|
||||
if (session.public_id !== exceptSessionId) await revokeSession(userId, session.id, actorId, reason, exceptSessionId);
|
||||
}
|
||||
return { revoked: sessions.filter((s) => s.public_id !== exceptSessionId).length };
|
||||
}
|
||||
|
||||
export async function assertManageTarget(userId, actor) {
|
||||
if (!Number.isSafeInteger(userId) || userId < 1) throw new HttpError(404, 'Usuario no encontrado');
|
||||
const [rows] = await pool.query(`SELECT u.id, r.code AS role_code FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = ?`, [userId]);
|
||||
if (!rows[0]) throw new HttpError(404, 'Usuario no encontrado');
|
||||
if (rows[0].role_code === 'superadmin' && actor.roleCode !== 'superadmin') throw new HttpError(403, 'No puedes administrar sesiones de un superadministrador');
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ok, asyncHandler } from '../../utils/http.js';
|
||||
import * as service from './service.js';
|
||||
import * as sessions from '../auth/sessions-service.js';
|
||||
|
||||
export const getUsers = asyncHandler(async (req, res) => ok(res, await service.listUsers()));
|
||||
export const getRoles = asyncHandler(async (req, res) => ok(res, await service.listRoles()));
|
||||
@@ -38,3 +39,20 @@ export const getUserModules = asyncHandler(async (req, res) => ok(res, await ser
|
||||
export const putUserModules = asyncHandler(async (req, res) => {
|
||||
ok(res, await service.setUserModules(Number(req.params.id), req.body?.modules, req.currentUser));
|
||||
});
|
||||
|
||||
export const getUserSessions = asyncHandler(async (req, res) => {
|
||||
const userId = Number(req.params.id);
|
||||
await sessions.assertManageTarget(userId, req.currentUser);
|
||||
ok(res, await sessions.listSessions(userId, req.currentUser.id === userId ? req.currentUser.sessionPublicId : null));
|
||||
});
|
||||
export const deleteUserSession = asyncHandler(async (req, res) => {
|
||||
const userId = Number(req.params.id);
|
||||
await sessions.assertManageTarget(userId, req.currentUser);
|
||||
ok(res, await sessions.revokeSession(userId, req.params.sessionId, req.currentUser.id, 'admin', req.currentUser.sessionPublicId));
|
||||
});
|
||||
export const postRevokeUserSessions = asyncHandler(async (req, res) => {
|
||||
const userId = Number(req.params.id);
|
||||
await sessions.assertManageTarget(userId, req.currentUser);
|
||||
const result = await sessions.revokeAll(userId, req.currentUser.id, 'admin');
|
||||
ok(res, { ...result, current: userId === req.currentUser.id });
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { requireAuth, requireRole, CAN_MANAGE_USERS, ROLES } from '../../middlew
|
||||
import { requireModule } from '../../middleware/modules.js';
|
||||
import {
|
||||
getUsers, getRoles, postUser, putUser, patchUserActive, postApproveUser, postRejectUser, postResendUserEmail,
|
||||
getUserModules, putUserModules,
|
||||
getUserModules, putUserModules, getUserSessions, deleteUserSession, postRevokeUserSessions,
|
||||
} from './controller.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -19,5 +19,8 @@ router.post('/:id/reject', requireModule('organization'), requireRole(...CAN_MAN
|
||||
router.get('/:id/modules', requireRole(ROLES.SUPERADMIN), getUserModules);
|
||||
router.put('/:id/modules', requireRole(ROLES.SUPERADMIN), putUserModules);
|
||||
router.post('/:id/resend', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), postResendUserEmail);
|
||||
router.get('/:id/sessions', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), getUserSessions);
|
||||
router.delete('/:id/sessions/:sessionId', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), deleteUserSession);
|
||||
router.post('/:id/sessions/revoke-all', requireModule('organization'), requireRole(...CAN_MANAGE_USERS), postRevokeUserSessions);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MODULES, loadModules } from '../../middleware/modules.js';
|
||||
import { sendMail } from '../../utils/mailer.js';
|
||||
import { env } from '../../config/env.js';
|
||||
import { invitationEmail, verificationEmail } from '../../utils/emailHtml.js';
|
||||
import { revokeAll } from '../auth/sessions-service.js';
|
||||
|
||||
export const listUsers = () => repo.findAll();
|
||||
export const listRoles = () => repo.listRoles();
|
||||
@@ -79,6 +80,7 @@ export async function setUserActive(id, isActive, actor) {
|
||||
if (!existing) throw new HttpError(404, 'Usuario no encontrado');
|
||||
checkTarget(existing, actor);
|
||||
const user = await repo.setActive(id, isActive);
|
||||
if (!isActive) await revokeAll(id, actor.id, 'admin');
|
||||
await logAudit({
|
||||
userId: actor.id,
|
||||
action: isActive ? 'activate' : 'deactivate',
|
||||
|
||||
@@ -2,8 +2,8 @@ export function ok(res, data = null, status = 200) {
|
||||
return res.status(status).json({ success: true, data, error: null });
|
||||
}
|
||||
|
||||
export function fail(res, status, message) {
|
||||
return res.status(status).json({ success: false, data: null, error: message });
|
||||
export function fail(res, status, message, code) {
|
||||
return res.status(status).json({ success: false, data: null, error: message, ...(code ? { code } : {}) });
|
||||
}
|
||||
|
||||
export class HttpError extends Error {
|
||||
|
||||
Reference in New Issue
Block a user