102 lines
4.8 KiB
TypeScript
102 lines
4.8 KiB
TypeScript
|
|
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<string>('NODE_ENV') === 'production' || process.env.NODE_ENV === 'production';
|
|
const cookieDomain = this.config.get<string>('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<string>('NODE_ENV') === 'production' || process.env.NODE_ENV === 'production';
|
|
const cookieDomain = this.config.get<string>('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 };
|
|
}
|
|
}
|