/** * AUTH SERVICE - REFERENCE IMPLEMENTATION * * @description Implementación de referencia para servicio de autenticación JWT. * Este archivo muestra los patrones básicos sin dependencias específicas de proyecto. * * @usage Copiar y adaptar según necesidades del proyecto. * @origin gamilit/apps/backend/src/modules/auth/services/auth.service.ts */ import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import * as crypto from 'crypto'; // Adaptar imports según proyecto // import { User, Profile, UserSession } from '../entities'; // import { RegisterUserDto } from '../dto'; /** * Constantes de configuración * Mover a variables de entorno en producción */ const BCRYPT_COST = 10; const ACCESS_TOKEN_EXPIRES = '15m'; const REFRESH_TOKEN_EXPIRES = '7d'; @Injectable() export class AuthService { constructor( @InjectRepository(User, 'auth') private readonly userRepository: Repository, @InjectRepository(Profile, 'auth') private readonly profileRepository: Repository, @InjectRepository(UserSession, 'auth') private readonly sessionRepository: Repository, private readonly jwtService: JwtService, ) {} /** * Registro de nuevo usuario * * @pattern * 1. Validar email único * 2. Hashear password (bcrypt) * 3. Crear usuario * 4. Crear perfil * 5. Generar tokens */ async register(dto: RegisterUserDto): Promise { // 1. Validar email único const existingUser = await this.userRepository.findOne({ where: { email: dto.email }, }); if (existingUser) { throw new ConflictException('Email ya registrado'); } // 2. Hashear password const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_COST); // 3. Crear usuario const user = this.userRepository.create({ email: dto.email, encrypted_password: hashedPassword, role: dto.role || 'user', }); await this.userRepository.save(user); // 4. Crear perfil const profile = this.profileRepository.create({ id: user.id, user_id: user.id, email: user.email, // ...otros campos del perfil }); await this.profileRepository.save(profile); // 5. Generar tokens y crear sesión return this.createAuthResponse(user); } /** * Login de usuario * * @pattern * 1. Buscar usuario por email * 2. Validar password * 3. Crear sesión * 4. Generar tokens */ async login(email: string, password: string): Promise { // 1. Buscar usuario const user = await this.userRepository.findOne({ where: { email }, select: ['id', 'email', 'encrypted_password', 'role'], }); if (!user) { throw new UnauthorizedException('Credenciales inválidas'); } // 2. Validar password const isValid = await bcrypt.compare(password, user.encrypted_password); if (!isValid) { throw new UnauthorizedException('Credenciales inválidas'); } // 3-4. Crear sesión y generar tokens return this.createAuthResponse(user); } /** * Refresh de tokens * * @pattern * 1. Buscar sesión por refresh token hash * 2. Validar que no esté expirada * 3. Generar nuevos tokens * 4. Actualizar sesión */ async refreshToken(refreshToken: string): Promise { const tokenHash = this.hashToken(refreshToken); const session = await this.sessionRepository.findOne({ where: { refresh_token: tokenHash }, relations: ['user'], }); if (!session || session.is_revoked || new Date() > session.expires_at) { throw new UnauthorizedException('Token inválido o expirado'); } // Generar nuevos tokens return this.createAuthResponse(session.user, session); } /** * Logout * * @pattern Revocar sesión actual */ async logout(sessionId: string): Promise { await this.sessionRepository.update(sessionId, { is_revoked: true }); } // ============ HELPERS PRIVADOS ============ /** * Generar respuesta de autenticación completa */ private async createAuthResponse(user: User, existingSession?: UserSession): Promise { const payload = { sub: user.id, email: user.email, role: user.role, }; const accessToken = this.jwtService.sign(payload, { expiresIn: ACCESS_TOKEN_EXPIRES }); const refreshToken = crypto.randomBytes(32).toString('hex'); // Crear o actualizar sesión if (existingSession) { existingSession.refresh_token = this.hashToken(refreshToken); existingSession.expires_at = this.calculateExpiry(REFRESH_TOKEN_EXPIRES); await this.sessionRepository.save(existingSession); } else { const session = this.sessionRepository.create({ user_id: user.id, refresh_token: this.hashToken(refreshToken), expires_at: this.calculateExpiry(REFRESH_TOKEN_EXPIRES), }); await this.sessionRepository.save(session); } return { accessToken, refreshToken, user: { id: user.id, email: user.email, role: user.role, }, }; } /** * Hash de token para almacenamiento seguro */ private hashToken(token: string): string { return crypto.createHash('sha256').update(token).digest('hex'); } /** * Calcular fecha de expiración */ private calculateExpiry(duration: string): Date { const match = duration.match(/^(\d+)([mhd])$/); if (!match) throw new Error('Invalid duration format'); const [, value, unit] = match; const multipliers = { m: 60000, h: 3600000, d: 86400000 }; return new Date(Date.now() + parseInt(value) * multipliers[unit]); } } // ============ TIPOS ============ interface AuthResponse { accessToken: string; refreshToken: string; user: { id: string; email: string; role: string; }; } interface RegisterUserDto { email: string; password: string; role?: string; }