- 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
210 lines
6.1 KiB
TypeScript
210 lines
6.1 KiB
TypeScript
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<DocumentLegal>,
|
|
@InjectRepository(AcceptationDocument)
|
|
private acceptationRepo: Repository<AcceptationDocument>,
|
|
) {}
|
|
|
|
/**
|
|
* 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<DocumentLegal> {
|
|
// 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<void> {
|
|
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<DocumentLegal[]> {
|
|
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<boolean> {
|
|
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<AcceptationDocument> {
|
|
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<AcceptationDocument[]> {
|
|
return await this.acceptationRepo.find({
|
|
where: { utilisateur: { id: userId } },
|
|
order: { accepteLe: 'DESC' },
|
|
relations: ['document'],
|
|
});
|
|
}
|
|
}
|
|
|