import bcrypt from 'bcryptjs'; import { Repository } from 'typeorm'; import { AppDataSource } from '../../config/typeorm.js'; import { User, UserStatus, Role } from './entities/index.js'; import { tokenService, TokenPair, RequestMetadata } from './services/token.service.js'; import { UnauthorizedError, ValidationError, NotFoundError } from '../../shared/types/index.js'; import { logger } from '../../shared/utils/logger.js'; export interface LoginDto { email: string; password: string; metadata?: RequestMetadata; // IP and user agent for session tracking } export interface RegisterDto { email: string; password: string; // Soporta ambos formatos para compatibilidad frontend/backend full_name?: string; firstName?: string; lastName?: string; tenant_id?: string; companyName?: string; } /** * Transforma full_name a firstName/lastName para respuesta al frontend */ export function splitFullName(fullName: string): { firstName: string; lastName: string } { const parts = fullName.trim().split(/\s+/); if (parts.length === 1) { return { firstName: parts[0], lastName: '' }; } const firstName = parts[0]; const lastName = parts.slice(1).join(' '); return { firstName, lastName }; } /** * Transforma firstName/lastName a full_name para almacenar en BD */ export function buildFullName(firstName?: string, lastName?: string, fullName?: string): string { if (fullName) return fullName.trim(); return `${firstName || ''} ${lastName || ''}`.trim(); } export interface LoginResponse { user: Omit & { firstName: string; lastName: string }; tokens: TokenPair; } class AuthService { private userRepository: Repository; constructor() { this.userRepository = AppDataSource.getRepository(User); } async login(dto: LoginDto): Promise { // Find user by email using TypeORM const user = await this.userRepository.findOne({ where: { email: dto.email.toLowerCase(), status: UserStatus.ACTIVE }, relations: ['roles'], }); if (!user) { throw new UnauthorizedError('Credenciales inválidas'); } // Verify password const isValidPassword = await bcrypt.compare(dto.password, user.passwordHash || ''); if (!isValidPassword) { throw new UnauthorizedError('Credenciales inválidas'); } // Update last login user.lastLoginAt = new Date(); user.loginCount += 1; if (dto.metadata?.ipAddress) { user.lastLoginIp = dto.metadata.ipAddress; } await this.userRepository.save(user); // Generate token pair using TokenService const metadata: RequestMetadata = dto.metadata || { ipAddress: 'unknown', userAgent: 'unknown', }; const tokens = await tokenService.generateTokenPair(user, metadata); // Transform fullName to firstName/lastName for frontend response const { firstName, lastName } = splitFullName(user.fullName); // Remove passwordHash from response and add firstName/lastName const { passwordHash, ...userWithoutPassword } = user; const userResponse = { ...userWithoutPassword, firstName, lastName, }; logger.info('User logged in', { userId: user.id, email: user.email }); return { user: userResponse as any, tokens, }; } async register(dto: RegisterDto): Promise { // Check if email already exists using TypeORM const existingUser = await this.userRepository.findOne({ where: { email: dto.email.toLowerCase() }, }); if (existingUser) { throw new ValidationError('El email ya está registrado'); } // Transform firstName/lastName to fullName for database storage const fullName = buildFullName(dto.firstName, dto.lastName, dto.full_name); // Hash password const passwordHash = await bcrypt.hash(dto.password, 10); // Generate tenantId if not provided (new company registration) const tenantId = dto.tenant_id || crypto.randomUUID(); // Create user using TypeORM const newUser = this.userRepository.create({ email: dto.email.toLowerCase(), passwordHash, fullName, tenantId, status: UserStatus.ACTIVE, }); await this.userRepository.save(newUser); // Load roles relation for token generation const userWithRoles = await this.userRepository.findOne({ where: { id: newUser.id }, relations: ['roles'], }); if (!userWithRoles) { throw new Error('Error al crear usuario'); } // Generate token pair using TokenService const metadata: RequestMetadata = { ipAddress: 'unknown', userAgent: 'unknown', }; const tokens = await tokenService.generateTokenPair(userWithRoles, metadata); // Transform fullName to firstName/lastName for frontend response const { firstName, lastName } = splitFullName(userWithRoles.fullName); // Remove passwordHash from response and add firstName/lastName const { passwordHash: _, ...userWithoutPassword } = userWithRoles; const userResponse = { ...userWithoutPassword, firstName, lastName, }; logger.info('User registered', { userId: userWithRoles.id, email: userWithRoles.email }); return { user: userResponse as any, tokens, }; } async refreshToken(refreshToken: string, metadata: RequestMetadata): Promise { // Delegate completely to TokenService return tokenService.refreshTokens(refreshToken, metadata); } async logout(sessionId: string): Promise { await tokenService.revokeSession(sessionId, 'user_logout'); } async logoutAll(userId: string): Promise { return tokenService.revokeAllUserSessions(userId, 'logout_all'); } async changePassword(userId: string, currentPassword: string, newPassword: string): Promise { // Find user using TypeORM const user = await this.userRepository.findOne({ where: { id: userId }, }); if (!user) { throw new NotFoundError('Usuario no encontrado'); } // Verify current password const isValidPassword = await bcrypt.compare(currentPassword, user.passwordHash || ''); if (!isValidPassword) { throw new UnauthorizedError('Contraseña actual incorrecta'); } // Hash new password and update user const newPasswordHash = await bcrypt.hash(newPassword, 10); user.passwordHash = newPasswordHash; user.updatedAt = new Date(); await this.userRepository.save(user); // Revoke all sessions after password change for security const revokedCount = await tokenService.revokeAllUserSessions(userId, 'password_changed'); logger.info('Password changed and all sessions revoked', { userId, revokedCount }); } async getProfile(userId: string): Promise> { // Find user using TypeORM with relations const user = await this.userRepository.findOne({ where: { id: userId }, relations: ['roles', 'companies'], }); if (!user) { throw new NotFoundError('Usuario no encontrado'); } // Remove passwordHash from response const { passwordHash, ...userWithoutPassword } = user; return userWithoutPassword; } } export const authService = new AuthService();