/** * AuthService - Servicio de Autenticación * * Gestiona login, logout, refresh tokens y validación de JWT. * Implementa patrón multi-tenant con verificación de tenant_id. * * @module Auth */ import * as jwt from 'jsonwebtoken'; import * as bcrypt from 'bcryptjs'; import { Repository } from 'typeorm'; import { User } from '../../core/entities/user.entity'; import { Tenant } from '../../core/entities/tenant.entity'; import { LoginDto, RegisterDto, RefreshTokenDto, ChangePasswordDto, TokenPayload, AuthResponse, TokenValidationResult, } from '../dto/auth.dto'; export interface RefreshToken { id: string; userId: string; token: string; expiresAt: Date; revokedAt?: Date; } export class AuthService { private readonly jwtSecret: string; private readonly jwtExpiresIn: string; private readonly jwtRefreshExpiresIn: string; constructor( private readonly userRepository: Repository, private readonly tenantRepository: Repository, private readonly refreshTokenRepository: Repository ) { this.jwtSecret = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production-minimum-32-chars'; this.jwtExpiresIn = process.env.JWT_EXPIRES_IN || '1d'; this.jwtRefreshExpiresIn = process.env.JWT_REFRESH_EXPIRES_IN || '7d'; } /** * Login de usuario */ async login(dto: LoginDto): Promise { // Buscar usuario por email const user = await this.userRepository.findOne({ where: { email: dto.email, deletedAt: null } as any, relations: ['userRoles', 'userRoles.role'], }); if (!user) { throw new Error('Invalid credentials'); } // Verificar password const isPasswordValid = await bcrypt.compare(dto.password, user.passwordHash); if (!isPasswordValid) { throw new Error('Invalid credentials'); } // Verificar que el usuario esté activo if (!user.isActive) { throw new Error('User is not active'); } // Obtener tenant const tenantId = dto.tenantId || user.defaultTenantId; if (!tenantId) { throw new Error('No tenant specified'); } const tenant = await this.tenantRepository.findOne({ where: { id: tenantId, isActive: true, deletedAt: null } as any, }); if (!tenant) { throw new Error('Tenant not found or inactive'); } // Obtener roles del usuario const roles = user.userRoles?.map((ur) => ur.role.code) || []; // Generar tokens const accessToken = this.generateAccessToken(user, tenantId, roles); const refreshToken = await this.generateRefreshToken(user.id); // Actualizar último login await this.userRepository.update(user.id, { lastLoginAt: new Date() }); return { accessToken, refreshToken, expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn), user: { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, roles, }, tenant: { id: tenant.id, name: tenant.name, }, }; } /** * Registro de usuario */ async register(dto: RegisterDto): Promise { // Verificar si el email ya existe const existingUser = await this.userRepository.findOne({ where: { email: dto.email } as any, }); if (existingUser) { throw new Error('Email already registered'); } // Verificar que el tenant existe const tenant = await this.tenantRepository.findOne({ where: { id: dto.tenantId, isActive: true } as any, }); if (!tenant) { throw new Error('Tenant not found'); } // Hash del password const passwordHash = await bcrypt.hash(dto.password, 12); // Crear usuario const user = await this.userRepository.save( this.userRepository.create({ email: dto.email, passwordHash, firstName: dto.firstName, lastName: dto.lastName, defaultTenantId: dto.tenantId, isActive: true, }) ); // Generar tokens (rol default: user) const roles = ['user']; const accessToken = this.generateAccessToken(user, dto.tenantId, roles); const refreshToken = await this.generateRefreshToken(user.id); return { accessToken, refreshToken, expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn), user: { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, roles, }, tenant: { id: tenant.id, name: tenant.name, }, }; } /** * Refresh de token */ async refresh(dto: RefreshTokenDto): Promise { // Validar refresh token const validation = this.validateToken(dto.refreshToken, 'refresh'); if (!validation.valid || !validation.payload) { throw new Error('Invalid refresh token'); } // Verificar que el token no está revocado const storedToken = await this.refreshTokenRepository.findOne({ where: { token: dto.refreshToken, revokedAt: null } as any, }); if (!storedToken || storedToken.expiresAt < new Date()) { throw new Error('Refresh token expired or revoked'); } // Obtener usuario const user = await this.userRepository.findOne({ where: { id: validation.payload.sub, deletedAt: null } as any, relations: ['userRoles', 'userRoles.role'], }); if (!user || !user.isActive) { throw new Error('User not found or inactive'); } // Obtener tenant const tenant = await this.tenantRepository.findOne({ where: { id: validation.payload.tenantId, isActive: true } as any, }); if (!tenant) { throw new Error('Tenant not found or inactive'); } const roles = user.userRoles?.map((ur) => ur.role.code) || []; // Revocar token anterior await this.refreshTokenRepository.update(storedToken.id, { revokedAt: new Date() }); // Generar nuevos tokens const accessToken = this.generateAccessToken(user, tenant.id, roles); const refreshToken = await this.generateRefreshToken(user.id); return { accessToken, refreshToken, expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn), user: { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, roles, }, tenant: { id: tenant.id, name: tenant.name, }, }; } /** * Logout - Revocar refresh token */ async logout(refreshToken: string): Promise { await this.refreshTokenRepository.update( { token: refreshToken } as any, { revokedAt: new Date() } ); } /** * Cambiar password */ async changePassword(userId: string, dto: ChangePasswordDto): Promise { const user = await this.userRepository.findOne({ where: { id: userId } as any, }); if (!user) { throw new Error('User not found'); } const isCurrentValid = await bcrypt.compare(dto.currentPassword, user.passwordHash); if (!isCurrentValid) { throw new Error('Current password is incorrect'); } const newPasswordHash = await bcrypt.hash(dto.newPassword, 12); await this.userRepository.update(userId, { passwordHash: newPasswordHash }); // Revocar todos los refresh tokens del usuario await this.refreshTokenRepository.update( { userId } as any, { revokedAt: new Date() } ); } /** * Validar access token */ validateAccessToken(token: string): TokenValidationResult { return this.validateToken(token, 'access'); } /** * Validar token */ private validateToken(token: string, expectedType: 'access' | 'refresh'): TokenValidationResult { try { const payload = jwt.verify(token, this.jwtSecret) as TokenPayload; if (payload.type !== expectedType) { return { valid: false, error: 'Invalid token type' }; } return { valid: true, payload }; } catch (error) { if (error instanceof jwt.TokenExpiredError) { return { valid: false, error: 'Token expired' }; } if (error instanceof jwt.JsonWebTokenError) { return { valid: false, error: 'Invalid token' }; } return { valid: false, error: 'Token validation failed' }; } } /** * Generar access token */ private generateAccessToken(user: User, tenantId: string, roles: string[]): string { const payload: TokenPayload = { sub: user.id, userId: user.id, email: user.email, tenantId, roles, type: 'access', }; return jwt.sign(payload, this.jwtSecret, { expiresIn: this.jwtExpiresIn as jwt.SignOptions['expiresIn'], }); } /** * Generar refresh token */ private async generateRefreshToken(userId: string): Promise { const payload: Partial = { sub: userId, type: 'refresh', }; const token = jwt.sign(payload, this.jwtSecret, { expiresIn: this.jwtRefreshExpiresIn as jwt.SignOptions['expiresIn'], }); // Almacenar en DB const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 7); // 7 días await this.refreshTokenRepository.save( this.refreshTokenRepository.create({ userId, token, expiresAt, }) ); return token; } /** * Convertir expiresIn a segundos */ private getExpiresInSeconds(expiresIn: string): number { const match = expiresIn.match(/^(\d+)([dhms])$/); if (!match) return 86400; // default 1 día const value = parseInt(match[1]); const unit = match[2]; switch (unit) { case 'd': return value * 86400; case 'h': return value * 3600; case 'm': return value * 60; case 's': return value; default: return 86400; } } }