espace-paie-odentas/lib/signature-proof-pdf.ts

253 lines
7.9 KiB
TypeScript

import { jsPDF } from "jspdf";
interface SignatureProofData {
document_name: string;
signer_name: string;
signer_email: string;
signed_at: string;
signature_hash: string;
verification_url: string;
qr_code_data_url: string;
certificate_info: {
issuer: string;
subject: string;
valid_from: string;
valid_until: string;
serial_number: string;
};
}
/**
* Génère un PDF "Preuve de Signature" avec QR code
*/
export async function generateSignatureProofPDF(data: SignatureProofData): Promise<Blob> {
const doc = new jsPDF({
orientation: "portrait",
unit: "mm",
format: "a4",
});
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const margin = 20;
// Couleurs Odentas
const primaryColor: [number, number, number] = [99, 102, 241]; // Indigo
const successColor: [number, number, number] = [34, 197, 94]; // Green-500
const textColor: [number, number, number] = [30, 41, 59]; // Slate-800
const lightGray: [number, number, number] = [248, 250, 252]; // Slate-50
const borderGray: [number, number, number] = [226, 232, 240]; // Slate-200
// En-tête avec logo - Version professionnelle
doc.setFillColor(...primaryColor);
doc.rect(0, 0, pageWidth, 45, "F");
doc.setTextColor(255, 255, 255);
doc.setFontSize(28);
doc.setFont("helvetica", "bold");
doc.text("Odentas Sign", pageWidth / 2, 22, { align: "center" });
doc.setFontSize(11);
doc.setFont("helvetica", "normal");
doc.text("Certificat de Signature Électronique", pageWidth / 2, 32, { align: "center" });
// Badge "VALIDE"
doc.setFillColor(...successColor);
const badgeWidth = 30;
const badgeX = (pageWidth - badgeWidth) / 2;
doc.roundedRect(badgeX, 36, badgeWidth, 6, 1.5, 1.5, "F");
doc.setFontSize(8);
doc.setFont("helvetica", "bold");
doc.text("SIGNATURE VALIDE", pageWidth / 2, 40, { align: "center" });
// Titre principal
doc.setTextColor(...textColor);
doc.setFontSize(20);
doc.setFont("helvetica", "bold");
doc.text("Preuve d'Authenticité et d'Intégrité", margin, 62);
// Zone d'information document - Version professionnelle
let y = 72;
doc.setFillColor(...lightGray);
doc.setDrawColor(...borderGray);
doc.setLineWidth(0.5);
doc.roundedRect(margin, y, pageWidth - 2 * margin, 50, 3, 3, "FD");
doc.setFontSize(9);
doc.setFont("helvetica", "bold");
doc.setTextColor(...primaryColor);
doc.text("INFORMATIONS DU DOCUMENT", margin + 5, y + 8);
doc.setFont("helvetica", "normal");
doc.setTextColor(...textColor);
doc.setFontSize(8.5);
doc.text(`Document :`, margin + 5, y + 17);
doc.setFont("helvetica", "bold");
doc.text(data.document_name, margin + 28, y + 17);
doc.setFont("helvetica", "normal");
doc.text(`Signé le :`, margin + 5, y + 25);
doc.setFont("helvetica", "bold");
doc.text(
new Date(data.signed_at).toLocaleString("fr-FR", {
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}),
margin + 28,
y + 25
);
doc.setFont("helvetica", "normal");
doc.text(`Signataire :`, margin + 5, y + 33);
doc.setFont("helvetica", "bold");
doc.text(data.signer_name, margin + 28, y + 33);
doc.setFont("helvetica", "normal");
doc.text(`Email :`, margin + 5, y + 41);
doc.setFont("helvetica", "bold");
doc.text(data.signer_email, margin + 28, y + 41);
// QR Code (centré et encadré)
y = 132;
const qrSize = 70;
const qrX = (pageWidth - qrSize) / 2;
// Fond et bordure pour le QR code
doc.setFillColor(255, 255, 255);
doc.setDrawColor(...borderGray);
doc.setLineWidth(1);
doc.roundedRect(qrX - 5, y - 5, qrSize + 10, qrSize + 10, 3, 3, "FD");
doc.addImage(data.qr_code_data_url, "PNG", qrX, y, qrSize, qrSize);
doc.setFontSize(11);
doc.setFont("helvetica", "bold");
doc.setTextColor(...primaryColor);
doc.text("Vérifiez cette signature", pageWidth / 2, y + qrSize + 12, { align: "center" });
doc.setFontSize(8);
doc.setFont("helvetica", "normal");
doc.setTextColor(...textColor);
doc.text("Scannez ce QR code ou visitez l'URL ci-dessous", pageWidth / 2, y + qrSize + 18, { align: "center" });
doc.setTextColor(...primaryColor);
doc.setFont("helvetica", "bold");
doc.setFontSize(7);
// Tronquer l'URL si trop longue
const urlDisplay = data.verification_url.length > 65
? data.verification_url.substring(0, 62) + "..."
: data.verification_url;
doc.text(urlDisplay, pageWidth / 2, y + qrSize + 23, { align: "center" });
// Détails techniques - Version professionnelle
y = 225;
doc.setFontSize(13);
doc.setFont("helvetica", "bold");
doc.setTextColor(...textColor);
doc.text("Contrôles de Conformité", margin, y);
y += 8;
doc.setFontSize(8);
doc.setFont("helvetica", "normal");
// Grille de validation
const checks = [
"Format : PAdES-BASELINE-B (ETSI EN 319 102-1)",
"Algorithme : RSASSA-PSS avec SHA-256",
"Chiffrement : RSA 2048 bits",
"Intégrité : Document non modifié"
];
checks.forEach((check, index) => {
doc.setFillColor(240, 253, 244); // green-50
doc.setDrawColor(187, 247, 208); // green-200
doc.setLineWidth(0.3);
doc.roundedRect(margin, y + (index * 8), pageWidth - 2 * margin, 6, 1, 1, "FD");
doc.setTextColor(21, 128, 61); // green-700
doc.text("✓", margin + 3, y + (index * 8) + 4);
doc.setTextColor(...textColor);
doc.text(check, margin + 8, y + (index * 8) + 4);
});
y += 40;
// Empreinte cryptographique
doc.setFont("helvetica", "bold");
doc.setFontSize(9);
doc.text("Empreinte cryptographique SHA-256 :", margin, y);
y += 5;
doc.setFont("courier", "normal");
doc.setFontSize(7);
doc.setTextColor(71, 85, 105); // slate-600
const hashLines = doc.splitTextToSize(data.signature_hash, pageWidth - 2 * margin);
doc.text(hashLines, margin, y);
y += 10;
// Certificat
doc.setFont("helvetica", "bold");
doc.setFontSize(9);
doc.setTextColor(...textColor);
doc.text("Certificat de signature :", margin, y);
y += 5;
doc.setFont("helvetica", "normal");
doc.setFontSize(7.5);
doc.text(`Émetteur : ${data.certificate_info.issuer}`, margin + 3, y);
y += 4;
doc.text(`Sujet : ${data.certificate_info.subject}`, margin + 3, y);
y += 4;
doc.text(
`Validité : du ${new Date(data.certificate_info.valid_from).toLocaleDateString("fr-FR")} au ${new Date(
data.certificate_info.valid_until
).toLocaleDateString("fr-FR")}`,
margin + 3,
y
);
y += 4;
doc.text(`N° de série : ${data.certificate_info.serial_number}`, margin + 3, y);
// Note informative professionnelle
y = pageHeight - 35;
doc.setFillColor(241, 245, 249); // slate-100
doc.setDrawColor(...borderGray);
doc.setLineWidth(0.5);
doc.roundedRect(margin, y, pageWidth - 2 * margin, 18, 2, 2, "FD");
doc.setFontSize(7);
doc.setFont("helvetica", "bold");
doc.setTextColor(...textColor);
doc.text("À PROPOS DE CETTE SIGNATURE", margin + 3, y + 5);
doc.setFont("helvetica", "normal");
doc.setTextColor(71, 85, 105); // slate-600
const noteText = `Cette signature électronique est conforme aux standards techniques PAdES-BASELINE-B (ETSI EN 319 102-1), garantissant l'authenticité du signataire et l'intégrité du document. Le système utilise un chiffrement RSA 2048 bits avec SHA-256, offrant un niveau de sécurité élevé conforme aux recommandations de l'ANSSI.`;
const splitNote = doc.splitTextToSize(noteText, pageWidth - 2 * margin - 6);
doc.text(splitNote, margin + 3, y + 10);
// Footer
doc.setFontSize(8);
doc.setTextColor(100, 116, 139); // Slate-500
doc.setFont("helvetica", "normal");
doc.text(
`Odentas Media SAS - Signature électronique sécurisée`,
pageWidth / 2,
pageHeight - 10,
{ align: "center" }
);
doc.setFontSize(7);
doc.text(
`Document généré le ${new Date().toLocaleString("fr-FR")}`,
pageWidth / 2,
pageHeight - 5,
{ align: "center" }
);
return doc.output("blob");
}