- 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)
218 lines
6.5 KiB
JavaScript
Executable file
218 lines
6.5 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
||
|
||
/**
|
||
* Script simple pour créer une demande depuis un PDF local
|
||
* Utilise AWS SDK comme test-odentas-sign.js
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||
|
||
const API_BASE = process.env.API_BASE || 'http://localhost:3000/api/odentas-sign';
|
||
|
||
// S3 Client (utilise les credentials du .env)
|
||
const s3Client = new S3Client({
|
||
region: process.env.AWS_REGION || 'eu-west-3',
|
||
});
|
||
|
||
const BUCKET = 'odentas-sign';
|
||
|
||
/**
|
||
* Charge les templates de signature disponibles
|
||
*/
|
||
function loadTemplates() {
|
||
const templatesDir = path.join(__dirname, 'signature-templates');
|
||
const templates = [];
|
||
|
||
if (!fs.existsSync(templatesDir)) {
|
||
return templates;
|
||
}
|
||
|
||
const files = fs.readdirSync(templatesDir);
|
||
for (const file of files) {
|
||
if (file.endsWith('.json')) {
|
||
try {
|
||
const content = fs.readFileSync(path.join(templatesDir, file), 'utf-8');
|
||
templates.push(JSON.parse(content));
|
||
} catch (error) {
|
||
console.warn(`⚠️ Impossible de charger le template ${file}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
return templates;
|
||
}
|
||
|
||
/**
|
||
* Détecte le template approprié pour un PDF
|
||
*/
|
||
function detectTemplate(filename, templates) {
|
||
for (const template of templates) {
|
||
const pattern = new RegExp(template.pdfPattern, 'i');
|
||
if (pattern.test(filename)) {
|
||
return template;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Convertit les positions du template en format API
|
||
*/
|
||
function templateToPositions(template) {
|
||
const positions = {};
|
||
template.positions.forEach(p => {
|
||
positions[p.role] = {
|
||
page: p.page,
|
||
x: p.x,
|
||
y: p.y,
|
||
width: p.width,
|
||
height: p.height,
|
||
};
|
||
});
|
||
return positions;
|
||
}
|
||
|
||
async function main() {
|
||
const args = process.argv.slice(2);
|
||
|
||
if (args.length === 0) {
|
||
console.error('Usage: node create-real-signature.js <chemin-pdf>');
|
||
console.error('Exemple: node create-real-signature.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 réel');
|
||
console.log('═══════════════════════════════════════════════════════\n');
|
||
|
||
// Lire le PDF
|
||
const dataBuffer = fs.readFileSync(pdfPath);
|
||
const pdfSize = Math.round(dataBuffer.length / 1024);
|
||
const filename = path.basename(pdfPath);
|
||
|
||
console.log(`📖 PDF: ${filename}`);
|
||
console.log(` Taille: ${pdfSize} KB\n`);
|
||
|
||
// Charger les templates
|
||
console.log('🔍 Détection du template...');
|
||
const templates = loadTemplates();
|
||
console.log(` ${templates.length} template(s) disponible(s)`);
|
||
|
||
const template = detectTemplate(filename, templates);
|
||
|
||
let positions;
|
||
|
||
if (template) {
|
||
console.log(` ✅ Template détecté: ${template.templateName}`);
|
||
console.log(` 📝 ${template.description}`);
|
||
positions = templateToPositions(template);
|
||
} else {
|
||
console.log(' ⚠️ Aucun template trouvé, utilisation des positions par défaut');
|
||
// Positions par défaut (bas de page, centrées)
|
||
positions = {
|
||
'Employeur': { page: 1, x: 70, y: 120, width: 180, height: 70 },
|
||
'Salarié': { page: 1, x: 350, y: 120, width: 180, height: 70 },
|
||
};
|
||
}
|
||
|
||
console.log('\n📍 Positions de signature:');
|
||
Object.entries(positions).forEach(([role, pos]) => {
|
||
console.log(` ${role}: page ${pos.page}, (${pos.x}, ${pos.y}), ${pos.width}x${pos.height}px`);
|
||
});
|
||
|
||
// Upload 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: BUCKET,
|
||
Key: s3Key,
|
||
Body: dataBuffer,
|
||
ContentType: 'application/pdf',
|
||
Metadata: {
|
||
original_filename: filename,
|
||
ref: ref,
|
||
},
|
||
}));
|
||
|
||
console.log(` ✅ Uploadé: s3://${BUCKET}/${s3Key}`);
|
||
|
||
// Créer la demande
|
||
console.log('\n✍️ Création de la demande...');
|
||
|
||
const signatureRequest = {
|
||
contractId: `CDDU-${Date.now()}`,
|
||
contractRef: ref,
|
||
pdfS3Key: s3Key,
|
||
title: `Contrat CDDU - ${filename.replace('.pdf', '')}`,
|
||
signers: [
|
||
{
|
||
name: 'Odentas Paie',
|
||
email: 'paie@odentas.fr',
|
||
role: 'Employeur',
|
||
positions: [positions['Employeur']],
|
||
},
|
||
{
|
||
name: 'Renaud Breviere',
|
||
email: 'renaud.breviere@gmail.com',
|
||
role: 'Salarié',
|
||
positions: [positions['Salarié']],
|
||
},
|
||
],
|
||
};
|
||
|
||
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('\n❌ Erreur:', error);
|
||
process.exit(1);
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
// 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 (affiché dans les logs)');
|
||
console.log(' 3. Dessiner et valider la signature');
|
||
console.log(' 4. Répéter pour le 2ème signataire\n');
|
||
}
|
||
|
||
main().catch(console.error);
|