Files
goodgo-platform/apps/api/src/modules/auth/presentation/controllers/oauth.controller.ts
Ho Ngoc Hai 312532b1cb fix(api): resolve NestJS DI + ValidationPipe bugs from type-only imports
- Remove `type` modifier from imports used as DI constructor params
  across ~235 files (@Injectable, @Controller, @Module, @Catch,
  @CommandHandler, @QueryHandler, @EventsHandler, @WebSocketGateway).
  TypeScript emitDecoratorMetadata strips type-only imports, leaving
  Reflect.metadata with Function placeholder and breaking Nest DI.
- Fix controllers: DTOs used with @Body/@Query/@Param must be runtime
  imports so ValidationPipe can whitelist properties. Previously
  returned 400 "property X should not exist" on every request.
- Register ProjectsModule in AppModule (was defined but never wired).
- Add approve()/reject() methods to TransferListingEntity referenced by
  ModerateTransferListingHandler.
- Export BankTransferConfirmedEvent from payments barrel for
  subscription activation handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:50:30 +07:00

113 lines
3.8 KiB
TypeScript

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<void> {
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<void> {
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`);
}
}
}