/** * SESSION MANAGEMENT SERVICE - REFERENCE IMPLEMENTATION * * @description Servicio para gestión de sesiones de usuario. * Mantiene registro de sesiones activas, dispositivos y metadata. * * @usage Copiar y adaptar según necesidades del proyecto. * @origin gamilit/apps/backend/src/modules/auth/services/session-management.service.ts */ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, MoreThan } from 'typeorm'; import * as crypto from 'crypto'; // Adaptar imports según proyecto // import { UserSession } from '../entities'; @Injectable() export class SessionManagementService { constructor( @InjectRepository(UserSession, 'auth') private readonly sessionRepository: Repository, ) {} /** * Crear nueva sesión * * @param userId - ID del usuario * @param metadata - Información del dispositivo/cliente * @returns Sesión creada */ async createSession( userId: string, metadata: SessionMetadata, ): Promise { const session = this.sessionRepository.create({ user_id: userId, refresh_token: this.generateTokenHash(), ip_address: metadata.ip, user_agent: metadata.userAgent, device_type: this.detectDeviceType(metadata.userAgent), expires_at: this.calculateExpiry(7, 'days'), is_revoked: false, }); return this.sessionRepository.save(session); } /** * Obtener sesiones activas de un usuario */ async getActiveSessions(userId: string): Promise { return this.sessionRepository.find({ where: { user_id: userId, is_revoked: false, expires_at: MoreThan(new Date()), }, order: { last_activity_at: 'DESC' }, }); } /** * Revocar una sesión específica */ async revokeSession(sessionId: string, userId: string): Promise { const result = await this.sessionRepository.update( { id: sessionId, user_id: userId }, { is_revoked: true }, ); if (result.affected === 0) { throw new NotFoundException('Sesión no encontrada'); } } /** * Revocar todas las sesiones de un usuario (excepto la actual) */ async revokeAllOtherSessions(userId: string, currentSessionId: string): Promise { const result = await this.sessionRepository .createQueryBuilder() .update() .set({ is_revoked: true }) .where('user_id = :userId', { userId }) .andWhere('id != :currentSessionId', { currentSessionId }) .andWhere('is_revoked = false') .execute(); return result.affected || 0; } /** * Actualizar última actividad de sesión */ async updateLastActivity(sessionId: string): Promise { await this.sessionRepository.update(sessionId, { last_activity_at: new Date(), }); } /** * Validar sesión por refresh token */ async validateSession(refreshTokenHash: string): Promise { return this.sessionRepository.findOne({ where: { refresh_token: refreshTokenHash, is_revoked: false, expires_at: MoreThan(new Date()), }, relations: ['user'], }); } /** * Limpiar sesiones expiradas (para CRON job) */ async cleanupExpiredSessions(): Promise { const result = await this.sessionRepository .createQueryBuilder() .delete() .where('expires_at < :now', { now: new Date() }) .orWhere('is_revoked = true') .execute(); return result.affected || 0; } // ============ HELPERS PRIVADOS ============ private generateTokenHash(): string { return crypto.randomBytes(32).toString('hex'); } private detectDeviceType(userAgent: string): string { if (/mobile/i.test(userAgent)) return 'mobile'; if (/tablet/i.test(userAgent)) return 'tablet'; return 'desktop'; } private calculateExpiry(value: number, unit: 'hours' | 'days'): Date { const ms = unit === 'hours' ? value * 3600000 : value * 86400000; return new Date(Date.now() + ms); } } // ============ TIPOS ============ interface SessionMetadata { ip?: string; userAgent?: string; } interface UserSession { id: string; user_id: string; refresh_token: string; ip_address?: string; user_agent?: string; device_type?: string; expires_at: Date; is_revoked: boolean; last_activity_at?: Date; user?: any; }