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'], }); } }