import { Controller, Get, Post, Query, Res, Req, Body, HttpCode, HttpStatus, Logger, UseGuards } from '@nestjs/common'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { Response, Request } from 'express'; import { JwtGuard } from '../../../core/auth/guards/jwt.guard'; import { RefreshGuard } from '../../../core/auth/guards/refresh.guard'; import { ConfigService } from '@nestjs/config'; @ApiTags('Auth') @Controller('auth') export class AuthController { private readonly logger = new Logger('AuthController'); constructor(private readonly authService: AuthService, private readonly config: ConfigService) {} @Get('login') @ApiOperation({ summary: 'Start Authorization Code + PKCE login (redirect to Identity Provider)' }) async login(@Query('returnTo') returnTo: string | undefined, @Res() res: Response) { const redirect = await this.authService.createAuthorizationRedirect(returnTo); return res.redirect(302, redirect); } @Get('callback') @ApiOperation({ summary: 'OIDC callback endpoint' }) async callback(@Query('code') code: string, @Query('state') state: string, @Res() res: Response) { const result = await this.authService.handleCallback(code, state); // set cookies with environment-aware options const isProd = this.config.get('NODE_ENV') === 'production' || process.env.NODE_ENV === 'production'; const cookieDomain = this.config.get('RAYLAB_COOKIE_DOMAIN') || (isProd ? '.raylab.site' : undefined); const cookieOptions: any = { httpOnly: true, secure: isProd, // secure in production sameSite: isProd ? 'none' : 'lax', // cross-site in production path: '/', }; if (cookieDomain) cookieOptions.domain = cookieDomain; // access token cookie (internal JWT) this.logger.log(`Setting access & refresh cookies. cookieOptions=${JSON.stringify(cookieOptions)}`); res.cookie('raylab_jwt', result.accessToken, { ...cookieOptions, maxAge: result.expiresIn * 1000 }); // refresh token cookie res.cookie('raylab_refresh', result.refreshToken, { ...cookieOptions, maxAge: result.refreshTtl * 1000 }); this.logger.debug(`Callback complete. returnTo=${result.returnTo} user=${JSON.stringify(result.user)}`); return res.redirect(302, result.returnTo || '/'); } @Post('logout') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Logout (invalidate internal session and redirect to identity provider logout)' }) async logout(@Req() req: Request, @Res() res: Response) { const refreshToken = req.cookies?.raylab_refresh || req.body?.refreshToken; const redirect = await this.authService.logout(refreshToken); return res.redirect(302, redirect); } @Post('refresh') @UseGuards(RefreshGuard) @ApiOperation({ summary: 'Refresh internal JWT using internal refresh token' }) async refresh(@Req() req: Request, @Res() res: Response) { // req.authInfo should contain { refreshToken } const refreshToken = (req as any).authInfo?.refreshToken || (req.cookies?.raylab_refresh) || (req.body?.refreshToken); this.logger.debug(`Refresh called (guarded). cookies=${JSON.stringify((req as any).cookies)} authInfo=${JSON.stringify((req as any).authInfo)}`); const result = await this.authService.refresh(refreshToken); // set cookies with environment-aware options const isProd = this.config.get('NODE_ENV') === 'production' || process.env.NODE_ENV === 'production'; const cookieDomain = this.config.get('RAYLAB_COOKIE_DOMAIN') || (isProd ? '.raylab.site' : undefined); const cookieOptions: any = { httpOnly: true, secure: isProd, sameSite: isProd ? 'none' : 'lax', path: '/', }; if (cookieDomain) cookieOptions.domain = cookieDomain; // set new cookies (rotate) res.cookie('raylab_jwt', result.accessToken, { ...cookieOptions, maxAge: result.expiresIn * 1000 }); res.cookie('raylab_refresh', result.refreshToken, { ...cookieOptions, maxAge: result.refreshTtl * 1000 }); this.logger.debug(`Refresh complete. set new cookies for user`); return res.json({ success: true, data: result }); } @Get('me') @UseGuards(JwtGuard) @ApiOperation({ summary: 'Get current user from internal JWT (cookie or Authorization header)' }) async me(@Req() req: Request) { // With JwtGuard, req.user should be payload from token this.logger.debug(`Me called (guarded). cookies=${JSON.stringify((req as any).cookies)} user=${JSON.stringify((req as any).user)}`); const payload = (req as any).user; const user = await this.authService.me((req.cookies?.raylab_jwt) || (req.headers.authorization && (req.headers.authorization as string).replace(/^Bearer\s+/i, ''))); this.logger.debug(`Me returning user id=${user?.id}`); return { success: true, data: user }; } }