365 lines
10 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
} |