import { Controller, Get, Query, Req, Res, UseGuards, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { Throttle } from '@nestjs/throttler'; import { type Request, type Response } from 'express'; import { UnauthorizedException } from '@modules/shared'; import { type TokenPair } from '../../infrastructure/services/token.service'; import { ZaloOAuthStrategy } from '../../infrastructure/strategies/zalo-oauth.strategy'; import { GoogleOAuthGuard } from '../guards/google-oauth.guard'; const IS_PRODUCTION = process.env['NODE_ENV'] === 'production'; const ACCESS_TOKEN_MAX_AGE = 15 * 60 * 1000; const REFRESH_TOKEN_MAX_AGE = 30 * 24 * 60 * 60 * 1000; const AUTH_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 * 1000; const FRONTEND_URL = process.env['FRONTEND_URL'] ?? 'http://localhost:3000'; function setAuthCookies(res: Response, tokens: TokenPair): void { res.cookie('access_token', tokens.accessToken, { httpOnly: true, secure: IS_PRODUCTION, sameSite: 'strict', path: '/', maxAge: ACCESS_TOKEN_MAX_AGE, }); res.cookie('refresh_token', tokens.refreshToken, { httpOnly: true, secure: IS_PRODUCTION, sameSite: 'strict', path: '/auth', maxAge: REFRESH_TOKEN_MAX_AGE, }); res.cookie('goodgo_authenticated', '1', { httpOnly: false, secure: IS_PRODUCTION, sameSite: 'lax', path: '/', maxAge: AUTH_COOKIE_MAX_AGE, }); } @ApiTags('auth') @Controller('auth') export class OAuthController { constructor(private readonly zaloStrategy: ZaloOAuthStrategy) {} // ─── Google OAuth ────────────────────────────────────────────────── @Get('google') @UseGuards(GoogleOAuthGuard) @ApiOperation({ summary: 'Initiate Google OAuth2 login' }) @ApiResponse({ status: 302, description: 'Redirect to Google consent screen' }) googleLogin(): void { // Guard handles redirect to Google } @Throttle({ default: { ttl: 3_600_000, limit: 10 } }) @Get('google/callback') @UseGuards(GoogleOAuthGuard) @ApiOperation({ summary: 'Google OAuth2 callback' }) @ApiResponse({ status: 302, description: 'Redirect to frontend with auth cookies' }) async googleCallback( @Req() req: Request, @Res() res: Response, ): Promise { const tokens = req.user as TokenPair | undefined; if (!tokens?.accessToken) { throw new UnauthorizedException('Google authentication failed'); } setAuthCookies(res, tokens); res.redirect(`${FRONTEND_URL}/auth/callback?provider=google`); } // ─── Zalo OAuth ──────────────────────────────────────────────────── @Get('zalo') @ApiOperation({ summary: 'Initiate Zalo OAuth2 login' }) @ApiResponse({ status: 302, description: 'Redirect to Zalo consent screen' }) zaloLogin(@Res() res: Response): void { const authUrl = this.zaloStrategy.getAuthorizationUrl(); res.redirect(authUrl); } @Throttle({ default: { ttl: 3_600_000, limit: 10 } }) @Get('zalo/callback') @ApiOperation({ summary: 'Zalo OAuth2 callback' }) @ApiResponse({ status: 302, description: 'Redirect to frontend with auth cookies' }) async zaloCallback( @Query('code') code: string, @Query('code_verifier') codeVerifier: string | undefined, @Res() res: Response, ): Promise { if (!code) { throw new UnauthorizedException('Zalo authorization code missing'); } try { const result = await this.zaloStrategy.handleCallback(code, codeVerifier); setAuthCookies(res, result.tokens); res.redirect(`${FRONTEND_URL}/auth/callback?provider=zalo`); } catch { res.redirect(`${FRONTEND_URL}/auth/callback?error=zalo_auth_failed`); } } }