import { Injectable, UnauthorizedException, ConflictException, Logger, Inject, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import * as argon2 from 'argon2'; import { UserRepository, USER_REPOSITORY } from '../../domain/ports/out/user.repository'; import { User, UserRole } from '../../domain/entities/user.entity'; import { v4 as uuidv4 } from 'uuid'; export interface JwtPayload { sub: string; // user ID email: string; role: string; organizationId: string; type: 'access' | 'refresh'; } @Injectable() export class AuthService { private readonly logger = new Logger(AuthService.name); constructor( @Inject(USER_REPOSITORY) private readonly userRepository: UserRepository, // ✅ Correct injection private readonly jwtService: JwtService, private readonly configService: ConfigService ) {} /** * Register a new user */ async register( email: string, password: string, firstName: string, lastName: string, organizationId?: string ): Promise<{ accessToken: string; refreshToken: string; user: any }> { this.logger.log(`Registering new user: ${email}`); const existingUser = await this.userRepository.findByEmail(email); if (existingUser) { throw new ConflictException('User with this email already exists'); } const passwordHash = await argon2.hash(password, { type: argon2.argon2id, memoryCost: 65536, // 64 MB timeCost: 3, parallelism: 4, }); // Validate or generate organization ID const finalOrganizationId = this.validateOrGenerateOrganizationId(organizationId); const user = User.create({ id: uuidv4(), organizationId: finalOrganizationId, email, passwordHash, firstName, lastName, role: UserRole.USER, }); const savedUser = await this.userRepository.save(user); const tokens = await this.generateTokens(savedUser); this.logger.log(`User registered successfully: ${email}`); return { ...tokens, user: { id: savedUser.id, email: savedUser.email, firstName: savedUser.firstName, lastName: savedUser.lastName, role: savedUser.role, organizationId: savedUser.organizationId, }, }; } /** * Login user with email and password */ async login( email: string, password: string ): Promise<{ accessToken: string; refreshToken: string; user: any }> { this.logger.log(`Login attempt for: ${email}`); const user = await this.userRepository.findByEmail(email); if (!user) { throw new UnauthorizedException('Invalid credentials'); } if (!user.isActive) { throw new UnauthorizedException('User account is inactive'); } const isPasswordValid = await argon2.verify(user.passwordHash, password); if (!isPasswordValid) { throw new UnauthorizedException('Invalid credentials'); } const tokens = await this.generateTokens(user); this.logger.log(`User logged in successfully: ${email}`); return { ...tokens, user: { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, role: user.role, organizationId: user.organizationId, }, }; } /** * Refresh access token using refresh token */ async refreshAccessToken( refreshToken: string ): Promise<{ accessToken: string; refreshToken: string }> { try { const payload = await this.jwtService.verifyAsync(refreshToken, { secret: this.configService.get('JWT_SECRET'), }); if (payload.type !== 'refresh') { throw new UnauthorizedException('Invalid token type'); } const user = await this.userRepository.findById(payload.sub); if (!user || !user.isActive) { throw new UnauthorizedException('User not found or inactive'); } const tokens = await this.generateTokens(user); this.logger.log(`Access token refreshed for user: ${user.email}`); return tokens; } catch (error: any) { this.logger.error(`Token refresh failed: ${error?.message || 'Unknown error'}`); throw new UnauthorizedException('Invalid or expired refresh token'); } } /** * Validate user from JWT payload */ async validateUser(payload: JwtPayload): Promise { const user = await this.userRepository.findById(payload.sub); if (!user || !user.isActive) { return null; } return user; } /** * Generate access and refresh tokens */ private async generateTokens(user: User): Promise<{ accessToken: string; refreshToken: string }> { const accessPayload: JwtPayload = { sub: user.id, email: user.email, role: user.role, organizationId: user.organizationId, type: 'access', }; const refreshPayload: JwtPayload = { sub: user.id, email: user.email, role: user.role, organizationId: user.organizationId, type: 'refresh', }; const [accessToken, refreshToken] = await Promise.all([ this.jwtService.signAsync(accessPayload, { expiresIn: this.configService.get('JWT_ACCESS_EXPIRATION', '15m'), }), this.jwtService.signAsync(refreshPayload, { expiresIn: this.configService.get('JWT_REFRESH_EXPIRATION', '7d'), }), ]); return { accessToken, refreshToken }; } /** * Validate or generate a valid organization ID * If provided ID is invalid (not a UUID), generate a new one */ private validateOrGenerateOrganizationId(organizationId?: string): string { // UUID v4 regex pattern const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (organizationId && uuidRegex.test(organizationId)) { return organizationId; } // Generate new UUID if not provided or invalid const newOrgId = uuidv4(); this.logger.warn(`Invalid or missing organization ID. Generated new ID: ${newOrgId}`); return newOrgId; } }