import { Body, Controller, Get, Post, Req, Res, HttpCode, HttpStatus, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger'; import { Request, Response } from 'express'; import { ConfigService } from '@nestjs/config'; import { ZodValidationPipe } from '../../common/pipes/zod-validation.pipe'; import { Public } from '../../common/decorators/public.decorator'; import { ChangePasswordDtoSchema, LoginDtoSchema, RefreshDtoSchema, RegisterDtoSchema, } from './dto/auth.dto'; import { AuthService } from './auth.service'; import { CsrfMiddleware } from '../../common/middleware/csrf.middleware'; import { setRefreshCookie, clearRefreshCookie } from './cookie'; @ApiTags('auth') @Controller('api/v1/auth') export class AuthController { constructor( private readonly auth: AuthService, private readonly csrf: CsrfMiddleware, private readonly config: ConfigService, ) {} @Public() @Post('register') @ApiOperation({ summary: 'Public player self-registration' }) @ApiResponse({ status: 201, description: 'Registration successful, returns login session' }) async register( @Body(new ZodValidationPipe(RegisterDtoSchema)) body: { username: string; password: string; passwordConfirm: string; }, @Req() req: Request, @Res({ passthrough: true }) res: Response, ) { const ip = (req.ip || req.socket.remoteAddress || 'unknown') as string; const session = await this.auth.registerPlayer(body, ip); setRefreshCookie(res, session.refreshToken, this.config); return { accessToken: session.accessToken, expiresIn: session.expiresIn, user: session.user, }; } @Public() @Post('login') @ApiOperation({ summary: 'Login with username and password' }) @ApiBody({ schema: { example: { username: 'admin', password: 'SuperSecret123!' } } }) @ApiResponse({ status: 200, description: 'Login successful' }) async login( @Body(new ZodValidationPipe(LoginDtoSchema)) body: { username: string; password: string }, @Req() req: Request, @Res({ passthrough: true }) res: Response, ) { const ip = (req.ip || req.socket.remoteAddress || 'unknown') as string; const session = await this.auth.login(body, ip); setRefreshCookie(res, session.refreshToken, this.config); return { accessToken: session.accessToken, expiresIn: session.expiresIn, user: session.user, }; } @Public() @Post('refresh') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Rotate the refresh token' }) async refresh( @Req() req: Request, @Res({ passthrough: true }) res: Response, @Body(new ZodValidationPipe(RefreshDtoSchema)) body: { refreshToken?: string }, ) { const fromCookie = (req.cookies?.['rt'] as string | undefined) ?? ''; const token = body.refreshToken ?? fromCookie; if (!token) return { message: 'No refresh token' }; const session = await this.auth.refresh(token); setRefreshCookie(res, session.refreshToken, this.config); return { accessToken: session.accessToken, expiresIn: session.expiresIn, user: session.user, }; } @Public() @Post('logout') @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ summary: 'Invalidate the current refresh token' }) async logout(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise { const token = (req.cookies?.['rt'] as string | undefined) ?? ''; await this.auth.logout(token || undefined); clearRefreshCookie(res); } @Get('me') @ApiOperation({ summary: 'Current authenticated user profile (id, username, role, rank, points)' }) @ApiResponse({ status: 200, description: 'Returns the current user projection.' }) async me(@Req() req: Request): Promise<{ id: string; username: string; role: 'admin' | 'player'; rank: number | null; points: number; }> { const user = req.user as { sub: string } | undefined; const userId = user?.sub; if (!userId) { throw new (await import('@nestjs/common')).UnauthorizedException('Not authenticated'); } return this.auth.getMe(userId); } @Post('change-password') @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ summary: 'Change the current user password (requires old password)' }) @ApiBody({ schema: { example: { oldPassword: 'Sup3rSecret!Pass', newPassword: 'NewSecret!Pass1', confirmNewPassword: 'NewSecret!Pass1' }, }, }) async changePassword( @Req() req: Request, @Body(new ZodValidationPipe(ChangePasswordDtoSchema)) body: { oldPassword: string; newPassword: string; confirmNewPassword: string }, ): Promise { const user = req.user as { sub: string } | undefined; const userId = user?.sub; if (!userId) { throw new (await import('@nestjs/common')).UnauthorizedException('Not authenticated'); } await this.auth.changePassword(userId, body); } @Public() @Get('csrf') @ApiOperation({ summary: 'Issue a CSRF token (sets csrf cookie, returns token)' }) @ApiResponse({ status: 200, schema: { example: { csrfToken: '...' } } }) csrfEndpoint(@Req() req: Request, @Res({ passthrough: true }) res: Response): { csrfToken: string } { const token = this.csrf.setOrGetCsrfToken(req, res); return { csrfToken: token }; } }