import { Controller, Req, Get, Post, Body, Param, Logger, HttpException, HttpStatus, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiBody, ApiHeader, ApiProperty } from '@nestjs/swagger'; import { AuthenticatedUser, Public, Roles } from 'nest-keycloak-connect'; import { TokenService } from '../services/token.service'; import { ConfigService } from '@nestjs/config'; import type { Request } from 'express'; import { HubUsersService } from '../../hub-users/services/hub-users.service'; import * as user from '../../hub-users/models/hub-user.model'; // DTOs pour Swagger export class LoginDto { @ApiProperty({ description: 'Username', example: 'admin@dcb.com' }) username: string; @ApiProperty({ description: 'Password', example: 'your_password' }) password: string; } export class RefreshTokenDto { @ApiProperty({ description: 'Refresh token' }) refresh_token: string; } export class LoginResponseDto { @ApiProperty({ description: 'Access token JWT' }) access_token: string; @ApiProperty({ description: 'Refresh token' }) refresh_token: string; @ApiProperty({ description: 'Token expiration in seconds' }) expires_in: number; @ApiProperty({ description: 'Token type', example: 'Bearer' }) token_type: string; } export class LogoutResponseDto { @ApiProperty({ description: 'Success message' }) message: string; } export class AuthStatusResponseDto { @ApiProperty({ description: 'Whether user is authenticated' }) authenticated: boolean; @ApiProperty({ description: 'Authentication status message' }) status: string; } export class ErrorResponseDto { @ApiProperty({ description: 'Error message' }) message: string; @ApiProperty({ description: 'HTTP status code' }) statusCode: number; @ApiProperty({ description: 'Error type' }) error: string; } @ApiTags('Authentication') @Controller('auth') export class AuthController { private readonly logger = new Logger(AuthController.name); constructor( private readonly tokenService: TokenService, private readonly configService: ConfigService, private readonly usersService: HubUsersService ) {} /** ------------------------------- * LOGIN (Resource Owner Password Credentials) * ------------------------------- */ @Public() @Post('login') @ApiOperation({ summary: 'User login', description: 'Authenticate user with username and password using Resource Owner Password Credentials flow' }) @ApiBody({ type: LoginDto }) @ApiResponse({ status: 200, description: 'Login successful', type: LoginResponseDto }) @ApiResponse({ status: 400, description: 'Bad request - missing credentials', type: ErrorResponseDto }) @ApiResponse({ status: 401, description: 'Unauthorized - invalid credentials', type: ErrorResponseDto }) @ApiResponse({ status: 403, description: 'Forbidden - account disabled or not fully set up', type: ErrorResponseDto }) async login(@Body() loginDto: user.LoginDto) { this.logger.log(`User login attempt: ${loginDto.username}`); const { username, password } = loginDto; if (!username || !password) { throw new HttpException('Username and password are required', HttpStatus.BAD_REQUEST); } try { // Appel au UserService pour l'authentification const tokenResponse = await this.usersService.authenticateUser(loginDto); this.logger.log(`User "${username}" authenticated successfully`); return { access_token: tokenResponse.access_token, refresh_token: tokenResponse.refresh_token, expires_in: tokenResponse.expires_in, token_type: 'Bearer', }; } catch (err: any) { const msg = err.message || ''; if (msg.includes('Account is not fully set up')) { this.logger.warn(`User account not fully set up: "${username}"`); throw new HttpException('Account setup incomplete', HttpStatus.FORBIDDEN); } if (msg.includes('Invalid user credentials')) { this.logger.warn(`Invalid credentials for user: "${username}"`); throw new HttpException('Invalid username or password', HttpStatus.UNAUTHORIZED); } if (msg.includes('User is disabled')) { this.logger.warn(`Disabled user attempted login: "${username}"`); throw new HttpException('Account is disabled', HttpStatus.FORBIDDEN); } this.logger.warn(`Authentication failed for "${username}": ${msg}`); throw new HttpException('Authentication failed', HttpStatus.UNAUTHORIZED); } } /** ------------------------------- * LOGOUT * ------------------------------- */ @Post('logout') @ApiOperation({ summary: 'User logout', description: 'Logout user by revoking refresh token and clearing session' }) @ApiBearerAuth() @ApiHeader({ name: 'Authorization', description: 'Bearer token', required: true }) @ApiResponse({ status: 200, description: 'Logout successful', type: LogoutResponseDto }) @ApiResponse({ status: 400, description: 'Bad request - no token provided', type: ErrorResponseDto }) @ApiResponse({ status: 401, description: 'Unauthorized - invalid token', type: ErrorResponseDto }) @ApiResponse({ status: 500, description: 'Internal server error', type: ErrorResponseDto }) async logout(@Req() req: Request) { const token = req.headers['authorization']?.split(' ')[1]; if (!token) throw new HttpException('No token provided', HttpStatus.BAD_REQUEST); try { // Récupérer le refresh token depuis le UserService const refreshToken = this.tokenService.getUserRefreshToken(); if (refreshToken) { try { // Appel au TokenService pour révoquer le token await this.tokenService.revokeToken(refreshToken); this.logger.log('Refresh token revoked successfully'); } catch (revokeError) { this.logger.warn('Failed to revoke refresh token, continuing with local logout', revokeError); } } // Nettoyer les tokens stockés dans UserService await this.tokenService.clearUserToken(); this.logger.log(`User logged out successfully`); return { message: 'Logout successful' }; } catch (err: any) { this.logger.error('Logout failed', err); // En cas d'erreur, nettoyer quand même les tokens dans UserService await this.tokenService.clearUserToken(); if (err instanceof HttpException) { throw err; } throw new HttpException('Logout failed', HttpStatus.INTERNAL_SERVER_ERROR); } } /** ------------------------------- * REFRESH TOKEN * ------------------------------- */ @Public() @Post('refresh') @ApiOperation({ summary: 'Refresh access token', description: 'Obtain new access token using refresh token' }) @ApiBody({ type: RefreshTokenDto }) @ApiResponse({ status: 200, description: 'Token refreshed successfully', type: LoginResponseDto }) @ApiResponse({ status: 400, description: 'Bad request - refresh token missing', type: ErrorResponseDto }) @ApiResponse({ status: 401, description: 'Unauthorized - invalid refresh token', type: ErrorResponseDto }) async refreshToken(@Body() body: { refresh_token: string }) { const { refresh_token } = body; if (!refresh_token) throw new HttpException('Refresh token is required', HttpStatus.BAD_REQUEST); try { const tokenResponse = await this.tokenService.refreshToken(refresh_token); return { access_token: tokenResponse.access_token, refresh_token: tokenResponse.refresh_token, expires_in: tokenResponse.expires_in, token_type: 'Bearer', }; } catch (err: any) { this.logger.error('Token refresh failed', err); throw new HttpException('Invalid refresh token', HttpStatus.UNAUTHORIZED); } } /** ------------------------------- * AUTH STATUS CHECK (public) * ------------------------------- */ @Public() @Get('status') @ApiOperation({ summary: 'Check authentication status', description: 'Verify if the provided token is valid' }) @ApiHeader({ name: 'Authorization', description: 'Bearer token (optional)', required: false }) @ApiResponse({ status: 200, description: 'Authentication status retrieved', type: AuthStatusResponseDto }) async getAuthStatus(@Req() req: Request) { const token = req.headers['authorization']?.replace('Bearer ', ''); let isValid = false; if (token) { try { isValid = await this.tokenService.validateToken(token); } catch { this.logger.debug('Token validation failed in status check'); } } return { authenticated: isValid, status: isValid ? 'Token is valid' : 'Token is invalid or expired' }; } /** ------------------------------- * USER PROFILE (protected) * ------------------------------- */ @Get('profile') @ApiOperation({ summary: 'Get current user profile', description: 'Retrieve profile information of authenticated user' }) @ApiBearerAuth() @ApiResponse({ status: 200, description: 'User profile retrieved successfully' }) @ApiResponse({ status: 401, description: 'Unauthorized - invalid token', type: ErrorResponseDto }) async getProfile(@AuthenticatedUser() user: any) { return this.usersService.getCompleteUserProfile(user.sub, user); } /** ------------------------------- * VALIDATE TOKEN (protected) * ------------------------------- */ @Get('validate') @ApiOperation({ summary: 'Validate token', description: 'Check if the current token is valid and get user information' }) @ApiBearerAuth() @ApiResponse({ status: 200, description: 'Token is valid' }) @ApiResponse({ status: 401, description: 'Unauthorized - invalid token', type: ErrorResponseDto }) async validateToken(@AuthenticatedUser() user: any) { this.logger.log(`Token validation requested for user: ${user.preferred_username}`); return { valid: true, user: { id: user.sub, username: user.preferred_username, email: user.email, firstName: user.given_name, lastName: user.family_name, roles: user.resource_access?.[this.configService.get('KEYCLOAK_CLIENT_ID')]?.roles || [], }, expires_in: user.exp ? user.exp - Math.floor(Date.now() / 1000) : 0, }; } }