Compare commits

..

5 Commits

Author SHA1 Message Date
13744eaaaa Merge branch 'security/sakuin/guards-controllers'
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 14s
# Conflicts:
#	backend/src/work/work.controller.ts
2026-04-05 07:52:59 +02:00
494d05c755 Merge branch 'security/sakuin/throttler' 2026-04-05 07:52:31 +02:00
14823ed769 security: AuthGuard classe sur controllers + @Public() decorator pour search 2026-04-05 07:52:16 +02:00
d3e9dc61a5 security: add @nestjs/throttler — rate limiting global 60/min + search 20/min 2026-04-05 07:51:05 +02:00
2e9e438baa security: AuthGuard cache max size — eviction FIFO 1000 entries 2026-04-05 07:49:56 +02:00
7 changed files with 48 additions and 3 deletions

View File

@@ -13,6 +13,7 @@
"@nestjs/config": "^4.0.3", "@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.1.17", "@nestjs/core": "^11.1.17",
"@nestjs/platform-express": "^11.1.17", "@nestjs/platform-express": "^11.1.17",
"@nestjs/throttler": "^6.5.0",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.15.1", "class-validator": "^0.15.1",
@@ -2141,6 +2142,17 @@
} }
} }
}, },
"node_modules/@nestjs/throttler": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz",
"integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"reflect-metadata": "^0.1.13 || ^0.2.0"
}
},
"node_modules/@nestjs/typeorm": { "node_modules/@nestjs/typeorm": {
"version": "11.0.0", "version": "11.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz",

View File

@@ -21,6 +21,7 @@
"@nestjs/config": "^4.0.3", "@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.1.17", "@nestjs/core": "^11.1.17",
"@nestjs/platform-express": "^11.1.17", "@nestjs/platform-express": "^11.1.17",
"@nestjs/throttler": "^6.5.0",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.15.1", "class-validator": "^0.15.1",

View File

@@ -1,6 +1,8 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { getDatabaseConfig } from './config/database.config'; import { getDatabaseConfig } from './config/database.config';
import { HealthModule } from './health/health.module'; import { HealthModule } from './health/health.module';
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
@@ -11,6 +13,7 @@ import { ListModule } from './list/list.module';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ isGlobal: true }), ConfigModule.forRoot({ isGlobal: true }),
ThrottlerModule.forRoot([{ ttl: 60000, limit: 60 }]),
TypeOrmModule.forRootAsync({ TypeOrmModule.forRootAsync({
inject: [ConfigService], inject: [ConfigService],
useFactory: getDatabaseConfig, useFactory: getDatabaseConfig,
@@ -21,5 +24,8 @@ import { ListModule } from './list/list.module';
WorkModule, WorkModule,
ListModule, ListModule,
], ],
providers: [
{ provide: APP_GUARD, useClass: ThrottlerGuard },
],
}) })
export class AppModule {} export class AppModule {}

View File

@@ -4,7 +4,9 @@ import {
Injectable, Injectable,
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { IS_PUBLIC_KEY } from './public.decorator';
interface CacheEntry { interface CacheEntry {
user: any; user: any;
@@ -12,14 +14,24 @@ interface CacheEntry {
} }
const TOKEN_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes const TOKEN_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const MAX_CACHE_SIZE = 1000;
@Injectable() @Injectable()
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
private readonly cache = new Map<string, CacheEntry>(); private readonly cache = new Map<string, CacheEntry>();
constructor(private readonly configService: ConfigService) {} constructor(
private readonly configService: ConfigService,
private readonly reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const token = this.extractToken(request); const token = this.extractToken(request);
@@ -49,6 +61,10 @@ export class AuthGuard implements CanActivate {
const user = await this.introspect(token); const user = await this.introspect(token);
if (user) { if (user) {
if (this.cache.size >= MAX_CACHE_SIZE) {
const oldest = this.cache.keys().next().value;
if (oldest) this.cache.delete(oldest);
}
this.cache.set(token, { this.cache.set(token, {
user, user,
expiresAt: Date.now() + TOKEN_CACHE_TTL_MS, expiresAt: Date.now() + TOKEN_CACHE_TTL_MS,

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View File

@@ -2,12 +2,12 @@ import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { AuthGuard } from '../auth/auth.guard'; import { AuthGuard } from '../auth/auth.guard';
@UseGuards(AuthGuard)
@Controller('api/user') @Controller('api/user')
export class UserController { export class UserController {
constructor(private readonly userService: UserService) {} constructor(private readonly userService: UserService) {}
@Get('me') @Get('me')
@UseGuards(AuthGuard)
async me(@Req() req: any) { async me(@Req() req: any) {
const user = await this.userService.findOrCreate({ const user = await this.userService.findOrCreate({
id: req.user.id, id: req.user.id,

View File

@@ -1,10 +1,16 @@
import { Controller, Get, Query } from '@nestjs/common'; import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { AuthGuard } from '../auth/auth.guard';
import { Public } from '../auth/public.decorator';
import { WorkService } from './work.service'; import { WorkService } from './work.service';
@UseGuards(AuthGuard)
@Controller('api/works') @Controller('api/works')
export class WorkController { export class WorkController {
constructor(private readonly workService: WorkService) {} constructor(private readonly workService: WorkService) {}
@Public()
@Throttle([{ ttl: 60000, limit: 20 }])
@Get('search') @Get('search')
async search( async search(
@Query('q') query: string, @Query('q') query: string,