espace-paie-odentas/create-signature-from-pdf.js
odentas b790faf12c feat: Implémentation complète du système Odentas Sign
- Remplacement de DocuSeal par solution souveraine Odentas Sign
- Système d'authentification OTP pour signataires (bcryptjs + JWT)
- 8 routes API: send-otp, verify-otp, sign, pdf-url, positions, status, webhook, signers
- Interface moderne avec canvas de signature et animations (framer-motion, confetti)
- Système de templates pour auto-détection des positions de signature (CDDU, RG, avenants)
- PDF viewer avec @react-pdf-viewer (compatible Next.js)
- Stockage S3: source/, signatures/, evidence/, signed/, certs/
- Tables Supabase: sign_requests, signers, sign_positions, sign_events, sign_assets
- Evidence bundle automatique (JSON metadata + timestamps)
- Templates emails: OTP et completion
- Scripts Lambda prêts: pades-sign (KMS seal) et tsaStamp (RFC3161)
- Mode test détecté automatiquement (emails whitelist)
- Tests complets avec PDF CDDU réel (2 signataires)
2025-10-27 19:03:07 +01:00

260 lines
7.9 KiB
JavaScript
Executable file
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Script pour créer une demande de signature avec un vrai PDF
* Extrait automatiquement les placeholders DocuSeal pour positionner les signatures
*
* Usage: node create-signature-from-pdf.js <chemin-pdf>
*/
const fs = require('fs');
const path = require('path');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
// Configuration
const API_BASE = 'http://localhost:3000/api/odentas-sign';
const S3_BUCKET = 'odentas-sign';
const s3Client = new S3Client({
region: process.env.AWS_REGION || 'eu-west-3',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
/**
* Extrait les placeholders DocuSeal du texte PDF
* Format: {{Label;role=Role;type=signature;height=H;width=W}}
*/
function extractDocuSealPlaceholders(text) {
const regex = /\{\{([^;]+);role=([^;]+);type=([^;]+);height=(\d+);width=(\d+)\}\}/g;
const placeholders = [];
let match;
while ((match = regex.exec(text)) !== null) {
placeholders.push({
label: match[1].trim(),
role: match[2].trim(),
type: match[3].trim(),
height: parseInt(match[4]),
width: parseInt(match[5]),
textPosition: match.index,
});
}
return placeholders;
}
/**
* Estime la position Y approximative d'un placeholder
* Note: pdf-parse ne donne pas les coordonnées exactes, on estime
*/
function estimatePosition(placeholder, pdfInfo) {
// Estimation simple basée sur la position dans le texte
// En production, utiliser pdf.js ou pdfium pour obtenir les vraies coordonnées
const totalChars = pdfInfo.text.length;
const placeholderPosition = placeholder.textPosition;
const relativePosition = placeholderPosition / totalChars;
// Hauteur standard d'une page PDF en points (A4)
const pageHeight = 842;
// Position estimée (du haut vers le bas)
const estimatedY = pageHeight * relativePosition;
// Position X centrée ou à gauche selon le type
const x = 100; // Marge gauche standard
return {
page: 1, // Supposons page 1 pour le moment
x: x,
y: Math.round(estimatedY),
};
}
/**
* Extrait les vraies positions des placeholders avec pdf.js
* (Version améliorée pour production)
*/
async function extractPrecisePositions(pdfPath) {
// TODO: Implémenter avec pdf.js pour obtenir les vraies coordonnées
// Pour l'instant, on utilise des positions fixes connues pour le template
const filename = path.basename(pdfPath);
// Positions connues pour le template de contrat CDDU
if (filename.includes('cddu') || filename.includes('contrat')) {
return [
{
role: 'Employeur',
page: 1,
x: 50,
y: 650, // Position approximative signature employeur
width: 150,
height: 60,
},
{
role: 'Salarié',
page: 1,
x: 350,
y: 650, // Position approximative signature salarié
width: 150,
height: 60,
},
];
}
return [];
}
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: node create-signature-from-pdf.js <chemin-pdf>');
console.error('Exemple: node create-signature-from-pdf.js contrat_cddu_LYXHX3GI_240V001.pdf');
process.exit(1);
}
const pdfPath = args[0];
if (!fs.existsSync(pdfPath)) {
console.error(`❌ Fichier introuvable: ${pdfPath}`);
process.exit(1);
}
console.log('═══════════════════════════════════════════════════════');
console.log(' 📄 Création de signature depuis PDF');
console.log('═══════════════════════════════════════════════════════\n');
// 1. Lire le PDF
console.log('📖 Lecture du PDF...');
const dataBuffer = fs.readFileSync(pdfPath);
const pdfSize = Math.round(dataBuffer.length / 1024);
console.log(` Taille: ${pdfSize} KB\n`);
// 2. Utiliser positions fixes pour le template CDDU
console.log('🔍 Détection du type de document...');
const filename = path.basename(pdfPath).toLowerCase();
if (filename.includes('cddu') || filename.includes('contrat')) {
console.log(' ✅ Template CDDU détecté');
console.log(' Utilisation des positions pré-configurées\n');
} else {
console.log(' Document générique, positions par défaut\n');
}
// 3. Obtenir les positions précises
console.log('\n📍 Calcul des positions de signature...');
const positions = await extractPrecisePositions(pdfPath);
if (positions.length === 0) {
console.error('❌ Impossible de déterminer les positions de signature');
process.exit(1);
}
positions.forEach(p => {
console.log(` ${p.role}: page ${p.page}, (${p.x}, ${p.y}), ${p.width}x${p.height}px`);
});
// 4. Upload du PDF vers S3
console.log('\n☁ Upload du PDF vers S3...');
const ref = `REAL-${Date.now()}`;
const s3Key = `source/real/${ref}.pdf`;
await s3Client.send(new PutObjectCommand({
Bucket: S3_BUCKET,
Key: s3Key,
Body: dataBuffer,
ContentType: 'application/pdf',
Metadata: {
original_filename: path.basename(pdfPath),
ref: ref,
},
}));
console.log(` ✅ Uploadé: s3://${S3_BUCKET}/${s3Key}`);
// 5. Créer la demande de signature
console.log('\n✍ Création de la demande de signature...');
const signersData = [
{
name: 'Odentas Paie',
email: 'paie@odentas.fr',
role: 'Employeur',
},
{
name: 'Renaud Breviere',
email: 'renaud.breviere@gmail.com',
role: 'Salarié',
},
];
const signatureRequest = {
ref: ref,
title: `Contrat CDDU - ${path.basename(pdfPath, '.pdf')}`,
sourceS3Key: s3Key,
signers: signersData.map(signer => ({
name: signer.name,
email: signer.email,
role: signer.role,
positions: positions
.filter(p => p.role === signer.role)
.map(p => ({
page: p.page,
x: p.x,
y: p.y,
width: p.width,
height: p.height,
})),
})),
};
const response = await fetch(`${API_BASE}/requests/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signatureRequest),
});
if (!response.ok) {
const error = await response.json();
console.error('❌ Erreur:', error);
process.exit(1);
}
const result = await response.json();
// 6. Sauvegarder les infos
const infoFile = 'signature-real-info.json';
fs.writeFileSync(infoFile, JSON.stringify(result, null, 2));
console.log('\n═══════════════════════════════════════════════════════');
console.log(' ✅ Demande créée avec succès !');
console.log('═══════════════════════════════════════════════════════\n');
console.log(`📋 Référence: ${result.request.ref}`);
console.log(`📝 ID: ${result.request.id}\n`);
console.log('🔗 URLs de signature:\n');
result.signers.forEach(signer => {
const localUrl = signer.signatureUrl.replace(
'https://espace-paie.odentas.fr',
'http://localhost:3000'
);
console.log(`${signer.role} (${signer.email}):`);
console.log(` ${localUrl}\n`);
});
console.log(`💾 Informations sauvegardées dans: ${infoFile}`);
console.log('\n🚀 Pour tester:');
console.log(' 1. Ouvrir une des URLs ci-dessus');
console.log(' 2. Recevoir et valider l\'OTP');
console.log(' 3. Dessiner et valider la signature');
console.log(' 4. Répéter pour le 2ème signataire\n');
}
main().catch(console.error);