import { Controller, Post, Body, HttpCode, HttpStatus, UseGuards, Get, Inject, NotFoundException, InternalServerErrorException, Logger, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import { AuthService } from '../auth/auth.service'; import { LoginDto, RegisterDto, AuthResponseDto, RefreshTokenDto, ForgotPasswordDto, ResetPasswordDto, ContactFormDto, } from '../dto/auth-login.dto'; import { EmailPort, EMAIL_PORT } from '@domain/ports/out/email.port'; import { Public } from '../decorators/public.decorator'; import { CurrentUser, UserPayload } from '../decorators/current-user.decorator'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository'; import { UserMapper } from '../mappers/user.mapper'; import { InvitationService } from '../services/invitation.service'; /** * Authentication Controller * * Handles user authentication endpoints: * - POST /auth/register - User registration * - POST /auth/login - User login * - POST /auth/refresh - Token refresh * - POST /auth/logout - User logout (placeholder) * - GET /auth/me - Get current user profile */ @ApiTags('Authentication') @Controller('auth') export class AuthController { private readonly logger = new Logger(AuthController.name); constructor( private readonly authService: AuthService, @Inject(USER_REPOSITORY) private readonly userRepository: UserRepository, private readonly invitationService: InvitationService, @Inject(EMAIL_PORT) private readonly emailService: EmailPort ) {} /** * Register a new user * * Creates a new user account and returns access + refresh tokens. * * @param dto - Registration data (email, password, firstName, lastName, organizationId) * @returns Access token, refresh token, and user info */ @Public() @Post('register') @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: 'Register new user', description: 'Create a new user account with email and password. Returns JWT tokens.', }) @ApiResponse({ status: 201, description: 'User successfully registered', type: AuthResponseDto, }) @ApiResponse({ status: 409, description: 'User with this email already exists', }) @ApiResponse({ status: 400, description: 'Validation error (invalid email, weak password, etc.)', }) async register(@Body() dto: RegisterDto): Promise { // If invitation token is provided, verify and use it let invitationOrganizationId: string | undefined; let invitationRole: string | undefined; if (dto.invitationToken) { const invitation = await this.invitationService.verifyInvitation(dto.invitationToken); // Verify email matches invitation if (invitation.email.toLowerCase() !== dto.email.toLowerCase()) { throw new NotFoundException('Invitation email does not match registration email'); } invitationOrganizationId = invitation.organizationId; invitationRole = invitation.role; // Override firstName/lastName from invitation if not provided dto.firstName = dto.firstName || invitation.firstName; dto.lastName = dto.lastName || invitation.lastName; } const result = await this.authService.register( dto.email, dto.password, dto.firstName, dto.lastName, invitationOrganizationId || dto.organizationId, dto.organization, invitationRole ); // Mark invitation as used if provided if (dto.invitationToken) { await this.invitationService.markInvitationAsUsed(dto.invitationToken); } return { accessToken: result.accessToken, refreshToken: result.refreshToken, user: result.user, }; } /** * Login with email and password * * Authenticates a user and returns access + refresh tokens. * * @param dto - Login credentials (email, password) * @returns Access token, refresh token, and user info */ @Public() @Post('login') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'User login', description: 'Authenticate with email and password. Returns JWT tokens.', }) @ApiResponse({ status: 200, description: 'Login successful', type: AuthResponseDto, }) @ApiResponse({ status: 401, description: 'Invalid credentials or inactive account', }) async login(@Body() dto: LoginDto): Promise { const result = await this.authService.login(dto.email, dto.password); return { accessToken: result.accessToken, refreshToken: result.refreshToken, user: result.user, }; } /** * Refresh access token * * Obtains a new access token using a valid refresh token. * * @param dto - Refresh token * @returns New access token */ @Public() @Post('refresh') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Refresh access token', description: 'Get a new access token using a valid refresh token. Refresh tokens are long-lived (7 days).', }) @ApiResponse({ status: 200, description: 'Token refreshed successfully', schema: { properties: { accessToken: { type: 'string', example: 'eyJhbGciOiJIUzI1NiIs...' }, }, }, }) @ApiResponse({ status: 401, description: 'Invalid or expired refresh token', }) async refresh(@Body() dto: RefreshTokenDto): Promise<{ accessToken: string }> { const result = await this.authService.refreshAccessToken(dto.refreshToken); return { accessToken: result.accessToken }; } /** * Logout (placeholder) * * Currently a no-op endpoint. With JWT, logout is typically handled client-side * by removing tokens. For more security, implement token blacklisting with Redis. * * @returns Success message */ @UseGuards(JwtAuthGuard) @Post('logout') @HttpCode(HttpStatus.OK) @ApiBearerAuth() @ApiOperation({ summary: 'Logout', description: 'Logout the current user. Currently handled client-side by removing tokens.', }) @ApiResponse({ status: 200, description: 'Logout successful', schema: { properties: { message: { type: 'string', example: 'Logout successful' }, }, }, }) async logout(): Promise<{ message: string }> { // TODO: Implement token blacklisting with Redis for more security // For now, logout is handled client-side by removing tokens return { message: 'Logout successful' }; } /** * Contact form — forwards message to contact@xpeditis.com */ @Public() @Post('contact') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Contact form', description: 'Send a contact message to the Xpeditis team.', }) @ApiResponse({ status: 200, description: 'Message sent successfully' }) async contact(@Body() dto: ContactFormDto): Promise<{ message: string }> { const subjectLabels: Record = { demo: 'Demande de démonstration', pricing: 'Questions sur les tarifs', partnership: 'Partenariat', support: 'Support technique', press: 'Relations presse', careers: 'Recrutement', other: 'Autre', }; const subjectLabel = subjectLabels[dto.subject] || dto.subject; const html = `

Nouveau message de contact

${dto.company ? `` : ''} ${dto.phone ? `` : ''}
Nom ${dto.firstName} ${dto.lastName}
Email ${dto.email}
Entreprise${dto.company}
Téléphone${dto.phone}
Sujet ${subjectLabel}

Message :

${dto.message}

Xpeditis — Formulaire de contact

`; try { await this.emailService.send({ to: 'contact@xpeditis.com', replyTo: dto.email, subject: `[Contact] ${subjectLabel} — ${dto.firstName} ${dto.lastName}`, html, }); } catch (error) { this.logger.error(`Failed to send contact email: ${error}`); throw new InternalServerErrorException( "Erreur lors de l'envoi du message. Veuillez réessayer." ); } return { message: 'Message envoyé avec succès.' }; } /** * Forgot password — sends reset email */ @Public() @Post('forgot-password') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Forgot password', description: 'Send a password reset email. Always returns 200 to avoid user enumeration.', }) @ApiResponse({ status: 200, description: 'Reset email sent (if account exists)' }) async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<{ message: string }> { await this.authService.forgotPassword(dto.email); return { message: 'Si un compte existe avec cet email, vous recevrez un lien de réinitialisation.', }; } /** * Reset password using token from email */ @Public() @Post('reset-password') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Reset password', description: 'Reset user password using the token received by email.', }) @ApiResponse({ status: 200, description: 'Password reset successfully' }) @ApiResponse({ status: 400, description: 'Invalid or expired token' }) async resetPassword(@Body() dto: ResetPasswordDto): Promise<{ message: string }> { await this.authService.resetPassword(dto.token, dto.newPassword); return { message: 'Mot de passe réinitialisé avec succès.' }; } /** * Get current user profile * * Returns the profile of the currently authenticated user with complete details. * * @param user - Current user from JWT token * @returns User profile with firstName, lastName, etc. */ @UseGuards(JwtAuthGuard) @Get('me') @ApiBearerAuth() @ApiOperation({ summary: 'Get current user profile', description: 'Returns the complete profile of the authenticated user.', }) @ApiResponse({ status: 200, description: 'User profile retrieved successfully', schema: { properties: { id: { type: 'string', format: 'uuid' }, email: { type: 'string', format: 'email' }, firstName: { type: 'string' }, lastName: { type: 'string' }, role: { type: 'string', enum: ['ADMIN', 'MANAGER', 'USER', 'VIEWER'] }, organizationId: { type: 'string', format: 'uuid' }, isActive: { type: 'boolean' }, createdAt: { type: 'string', format: 'date-time' }, updatedAt: { type: 'string', format: 'date-time' }, }, }, }) @ApiResponse({ status: 401, description: 'Unauthorized - invalid or missing token', }) async getProfile(@CurrentUser() user: UserPayload) { // Fetch complete user details from database const fullUser = await this.userRepository.findById(user.id); if (!fullUser) { throw new NotFoundException('User not found'); } // Return complete user data with firstName and lastName return UserMapper.toDto(fullUser); } }