[Backend] API Documents Légaux #31 #70

Merged
jmartin merged 1 commits from feature/31-api-documents-legaux into master 2025-11-30 15:00:53 +00:00
5 changed files with 239 additions and 0 deletions

View File

@ -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<DocumentsActifsResponseDto> {
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<DocumentVersionDto[]> {
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)',
};
}
}

View File

@ -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 {}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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';
}