Files
project.linkedcorp.net/backend/src/modules/auth/sessions-service.js
T
2026-10-26 16:42:19 -05:00

64 lines
3.4 KiB
JavaScript

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');
}