espace-paie-odentas/lib/signature-proof-pdf.ts
odentas d5a110484b feat: Système de vérification de signature électronique avec QR code
- Page publique /verify/[id] affichant Odentas Seal, TSA, certificat
- API /api/signatures/create-verification pour créer preuves
- Générateur PDF de preuve avec QR code (jsPDF)
- Hook useSignatureProof() pour intégration facile
- Table Supabase signature_verifications avec RLS public
- Page de test /test-signature-verification
- Documentation complète du système

Les signataires peuvent scanner le QR code ou visiter l'URL pour vérifier l'authenticité et l'intégrité de leur document signé.
2025-10-29 09:22:01 +01:00

204 lines
6 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 = [99, 102, 241]; // Indigo
const textColor = [30, 41, 59]; // Slate-800
const lightGray = [241, 245, 249]; // Slate-100
// En-tête avec logo
doc.setFillColor(...primaryColor);
doc.rect(0, 0, pageWidth, 40, "F");
doc.setTextColor(255, 255, 255);
doc.setFontSize(24);
doc.setFont("helvetica", "bold");
doc.text("Odentas Sign", pageWidth / 2, 20, { align: "center" });
doc.setFontSize(12);
doc.setFont("helvetica", "normal");
doc.text("Preuve de Signature Électronique", pageWidth / 2, 30, { align: "center" });
// Titre principal
doc.setTextColor(...textColor);
doc.setFontSize(18);
doc.setFont("helvetica", "bold");
doc.text("Certificat de Signature Électronique", margin, 60);
// Zone d'information document
let y = 75;
doc.setFillColor(...lightGray);
doc.roundedRect(margin, y, pageWidth - 2 * margin, 45, 3, 3, "F");
doc.setFontSize(10);
doc.setFont("helvetica", "bold");
doc.setTextColor(...primaryColor);
doc.text("DOCUMENT SIGNÉ", margin + 5, y + 8);
doc.setFont("helvetica", "normal");
doc.setTextColor(...textColor);
doc.setFontSize(9);
doc.text(`Nom du document :`, margin + 5, y + 16);
doc.setFont("helvetica", "bold");
doc.text(data.document_name, margin + 40, y + 16);
doc.setFont("helvetica", "normal");
doc.text(`Date de signature :`, margin + 5, y + 23);
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 + 40,
y + 23
);
doc.setFont("helvetica", "normal");
doc.text(`Signataire :`, margin + 5, y + 30);
doc.setFont("helvetica", "bold");
doc.text(data.signer_name, margin + 40, y + 30);
doc.setFont("helvetica", "normal");
doc.text(`Email :`, margin + 5, y + 37);
doc.setFont("helvetica", "bold");
doc.text(data.signer_email, margin + 40, y + 37);
// QR Code (plus grand et centré)
y = 130;
const qrSize = 70;
const qrX = (pageWidth - qrSize) / 2;
doc.addImage(data.qr_code_data_url, "PNG", qrX, y, qrSize, qrSize);
doc.setFontSize(10);
doc.setFont("helvetica", "bold");
doc.setTextColor(...primaryColor);
doc.text("Scannez pour vérifier", pageWidth / 2, y + qrSize + 8, { align: "center" });
doc.setFontSize(8);
doc.setFont("helvetica", "normal");
doc.setTextColor(...textColor);
doc.text("Ou visitez :", pageWidth / 2, y + qrSize + 14, { align: "center" });
doc.setTextColor(...primaryColor);
doc.setFont("helvetica", "bold");
// Tronquer l'URL si trop longue
const urlDisplay = data.verification_url.length > 60
? data.verification_url.substring(0, 57) + "..."
: data.verification_url;
doc.text(urlDisplay, pageWidth / 2, y + qrSize + 19, { align: "center" });
// Détails techniques
y = 220;
doc.setFontSize(12);
doc.setFont("helvetica", "bold");
doc.setTextColor(...textColor);
doc.text("Détails Techniques", margin, y);
y += 7;
doc.setFontSize(8);
doc.setFont("helvetica", "normal");
doc.text(`Format : PAdES-BASELINE-B (ETSI TS 102 778)`, margin, y);
y += 5;
doc.text(`Algorithme : RSASSA-PSS avec SHA-256`, margin, y);
y += 5;
doc.text(`Empreinte SHA-256 :`, margin, y);
y += 4;
doc.setFont("courier", "normal");
doc.setFontSize(7);
doc.text(data.signature_hash, margin, y);
y += 8;
doc.setFont("helvetica", "bold");
doc.setFontSize(8);
doc.text("Certificat de signature :", margin, y);
y += 5;
doc.setFont("helvetica", "normal");
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 importante
y = pageHeight - 40;
doc.setFillColor(255, 243, 224); // Orange-50
doc.roundedRect(margin, y, pageWidth - 2 * margin, 20, 2, 2, "F");
doc.setFontSize(7);
doc.setFont("helvetica", "bold");
doc.setTextColor(194, 65, 12); // Orange-700
doc.text("NOTE IMPORTANTE", margin + 3, y + 5);
doc.setFont("helvetica", "normal");
doc.setTextColor(124, 45, 18); // Orange-900
const noteText = `Cette signature est techniquement conforme au standard PAdES-BASELINE-B. Le certificat utilisé est auto-signé et n'est pas reconnu par les autorités de certification européennes. Pour une validation complète avec reconnaissance légale, un certificat qualifié serait nécessaire.`;
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");
}