import { Controller, Get, Post, UseGuards, Req, Res, HttpStatus, HttpException, Logger, } from '@nestjs/common'; import { Request, Response } from 'express'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiExcludeEndpoint, } from '@nestjs/swagger'; import { User } from '@prisma/client'; import { AuthService } from './auth.service'; import { GoogleAuthGuard, JwtAuthGuard, Public } from './auth.guard'; import { LoginResponseDto, LogoutResponseDto, AuthProfileDto } from './dto/auth.dto'; export interface AuthenticatedRequest extends Request { user: User; } @ApiTags('Authentication') @Controller('auth') export class AuthController { private readonly logger = new Logger(AuthController.name); constructor(private readonly authService: AuthService) {} @Get('google') @Public() @UseGuards(GoogleAuthGuard) @ApiOperation({ summary: 'Initiate Google OAuth authentication', description: 'Redirects user to Google OAuth consent screen' }) @ApiResponse({ status: 302, description: 'Redirect to Google OAuth' }) @ApiExcludeEndpoint() // Don't show in Swagger UI as it's a redirect async googleAuth() { // Guard handles the redirect to Google // This method exists for the decorator } @Get('google/callback') @Public() @UseGuards(GoogleAuthGuard) @ApiOperation({ summary: 'Google OAuth callback', description: 'Handles the callback from Google OAuth and creates/logs in user' }) @ApiResponse({ status: 200, description: 'Authentication successful', type: LoginResponseDto }) @ApiResponse({ status: 401, description: 'Authentication failed' }) @ApiExcludeEndpoint() // Don't show in Swagger UI as it's a callback async googleCallback( @Req() req: AuthenticatedRequest, @Res() res: Response, ) { try { if (!req.user) { throw new HttpException('Authentication failed', HttpStatus.UNAUTHORIZED); } // Generate JWT tokens for the authenticated user const tokenData = await this.authService.generateTokens(req.user); // Get frontend URL from config const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000'; // Set secure HTTP-only cookie with the JWT token res.cookie('access_token', tokenData.accessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: tokenData.expiresIn * 1000, // Convert to milliseconds path: '/', }); // Redirect to frontend with success indication const redirectUrl = `${frontendUrl}/auth/success?user=${encodeURIComponent( JSON.stringify({ id: tokenData.user.id, email: tokenData.user.email, plan: tokenData.user.plan, quotaRemaining: tokenData.user.quotaRemaining, }) )}`; this.logger.log(`User ${req.user.email} authenticated successfully`); return res.redirect(redirectUrl); } catch (error) { this.logger.error('OAuth callback error:', error); const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000'; return res.redirect(`${frontendUrl}/auth/error?message=${encodeURIComponent('Authentication failed')}`); } } @Post('logout') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Logout user', description: 'Invalidates the user session and clears authentication cookies' }) @ApiResponse({ status: 200, description: 'Successfully logged out', type: LogoutResponseDto }) @ApiResponse({ status: 401, description: 'Unauthorized' }) async logout( @Req() req: AuthenticatedRequest, @Res() res: Response, ): Promise { try { const result = await this.authService.logout(req.user.id); // Clear the authentication cookie res.clearCookie('access_token', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', }); this.logger.log(`User ${req.user.email} logged out successfully`); return res.status(HttpStatus.OK).json(result); } catch (error) { this.logger.error('Logout error:', error); throw new HttpException('Logout failed', HttpStatus.INTERNAL_SERVER_ERROR); } } @Get('profile') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Get current user profile', description: 'Returns the authenticated user\'s profile information' }) @ApiResponse({ status: 200, description: 'User profile retrieved successfully', type: AuthProfileDto }) @ApiResponse({ status: 401, description: 'Unauthorized' }) async getProfile(@Req() req: AuthenticatedRequest): Promise { try { const user = await this.authService.getProfile(req.user.id); return { id: user.id, email: user.email, plan: user.plan, quotaRemaining: user.quotaRemaining, quotaResetDate: user.quotaResetDate, isActive: user.isActive, createdAt: user.createdAt, }; } catch (error) { this.logger.error('Get profile error:', error); throw new HttpException('Failed to retrieve profile', HttpStatus.INTERNAL_SERVER_ERROR); } } @Get('status') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Check authentication status', description: 'Verifies if the current JWT token is valid' }) @ApiResponse({ status: 200, description: 'Token is valid', schema: { type: 'object', properties: { authenticated: { type: 'boolean', example: true }, user: { type: 'object', properties: { id: { type: 'string' }, email: { type: 'string' }, plan: { type: 'string' }, } } } } }) @ApiResponse({ status: 401, description: 'Token is invalid or expired' }) async checkStatus(@Req() req: AuthenticatedRequest) { return { authenticated: true, user: { id: req.user.id, email: req.user.email, plan: req.user.plan, quotaRemaining: req.user.quotaRemaining, }, }; } }