import { Controller, Post, Body, HttpCode, HttpStatus, UseGuards, Get, Inject, NotFoundException, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import { AuthService } from '../auth/auth.service'; import { LoginDto, RegisterDto, AuthResponseDto, RefreshTokenDto } from '../dto/auth-login.dto'; 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 { constructor( private readonly authService: AuthService, @Inject(USER_REPOSITORY) private readonly userRepository: UserRepository, private readonly invitationService: InvitationService ) {} /** * 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' }; } /** * 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); } }