From 1fb8c33cbfd5e907f01564494edfe9e79140e13f Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Sun, 30 Nov 2025 15:50:38 +0100 Subject: [PATCH] =?UTF-8?q?feat(backend):=20service=20gestion=20documents?= =?UTF-8?q?=20l=C3=A9gaux=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Création entité DocumentLegal - Création entité AcceptationDocument - Création DocumentsLegauxService avec méthodes: * getDocumentsActifs() * uploadNouvelleVersion() (avec hash SHA-256) * activerVersion() (transaction) * listerVersions() * telechargerDocument() * verifierIntegrite() * enregistrerAcceptation() * getAcceptationsUtilisateur() - Création DocumentsLegauxModule - Intégration dans AppModule - Ajout dépendances multer + @types/multer Réf: docs/22_DOCUMENTS-LEGAUX.md --- backend/package.json | 2 + backend/src/app.module.ts | 2 + .../entities/acceptation-document.entity.ts | 40 ++++ backend/src/entities/document-legal.entity.ts | 44 ++++ .../documents-legaux.module.ts | 13 ++ .../documents-legaux.service.ts | 209 ++++++++++++++++++ backend/src/modules/documents-legaux/index.ts | 3 + 7 files changed, 313 insertions(+) create mode 100644 backend/src/entities/acceptation-document.entity.ts create mode 100644 backend/src/entities/document-legal.entity.ts create mode 100644 backend/src/modules/documents-legaux/documents-legaux.module.ts create mode 100644 backend/src/modules/documents-legaux/documents-legaux.service.ts create mode 100644 backend/src/modules/documents-legaux/index.ts diff --git a/backend/package.json b/backend/package.json index 6ffec65..0b68ed1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,6 +37,7 @@ "class-validator": "^0.14.2", "joi": "^18.0.0", "mapped-types": "^0.0.1", + "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.16", "passport-jwt": "^4.0.1", "pg": "^8.16.3", @@ -54,6 +55,7 @@ "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", + "@types/multer": "^1.4.12", "@types/node": "^22.10.7", "@types/nodemailer": "^6.4.16", "@types/passport-jwt": "^4.0.1", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 18b36a4..0124bfe 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -15,6 +15,7 @@ import { SentryGlobalFilter } from '@sentry/nestjs/setup'; import { AllExceptionsFilter } from './common/filters/all_exceptions.filters'; import { EnfantsModule } from './routes/enfants/enfants.module'; import { AppConfigModule } from './modules/config/config.module'; +import { DocumentsLegauxModule } from './modules/documents-legaux'; @Module({ imports: [ @@ -51,6 +52,7 @@ import { AppConfigModule } from './modules/config/config.module'; EnfantsModule, AuthModule, AppConfigModule, + DocumentsLegauxModule, ], controllers: [AppController], providers: [ diff --git a/backend/src/entities/acceptation-document.entity.ts b/backend/src/entities/acceptation-document.entity.ts new file mode 100644 index 0000000..011aa7d --- /dev/null +++ b/backend/src/entities/acceptation-document.entity.ts @@ -0,0 +1,40 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, +} from 'typeorm'; +import { Users } from './users.entity'; +import { DocumentLegal } from './document-legal.entity'; + +@Entity('acceptations_documents') +export class AcceptationDocument { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Users, { nullable: false, onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_utilisateur' }) + utilisateur: Users; + + @ManyToOne(() => DocumentLegal, { nullable: true }) + @JoinColumn({ name: 'id_document' }) + document: DocumentLegal | null; + + @Column({ type: 'varchar', length: 50, nullable: false }) + type_document: 'cgu' | 'privacy'; + + @Column({ type: 'integer', nullable: false }) + version_document: number; + + @CreateDateColumn({ name: 'accepte_le', type: 'timestamptz' }) + accepteLe: Date; + + @Column({ type: 'inet', nullable: true }) + ip_address: string | null; + + @Column({ type: 'text', nullable: true }) + user_agent: string | null; +} + diff --git a/backend/src/entities/document-legal.entity.ts b/backend/src/entities/document-legal.entity.ts new file mode 100644 index 0000000..845bb62 --- /dev/null +++ b/backend/src/entities/document-legal.entity.ts @@ -0,0 +1,44 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, +} from 'typeorm'; +import { Users } from './users.entity'; + +@Entity('documents_legaux') +export class DocumentLegal { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 50, nullable: false }) + type: 'cgu' | 'privacy'; + + @Column({ type: 'integer', nullable: false }) + version: number; + + @Column({ type: 'varchar', length: 255, nullable: false }) + fichier_nom: string; + + @Column({ type: 'varchar', length: 500, nullable: false }) + fichier_path: string; + + @Column({ type: 'varchar', length: 64, nullable: false }) + fichier_hash: string; + + @Column({ type: 'boolean', default: false }) + actif: boolean; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'televerse_par' }) + televersePar: Users | null; + + @CreateDateColumn({ name: 'televerse_le', type: 'timestamptz' }) + televerseLe: Date; + + @Column({ name: 'active_le', type: 'timestamptz', nullable: true }) + activeLe: Date | null; +} + diff --git a/backend/src/modules/documents-legaux/documents-legaux.module.ts b/backend/src/modules/documents-legaux/documents-legaux.module.ts new file mode 100644 index 0000000..4518e28 --- /dev/null +++ b/backend/src/modules/documents-legaux/documents-legaux.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +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'; + +@Module({ + imports: [TypeOrmModule.forFeature([DocumentLegal, AcceptationDocument])], + providers: [DocumentsLegauxService], + exports: [DocumentsLegauxService], +}) +export class DocumentsLegauxModule {} + diff --git a/backend/src/modules/documents-legaux/documents-legaux.service.ts b/backend/src/modules/documents-legaux/documents-legaux.service.ts new file mode 100644 index 0000000..02ce74b --- /dev/null +++ b/backend/src/modules/documents-legaux/documents-legaux.service.ts @@ -0,0 +1,209 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { DocumentLegal } from '../../entities/document-legal.entity'; +import { AcceptationDocument } from '../../entities/acceptation-document.entity'; +import * as crypto from 'crypto'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +@Injectable() +export class DocumentsLegauxService { + private readonly UPLOAD_DIR = '/app/documents/legaux'; + + constructor( + @InjectRepository(DocumentLegal) + private docRepo: Repository, + @InjectRepository(AcceptationDocument) + private acceptationRepo: Repository, + ) {} + + /** + * Récupérer les documents actifs (CGU + Privacy) + */ + async getDocumentsActifs(): Promise<{ cgu: DocumentLegal; privacy: DocumentLegal }> { + const cgu = await this.docRepo.findOne({ + where: { type: 'cgu', actif: true }, + }); + + const privacy = await this.docRepo.findOne({ + where: { type: 'privacy', actif: true }, + }); + + if (!cgu || !privacy) { + throw new NotFoundException('Documents légaux manquants'); + } + + return { cgu, privacy }; + } + + /** + * Uploader une nouvelle version d'un document + */ + async uploadNouvelleVersion( + type: 'cgu' | 'privacy', + file: Express.Multer.File, + userId: string, + ): Promise { + // Validation du type de fichier + if (file.mimetype !== 'application/pdf') { + throw new BadRequestException('Seuls les fichiers PDF sont acceptés'); + } + + // Validation de la taille (max 10MB) + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + throw new BadRequestException('Le fichier ne doit pas dépasser 10MB'); + } + + // 1. Calculer la prochaine version + const lastDoc = await this.docRepo.findOne({ + where: { type }, + order: { version: 'DESC' }, + }); + const nouvelleVersion = (lastDoc?.version || 0) + 1; + + // 2. Calculer le hash SHA-256 du fichier + const fileBuffer = file.buffer; + const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); + + // 3. Générer le nom de fichier unique + const timestamp = Date.now(); + const fileName = `${type}_v${nouvelleVersion}_${timestamp}.pdf`; + const filePath = path.join(this.UPLOAD_DIR, fileName); + + // 4. Créer le répertoire si nécessaire et sauvegarder le fichier + await fs.mkdir(this.UPLOAD_DIR, { recursive: true }); + await fs.writeFile(filePath, fileBuffer); + + // 5. Créer l'entrée en BDD + const document = this.docRepo.create({ + type, + version: nouvelleVersion, + fichier_nom: file.originalname, + fichier_path: filePath, + fichier_hash: hash, + actif: false, // Pas actif par défaut + televersePar: { id: userId } as any, + televerseLe: new Date(), + }); + + return await this.docRepo.save(document); + } + + /** + * Activer une version (désactive automatiquement l'ancienne) + */ + async activerVersion(documentId: string): Promise { + const document = await this.docRepo.findOne({ where: { id: documentId } }); + + if (!document) { + throw new NotFoundException('Document non trouvé'); + } + + // Transaction : désactiver l'ancienne version, activer la nouvelle + await this.docRepo.manager.transaction(async (manager) => { + // Désactiver toutes les versions de ce type + await manager.update( + DocumentLegal, + { type: document.type, actif: true }, + { actif: false }, + ); + + // Activer la nouvelle version + await manager.update( + DocumentLegal, + { id: documentId }, + { actif: true, activeLe: new Date() }, + ); + }); + } + + /** + * Lister toutes les versions d'un type de document + */ + async listerVersions(type: 'cgu' | 'privacy'): Promise { + return await this.docRepo.find({ + where: { type }, + order: { version: 'DESC' }, + relations: ['televersePar'], + }); + } + + /** + * Télécharger un document (retourne le buffer et le nom) + */ + async telechargerDocument(documentId: string): Promise<{ stream: Buffer; filename: string }> { + const document = await this.docRepo.findOne({ where: { id: documentId } }); + + if (!document) { + throw new NotFoundException('Document non trouvé'); + } + + try { + const fileBuffer = await fs.readFile(document.fichier_path); + + return { + stream: fileBuffer, + filename: document.fichier_nom, + }; + } catch (error) { + throw new NotFoundException('Fichier introuvable sur le système de fichiers'); + } + } + + /** + * Vérifier l'intégrité d'un document (hash SHA-256) + */ + async verifierIntegrite(documentId: string): Promise { + const document = await this.docRepo.findOne({ where: { id: documentId } }); + + if (!document) { + throw new NotFoundException('Document non trouvé'); + } + + try { + const fileBuffer = await fs.readFile(document.fichier_path); + const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); + + return hash === document.fichier_hash; + } catch (error) { + return false; + } + } + + /** + * Enregistrer une acceptation de document (lors de l'inscription) + */ + async enregistrerAcceptation( + userId: string, + documentId: string, + typeDocument: 'cgu' | 'privacy', + versionDocument: number, + ipAddress: string | null, + userAgent: string | null, + ): Promise { + const acceptation = this.acceptationRepo.create({ + utilisateur: { id: userId } as any, + document: { id: documentId } as any, + type_document: typeDocument, + version_document: versionDocument, + ip_address: ipAddress, + user_agent: userAgent, + }); + + return await this.acceptationRepo.save(acceptation); + } + + /** + * Récupérer l'historique des acceptations d'un utilisateur + */ + async getAcceptationsUtilisateur(userId: string): Promise { + return await this.acceptationRepo.find({ + where: { utilisateur: { id: userId } }, + order: { accepteLe: 'DESC' }, + relations: ['document'], + }); + } +} + diff --git a/backend/src/modules/documents-legaux/index.ts b/backend/src/modules/documents-legaux/index.ts new file mode 100644 index 0000000..28593ce --- /dev/null +++ b/backend/src/modules/documents-legaux/index.ts @@ -0,0 +1,3 @@ +export * from './documents-legaux.module'; +export * from './documents-legaux.service'; + -- 2.47.2