// backend/src/modules/auth/auth.controller.ts import { Controller, Post, Body, UseGuards, Req, Res, HttpCode, HttpStatus, Get, Patch, UnauthorizedException, } from '@nestjs/common'; import type { Response } from 'express'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiBody, ApiUnauthorizedResponse, ApiBadRequestResponse } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { LoginDto } from './dto/login.dto'; import { RegisterDto } from './dto/register.dto'; import { CreateOrganizationDto } from './dto/create-organization.dto'; import { JwtAuthGuard } from './guards/jwt-auth.guard'; import { LocalAuthGuard } from './guards/local-auth.guard'; import { UpdateLanguageDto } from './dto/update-language.dto'; import { ForgotPasswordSendCodeDto, ForgotPasswordVerifyDto, } from './dto/forgot-password.dto'; import { ChangePasswordDto } from './dto/change-password.dto'; @ApiTags('auth') @Controller('auth') export class AuthController { private static readonly REMEMBER_ME_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; private static readonly PASSWORD_RESET_MAX_AGE_MS = 15 * 60 * 1000; constructor(private readonly authService: AuthService) {} // ========================= // LOGIN // ========================= @Post('login') @HttpCode(HttpStatus.OK) @UseGuards(LocalAuthGuard) @ApiOperation({ summary: 'Login with email and password' }) @ApiBody({ type: LoginDto }) @ApiResponse({ status: 200, description: 'Login successful' }) @ApiUnauthorizedResponse({ description: 'Invalid credentials' }) @ApiBadRequestResponse({ description: 'Invalid input data' }) async login( @Body() loginDto: LoginDto, @Req() req, @Res({ passthrough: true }) res: Response ) { console.log('Login endpoint hit'); const result = await this.authService.login(loginDto, req.user); const rememberMe = Boolean(loginDto.rememberMe); this.setAuthCookies( res, result.data.accessToken, result.data.refreshToken, rememberMe, ); return { success: true, data: { user: result.data.user, organizations: result.data.organizations, }, }; } // ========================= // REGISTER // ========================= @Post('register') @ApiOperation({ summary: 'Register a new user' }) @ApiBody({ type: RegisterDto }) @ApiResponse({ status: 201, description: 'User registered successfully' }) @ApiBadRequestResponse({ description: 'Invalid input data' }) async register( @Body() registerDto: RegisterDto, @Res({ passthrough: true }) res: Response ) { console.log('Register endpoint hit'); const result = await this.authService.register(registerDto); // ✅ SET COOKIES HERE this.setAuthCookies(res, result.data.accessToken, result.data.refreshToken); return { success: true, data: { user: result.data.user, organizations: result.data.organizations, }, }; } // ========================= // SELECT ORGANIZATION // ========================= @Post('select-organization') @UseGuards(JwtAuthGuard) @ApiBearerAuth('JWT-auth') async selectOrganization( @Req() req, @Body('organizationId') organizationId: string, @Res({ passthrough: true }) res: Response ) { const result = await this.authService.selectOrganization( req.user.id, organizationId ); this.setAccessToken( res, result.data.accessToken, this.isPersistentSession(req), ); return { success: true, data: { organization: result.data.organization, }, }; } @Post('organizations') @UseGuards(JwtAuthGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Create organization for current user' }) async createOrganization(@Req() req, @Body() dto: CreateOrganizationDto) { return this.authService.createOrganization( req.user.id, req.user.organizationId, dto, ); } // ========================= // PROFILE // ========================= @Get('profile') @UseGuards(JwtAuthGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Get user profile' }) @ApiResponse({ status: 200, description: 'Profile retrieved successfully' }) @ApiUnauthorizedResponse({ description: 'Invalid or missing JWT token' }) async getProfile(@Req() req) { console.log('Profile endpoint hit'); console.log('USER FROM JWT:', req.user); return this.authService.getProfile(req.user.id); } @Patch('profile/language') @UseGuards(JwtAuthGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Update user language preference' }) async updateLanguage(@Req() req, @Body() dto: UpdateLanguageDto) { return this.authService.updateLanguage(req.user.id, dto); } @Patch('profile/password') @UseGuards(JwtAuthGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Change account password' }) async changePassword( @Req() req, @Body() dto: ChangePasswordDto, @Res({ passthrough: true }) res: Response, ) { const skipCurrentPassword = req?.cookies?.passwordResetVerified === '1'; const result = await this.authService.changePassword( req.user.id, dto.currentPassword, dto.newPassword, skipCurrentPassword, ); this.clearAuthCookies(res); res.clearCookie('passwordResetVerified', this.baseCookieOptions()); return result; } @Post('forgot-password/send-code') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Send forgot-password SMS verification code' }) async sendForgotPasswordCode(@Body() dto: ForgotPasswordSendCodeDto) { return this.authService.sendForgotPasswordCode(dto); } @Post('forgot-password/verify') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Verify SMS code and sign in for password reset' }) async verifyForgotPasswordCode( @Body() dto: ForgotPasswordVerifyDto, @Res({ passthrough: true }) res: Response, ) { const result = await this.authService.verifyForgotPasswordCode(dto); this.setAuthCookies( res, result.data.accessToken, result.data.refreshToken, ); res.cookie('passwordResetVerified', '1', { ...this.baseCookieOptions(), maxAge: AuthController.PASSWORD_RESET_MAX_AGE_MS, }); return { success: true, data: { user: result.data.user, organizations: result.data.organizations, redirectTo: '/settings/account', }, }; } @Get('subscription-alert') @UseGuards(JwtAuthGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Owner-only: seat / trial status for warning indicator (current org from JWT)', }) async getSubscriptionAlert(@Req() req) { return this.authService.getOwnerSubscriptionAlert( req.user.id, req.user.organizationId, ); } // ========================= // REFRESH // ========================= @Post('refresh') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Refresh access token using refresh cookie' }) @ApiResponse({ status: 200, description: 'Access token refreshed' }) @ApiUnauthorizedResponse({ description: 'Invalid or missing refresh token' }) async refresh(@Req() req, @Res({ passthrough: true }) res: Response) { const refreshToken = req?.cookies?.refreshToken; if (!refreshToken) { throw new UnauthorizedException('Refresh token not found'); } const result = await this.authService.refreshToken(refreshToken); this.setAccessToken( res, result.data.accessToken, this.isPersistentSession(req), ); return { success: true, data: { accessToken: result.data.accessToken, }, }; } // ========================= // LOGOUT // ========================= @Post('logout') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Logout current user' }) @ApiResponse({ status: 200, description: 'Logout successful' }) async logout(@Req() req, @Res({ passthrough: true }) res: Response) { const accessToken = req?.cookies?.accessToken; if (accessToken) { await this.authService.logout(accessToken); } this.clearAuthCookies(res); return { success: true, message: 'Logged out successfully', }; } // ========================= // TEST // ========================= @Get('test') @ApiOperation({ summary: 'Test endpoint' }) test() { return { message: 'Auth controller is working!' }; } // ========================= // 🔥 COOKIE HELPERS // ========================= private isPersistentSession(req: { cookies?: Record }): boolean { return req?.cookies?.authRemember === '1'; } private baseCookieOptions() { return { httpOnly: true, secure: false, // ⚠️ true in production (HTTPS) sameSite: 'lax' as const, path: '/', }; } private setAuthCookies( res: Response, accessToken: string, refreshToken: string, rememberMe = false, ) { this.setAccessToken(res, accessToken, rememberMe); this.setRefreshToken(res, refreshToken, rememberMe); this.setRememberMeFlag(res, rememberMe); } private setAccessToken(res: Response, token: string, rememberMe = false) { res.cookie('accessToken', token, { ...this.baseCookieOptions(), ...(rememberMe ? { maxAge: AuthController.REMEMBER_ME_MAX_AGE_MS } : {}), }); } private setRefreshToken(res: Response, token: string, rememberMe = false) { res.cookie('refreshToken', token, { ...this.baseCookieOptions(), ...(rememberMe ? { maxAge: AuthController.REMEMBER_ME_MAX_AGE_MS } : {}), }); } private setRememberMeFlag(res: Response, rememberMe: boolean) { if (rememberMe) { res.cookie('authRemember', '1', { ...this.baseCookieOptions(), maxAge: AuthController.REMEMBER_ME_MAX_AGE_MS, }); return; } res.clearCookie('authRemember', this.baseCookieOptions()); } private clearAuthCookies(res: Response) { const options = this.baseCookieOptions(); res.clearCookie('accessToken', options); res.clearCookie('refreshToken', options); res.clearCookie('authRemember', options); res.clearCookie('passwordResetVerified', options); } }