import { Body, Controller, Get, HttpCode, Post, Req, Res, UseGuards, UnauthorizedException, } from '@nestjs/common'; import { ApiBearerAuth, ApiNoContentResponse, ApiOkResponse, ApiProperty, ApiTags, } from '@nestjs/swagger'; import type { Request, Response } from 'express'; import { CredentialsDto } from './auth.dto'; import { AuthService } from './auth.service'; import { AccessPayload } from './auth.types'; import { JwtAuthGuard } from './jwt-auth.guard'; import { ApiProblemResponses } from '../common/decorators/api-safe-error-response.decorator'; const REFRESH_COOKIE = 'refresh_token'; const COOKIE_PATH = '/api/v1/auth'; class UserDto { @ApiProperty() id: string; @ApiProperty() email: string; } class AuthenticationDto { @ApiProperty() accessToken: string; @ApiProperty({ type: UserDto }) user: UserDto; } @ApiTags('auth') @ApiProblemResponses() @Controller('auth') export class AuthController { constructor(private readonly auth: AuthService) {} @Post('register') @HttpCode(200) @ApiOkResponse({ type: AuthenticationDto }) async register( @Body() credentials: CredentialsDto, @Res({ passthrough: true }) response: Response, ) { return this.respond( response, await this.auth.register(credentials.email, credentials.password), ); } @Post('login') @HttpCode(200) @ApiOkResponse({ type: AuthenticationDto }) async login( @Body() credentials: CredentialsDto, @Res({ passthrough: true }) response: Response, ) { return this.respond( response, await this.auth.login(credentials.email, credentials.password), ); } @Post('refresh') @HttpCode(200) @ApiOkResponse({ type: AuthenticationDto }) async refresh( @Req() request: Request, @Res({ passthrough: true }) response: Response, ) { try { const token = this.refreshToken(request); if (!token) throw new UnauthorizedException({ code: 'UNAUTHORIZED' }); return this.respond(response, await this.auth.refresh(token)); } catch (error) { this.clearCookie(response); throw error; } } @Post('logout') @HttpCode(204) @ApiNoContentResponse() async logout( @Req() request: Request, @Res({ passthrough: true }) response: Response, ): Promise { await this.auth.logout(this.refreshToken(request)); this.clearCookie(response); } @Get('me') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOkResponse({ type: UserDto }) async me(@Req() request: Request & { user: AccessPayload }) { const user = await this.auth.userById(request.user.sub); return this.auth.toUser(user); } private respond( response: Response, result: { user: { id: string; email: string }; accessToken: string; refreshToken: string; }, ) { response.cookie(REFRESH_COOKIE, result.refreshToken, this.cookieOptions()); return { user: result.user, accessToken: result.accessToken }; } private clearCookie(response: Response): void { response.clearCookie(REFRESH_COOKIE, this.cookieOptions()); } private refreshToken(request: Request): string | undefined { const token: unknown = request.cookies?.[REFRESH_COOKIE]; return typeof token === 'string' ? token : undefined; } private cookieOptions() { return { httpOnly: true, sameSite: 'lax' as const, secure: process.env['NODE_ENV'] !== 'development', path: COOKIE_PATH, }; } }