feat: Intégration du backend NestJS depuis YNOV

- Framework: NestJS avec TypeORM
- Authentification: JWT (access + refresh tokens)
- Gestion utilisateurs: CRUD complet avec validation
- Routes: auth, users, parents, assistantes maternelles
- Dockerfile pour conteneurisation
This commit is contained in:
2025-11-24 15:44:07 +01:00
parent 49f0684ad3
commit 33d6e7b0c3
108 changed files with 13872 additions and 1908 deletions
@@ -0,0 +1,4 @@
import { SetMetadata } from "@nestjs/common";
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@@ -0,0 +1,3 @@
import { SetMetadata } from "@nestjs/common";
export const Roles = (...roles: string[]) => SetMetadata("roles", roles);
@@ -0,0 +1,7 @@
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
export const User = createParamDecorator((data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
});
@@ -0,0 +1,11 @@
import { IsDateString, IsOptional } from "class-validator";
export class DateRangeQueryDto {
@IsOptional()
@IsDateString()
start?: string;
@IsOptional()
@IsDateString()
end?: string;
}
+6
View File
@@ -0,0 +1,6 @@
import { IsUUID } from "class-validator";
export class IdParamDto {
@IsUUID()
id: string;
}
@@ -0,0 +1,11 @@
import { IsOptional, IsPositive } from "class-validator";
export class PaginationQueryDto {
@IsOptional()
@IsPositive()
offset?: number;
@IsOptional()
@IsPositive()
limit?: number;
}
@@ -0,0 +1,8 @@
import { IsOptional, IsString, MinLength } from "class-validator";
export class SearchQueryDto {
@IsOptional()
@IsString()
@MinLength(2)
q?: string;
}
@@ -0,0 +1,27 @@
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.getResponse()
: { message: 'Internal server error' };
response.status(status).json({
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
}
+49
View File
@@ -0,0 +1,49 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { JwtService } from "@nestjs/jwt";
import { Request } from 'express';
import { IS_PUBLIC_KEY } from "../decorators/public.decorator";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly reflector: Reflector,
private readonly configService: ConfigService,
) {}
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<Request>();
if (request.path.startsWith('/api-docs')) {
return true;
}
const authHeader = request.headers['authorization'] as string | undefined;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Token manquant ou invalide');
}
const token = authHeader.split(' ')[1];
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>('jwt.accessSecret'),
});
request.user = {
...payload,
id: payload.sub,
};
return true;
} catch (error) {
throw new UnauthorizedException('Token invalide ou expiré');
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable } from "rxjs";
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user || !user.role) {
return false;
}
return requiredRoles.includes(user.role);
}
}
@@ -0,0 +1,15 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { map, Observable, timestamp } from "rxjs";
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => ({
success: true,
timestamp: new Date().toISOString(),
data
})),
);
}
}