Files
memegoat/backend/src/auth/auth.controller.ts
Mathis HERRIOT 3f0b1e5119 feat(auth): add bootstrap token flow for initial admin creation
- Introduced `BootstrapService` to handle admin creation when no admins exist.
- Added `/auth/bootstrap-admin` endpoint to consume bootstrap tokens.
- Updated `RbacRepository` to support counting admins and assigning roles.
- Included unit tests for `BootstrapService` to ensure token behavior and admin assignment.
2026-01-21 11:07:02 +01:00

134 lines
3.6 KiB
TypeScript

import { Body, Controller, Get, Headers, Post, Query, Req, Res } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Throttle } from "@nestjs/throttler";
import type { Request, Response } from "express";
import { getIronSession } from "iron-session";
import { AuthService } from "./auth.service";
import { BootstrapService } from "./bootstrap.service";
import { LoginDto } from "./dto/login.dto";
import { RegisterDto } from "./dto/register.dto";
import { Verify2faDto } from "./dto/verify-2fa.dto";
import { getSessionOptions, SessionData } from "./session.config";
@Controller("auth")
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly bootstrapService: BootstrapService,
private readonly configService: ConfigService,
) {}
@Post("register")
@Throttle({ default: { limit: 5, ttl: 60000 } })
register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
@Post("login")
@Throttle({ default: { limit: 5, ttl: 60000 } })
async login(
@Body() loginDto: LoginDto,
@Headers("user-agent") userAgent: string,
@Req() req: Request,
@Res() res: Response,
) {
const ip = req.ip;
const result = await this.authService.login(loginDto, userAgent, ip);
if (result.access_token) {
const session = await getIronSession<SessionData>(
req,
res,
getSessionOptions(this.configService.get("SESSION_PASSWORD") as string),
);
session.accessToken = result.access_token;
session.refreshToken = result.refresh_token;
session.userId = result.userId;
await session.save();
// On ne renvoie pas les tokens dans le body pour plus de sécurité
return res.json({
message: result.message,
userId: result.userId,
});
}
return res.json(result);
}
@Post("verify-2fa")
@Throttle({ default: { limit: 5, ttl: 60000 } })
async verifyTwoFactor(
@Body() verify2faDto: Verify2faDto,
@Headers("user-agent") userAgent: string,
@Req() req: Request,
@Res() res: Response,
) {
const ip = req.ip;
const result = await this.authService.verifyTwoFactorLogin(
verify2faDto.userId,
verify2faDto.token,
userAgent,
ip,
);
if (result.access_token) {
const session = await getIronSession<SessionData>(
req,
res,
getSessionOptions(this.configService.get("SESSION_PASSWORD") as string),
);
session.accessToken = result.access_token;
session.refreshToken = result.refresh_token;
session.userId = verify2faDto.userId;
await session.save();
return res.json({
message: result.message,
});
}
return res.json(result);
}
@Post("refresh")
async refresh(@Req() req: Request, @Res() res: Response) {
const session = await getIronSession<SessionData>(
req,
res,
getSessionOptions(this.configService.get("SESSION_PASSWORD") as string),
);
if (!session.refreshToken) {
return res.status(401).json({ message: "No refresh token" });
}
const result = await this.authService.refresh(session.refreshToken);
session.accessToken = result.access_token;
session.refreshToken = result.refresh_token;
await session.save();
return res.json({ message: "Token refreshed" });
}
@Post("logout")
async logout(@Req() req: Request, @Res() res: Response) {
const session = await getIronSession<SessionData>(
req,
res,
getSessionOptions(this.configService.get("SESSION_PASSWORD") as string),
);
session.destroy();
return res.json({ message: "User logged out" });
}
@Get("bootstrap-admin")
async bootstrapAdmin(
@Query("token") token: string,
@Query("username") username: string,
) {
return this.bootstrapService.consumeToken(token, username);
}
}