dcb-user-service/src/auth/controllers/auth.controller.ts

365 lines
10 KiB
TypeScript

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,
};
}
}