- 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>
113 lines
3.8 KiB
TypeScript
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`);
|
|
}
|
|
}
|
|
}
|