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:
Milton Gonzalez
2026-10-26 16:42:19 -05:00
parent 544928d04a
commit a6cc41666e
34 changed files with 464 additions and 110 deletions
+17 -13
View File
@@ -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 });
}