Files
HIPCTF2/backend/src/modules/auth/auth.controller.ts
T

156 lines
5.2 KiB
TypeScript

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<void> {
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<void> {
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 };
}
}