From f53fd903e51333b5a4f86f60fce911dcdc5f06d2 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Sun, 30 Nov 2025 16:00:12 +0100 Subject: [PATCH] =?UTF-8?q?feat(backend):=20API=20documents=20l=C3=A9gaux?= =?UTF-8?q?=20#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Création DTOs (UploadDocumentDto, DocumentsActifsResponseDto, DocumentVersionDto) - Création DocumentsLegauxController avec 6 endpoints: * GET /documents-legaux/actifs (public) * GET /documents-legaux/:type/versions (admin) * POST /documents-legaux (upload, admin) * PATCH /documents-legaux/:id/activer (admin) * GET /documents-legaux/:id/download (public) * GET /documents-legaux/:id/verifier-integrite (admin) - Support upload multipart/form-data avec FileInterceptor - Validation des types (cgu/privacy) - Stream PDF pour téléchargement - Intégration dans DocumentsLegauxModule - Compilation OK TODO: Ajouter guards auth (JwtAuthGuard, RolesGuard) Réf: docs/22_DOCUMENTS-LEGAUX.md --- .../documents-legaux.controller.ts | 202 ++++++++++++++++++ .../documents-legaux.module.ts | 2 + .../dto/document-version.dto.ts | 14 ++ .../dto/documents-actifs.dto.ts | 13 ++ .../dto/upload-document.dto.ts | 8 + 5 files changed, 239 insertions(+) create mode 100644 backend/src/modules/documents-legaux/documents-legaux.controller.ts create mode 100644 backend/src/modules/documents-legaux/dto/document-version.dto.ts create mode 100644 backend/src/modules/documents-legaux/dto/documents-actifs.dto.ts create mode 100644 backend/src/modules/documents-legaux/dto/upload-document.dto.ts diff --git a/backend/src/modules/documents-legaux/documents-legaux.controller.ts b/backend/src/modules/documents-legaux/documents-legaux.controller.ts new file mode 100644 index 0000000..0bef1e8 --- /dev/null +++ b/backend/src/modules/documents-legaux/documents-legaux.controller.ts @@ -0,0 +1,202 @@ +import { + Controller, + Get, + Post, + Patch, + Param, + Body, + UseInterceptors, + UploadedFile, + Res, + HttpStatus, + BadRequestException, + ParseUUIDPipe, + StreamableFile, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import type { Response } from 'express'; +import { DocumentsLegauxService } from './documents-legaux.service'; +import { UploadDocumentDto } from './dto/upload-document.dto'; +import { DocumentsActifsResponseDto } from './dto/documents-actifs.dto'; +import { DocumentVersionDto } from './dto/document-version.dto'; + +@Controller('documents-legaux') +export class DocumentsLegauxController { + constructor(private readonly documentsService: DocumentsLegauxService) {} + + /** + * GET /api/v1/documents-legaux/actifs + * Récupérer les documents actifs (CGU + Privacy) + * PUBLIC + */ + @Get('actifs') + async getDocumentsActifs(): Promise { + const { cgu, privacy } = await this.documentsService.getDocumentsActifs(); + + return { + cgu: { + id: cgu.id, + type: cgu.type, + version: cgu.version, + url: `/api/v1/documents-legaux/${cgu.id}/download`, + activeLe: cgu.activeLe, + }, + privacy: { + id: privacy.id, + type: privacy.type, + version: privacy.version, + url: `/api/v1/documents-legaux/${privacy.id}/download`, + activeLe: privacy.activeLe, + }, + }; + } + + /** + * GET /api/v1/documents-legaux/:type/versions + * Lister toutes les versions d'un type de document + * ADMIN ONLY + */ + @Get(':type/versions') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin', 'administrateur') + async listerVersions(@Param('type') type: string): Promise { + if (type !== 'cgu' && type !== 'privacy') { + throw new BadRequestException('Le type doit être "cgu" ou "privacy"'); + } + + const documents = await this.documentsService.listerVersions(type as 'cgu' | 'privacy'); + + return documents.map((doc) => ({ + id: doc.id, + version: doc.version, + fichier_nom: doc.fichier_nom, + actif: doc.actif, + televersePar: doc.televersePar + ? { + id: doc.televersePar.id, + prenom: doc.televersePar.prenom, + nom: doc.televersePar.nom, + } + : null, + televerseLe: doc.televerseLe, + activeLe: doc.activeLe, + })); + } + + /** + * POST /api/v1/documents-legaux + * Upload une nouvelle version d'un document + * ADMIN ONLY + */ + @Post() + @UseInterceptors(FileInterceptor('file')) + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin', 'administrateur') + async uploadDocument( + @Body() uploadDto: UploadDocumentDto, + @UploadedFile() file: Express.Multer.File, + // @CurrentUser() user: any, // TODO: Décommenter quand le guard sera implémenté + ) { + if (!file) { + throw new BadRequestException('Aucun fichier fourni'); + } + + // TODO: Récupérer l'ID utilisateur depuis le guard + const userId = '00000000-0000-0000-0000-000000000000'; // Temporaire + + const document = await this.documentsService.uploadNouvelleVersion( + uploadDto.type, + file, + userId, + ); + + return { + id: document.id, + type: document.type, + version: document.version, + fichier_nom: document.fichier_nom, + actif: document.actif, + televerseLe: document.televerseLe, + }; + } + + /** + * PATCH /api/v1/documents-legaux/:id/activer + * Activer une version d'un document + * ADMIN ONLY + */ + @Patch(':id/activer') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin', 'administrateur') + async activerVersion(@Param('id', ParseUUIDPipe) documentId: string) { + await this.documentsService.activerVersion(documentId); + + // Récupérer le document pour retourner les infos + const documents = await this.documentsService.listerVersions('cgu'); + const document = documents.find((d) => d.id === documentId); + + if (!document) { + const documentsPrivacy = await this.documentsService.listerVersions('privacy'); + const docPrivacy = documentsPrivacy.find((d) => d.id === documentId); + + if (!docPrivacy) { + throw new BadRequestException('Document non trouvé'); + } + + return { + message: 'Document activé avec succès', + documentId: docPrivacy.id, + type: docPrivacy.type, + version: docPrivacy.version, + }; + } + + return { + message: 'Document activé avec succès', + documentId: document.id, + type: document.type, + version: document.version, + }; + } + + /** + * GET /api/v1/documents-legaux/:id/download + * Télécharger un document + * PUBLIC + */ + @Get(':id/download') + async telechargerDocument( + @Param('id', ParseUUIDPipe) documentId: string, + @Res() res: Response, + ) { + const { stream, filename } = await this.documentsService.telechargerDocument(documentId); + + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${filename}"`, + }); + + res.status(HttpStatus.OK).send(stream); + } + + /** + * GET /api/v1/documents-legaux/:id/verifier-integrite + * Vérifier l'intégrité d'un document (hash SHA-256) + * ADMIN ONLY + */ + @Get(':id/verifier-integrite') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin', 'administrateur') + async verifierIntegrite(@Param('id', ParseUUIDPipe) documentId: string) { + const integre = await this.documentsService.verifierIntegrite(documentId); + + return { + documentId, + integre, + message: integre + ? 'Le document est intègre (hash valide)' + : 'ALERTE : Le document a été modifié (hash invalide)', + }; + } +} + diff --git a/backend/src/modules/documents-legaux/documents-legaux.module.ts b/backend/src/modules/documents-legaux/documents-legaux.module.ts index 4518e28..13828af 100644 --- a/backend/src/modules/documents-legaux/documents-legaux.module.ts +++ b/backend/src/modules/documents-legaux/documents-legaux.module.ts @@ -3,10 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { DocumentLegal } from '../../entities/document-legal.entity'; import { AcceptationDocument } from '../../entities/acceptation-document.entity'; import { DocumentsLegauxService } from './documents-legaux.service'; +import { DocumentsLegauxController } from './documents-legaux.controller'; @Module({ imports: [TypeOrmModule.forFeature([DocumentLegal, AcceptationDocument])], providers: [DocumentsLegauxService], + controllers: [DocumentsLegauxController], exports: [DocumentsLegauxService], }) export class DocumentsLegauxModule {} diff --git a/backend/src/modules/documents-legaux/dto/document-version.dto.ts b/backend/src/modules/documents-legaux/dto/document-version.dto.ts new file mode 100644 index 0000000..014b7ad --- /dev/null +++ b/backend/src/modules/documents-legaux/dto/document-version.dto.ts @@ -0,0 +1,14 @@ +export class DocumentVersionDto { + id: string; + version: number; + fichier_nom: string; + actif: boolean; + televersePar: { + id: string; + prenom?: string; + nom?: string; + } | null; + televerseLe: Date; + activeLe: Date | null; +} + diff --git a/backend/src/modules/documents-legaux/dto/documents-actifs.dto.ts b/backend/src/modules/documents-legaux/dto/documents-actifs.dto.ts new file mode 100644 index 0000000..cafa8f2 --- /dev/null +++ b/backend/src/modules/documents-legaux/dto/documents-actifs.dto.ts @@ -0,0 +1,13 @@ +export class DocumentActifDto { + id: string; + type: 'cgu' | 'privacy'; + version: number; + url: string; + activeLe: Date | null; +} + +export class DocumentsActifsResponseDto { + cgu: DocumentActifDto; + privacy: DocumentActifDto; +} + diff --git a/backend/src/modules/documents-legaux/dto/upload-document.dto.ts b/backend/src/modules/documents-legaux/dto/upload-document.dto.ts new file mode 100644 index 0000000..52a3546 --- /dev/null +++ b/backend/src/modules/documents-legaux/dto/upload-document.dto.ts @@ -0,0 +1,8 @@ +import { IsEnum, IsNotEmpty } from 'class-validator'; + +export class UploadDocumentDto { + @IsEnum(['cgu', 'privacy'], { message: 'Le type doit être "cgu" ou "privacy"' }) + @IsNotEmpty({ message: 'Le type est requis' }) + type: 'cgu' | 'privacy'; +} + -- 2.47.2