import { Injectable, UnauthorizedException, BadRequestException, ConflictException, Logger, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { UsersService } from '../users/users.service'; import { LoginDto, RegisterDto, AuthResponseDto, RefreshTokenDto } from './dto'; import { JwtPayload } from './interfaces/jwt-payload.interface'; import { UserRole } from '../users/entities/user.entity'; import { RefreshTokenService } from './services/refresh-token.service'; @Injectable() export class AuthService { private readonly logger = new Logger(AuthService.name); private readonly BCRYPT_ROUNDS = 10; constructor( private readonly usersService: UsersService, private readonly jwtService: JwtService, private readonly refreshTokenService: RefreshTokenService, ) {} /** * Validate user credentials (used by LocalStrategy) */ async validateUser(email: string, password: string): Promise { const user = await this.usersService.findByEmail(email); if (!user) { throw new UnauthorizedException('Invalid credentials'); } if (!user.isActive) { throw new UnauthorizedException('User account is inactive'); } const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { throw new UnauthorizedException('Invalid credentials'); } // Return user without password const { password: _, ...result } = user; return result; } /** * Register new user */ async register(registerDto: RegisterDto): Promise { // Check if user already exists const existingUser = await this.usersService.findByEmail(registerDto.email); if (existingUser) { throw new ConflictException('Email already registered'); } // Hash password const hashedPassword = await this.hashPassword(registerDto.password); // Create user with default role if not provided const user = await this.usersService.create({ ...registerDto, password: hashedPassword, roles: registerDto.roles || [UserRole.USER], }); // Generate JWT and return return this.login(user); } /** * Login user and generate JWT + Refresh Token */ async login(user: any): Promise { const payload: JwtPayload = { sub: user.id, email: user.email, roles: user.roles || [], }; // Generate access token const accessToken = this.jwtService.sign(payload); // Generate refresh token const refreshToken = await this.refreshTokenService.generateRefreshToken( user.id, ); this.logger.log(`User logged in: ${user.email}`); return { access_token: accessToken, refresh_token: refreshToken, user: { id: user.id, email: user.email, name: user.name, roles: user.roles, isActive: user.isActive, createdAt: user.createdAt, }, }; } /** * Validate JWT token */ async validateToken(token: string): Promise { try { const payload = this.jwtService.verify(token); return payload; } catch (error) { throw new UnauthorizedException('Invalid or expired token'); } } /** * Refresh access token using refresh token * Implements token rotation for enhanced security */ async refreshAccessToken( refreshTokenDto: RefreshTokenDto, ): Promise { // Validate refresh token const userId = await this.refreshTokenService.validateRefreshToken( refreshTokenDto.refreshToken, ); // Get user details const user = await this.usersService.findOne(userId); if (!user) { throw new UnauthorizedException('User not found'); } // Revoke old refresh token (token rotation) await this.refreshTokenService.revokeRefreshToken( refreshTokenDto.refreshToken, ); // Generate new tokens this.logger.log(`Token refreshed for user: ${user.email}`); return this.login(user); } /** * Logout user and revoke refresh token */ async logout(refreshToken: string): Promise { if (!refreshToken) { throw new BadRequestException('Refresh token is required'); } await this.refreshTokenService.revokeRefreshToken(refreshToken); this.logger.log('User logged out and refresh token revoked'); } /** * Revoke all refresh tokens for a user */ async revokeAllUserTokens(userId: string): Promise { await this.refreshTokenService.revokeAllUserTokens(userId); this.logger.log(`All tokens revoked for user: ${userId}`); } /** * Hash password using bcrypt */ private async hashPassword(password: string): Promise { return bcrypt.hash(password, this.BCRYPT_ROUNDS); } /** * Verify password hash */ async verifyPassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash); } }