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