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:
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user