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