- 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)
12 KiB
Odentas Sign - API Documentation
Système de signature électronique souverain conforme eIDAS
🎯 Vue d'ensemble
Odentas Sign remplace DocuSeal par une solution totalement interne et souveraine pour la signature des contrats de travail. Le système comprend :
- Odentas Sign : Interface de signature avec authentification OTP
- Odentas Seal : Scellage PAdES avec KMS (lambda-odentas-pades-sign)
- Odentas TSA : Horodatage RFC3161 (lambda-tsaStamp)
- Odentas Archive : Stockage S3 avec Object Lock 10 ans
📁 Structure du bucket S3 odentas-sign
odentas-sign/
├── source/ # PDFs non signés
├── signed/ # PDFs signés et scellés
├── evidence/ # Bundles de preuves (JSON)
├── signatures/ # Images de signatures
└── certs/ # Certificats de scellage
🔐 Architecture des tables Supabase
sign_requests
Demandes de signature électronique
| Colonne | Type | Description |
|---|---|---|
id |
uuid | Identifiant unique |
ref |
text | Référence du contrat (ex: CDDU-2025-0102) |
title |
text | Titre du document |
source_s3_key |
text | Clé S3 du PDF source |
status |
text | pending, in_progress, completed, cancelled |
created_at |
timestamp | Date de création |
signers
Signataires (Employeur + Salarié)
| Colonne | Type | Description |
|---|---|---|
id |
uuid | Identifiant unique |
request_id |
uuid | Référence à sign_requests |
role |
text | Employeur ou Salarié |
name |
text | Nom du signataire |
email |
text | Email pour OTP |
otp_hash |
text | Hash bcrypt du code OTP |
otp_expires_at |
timestamp | Expiration du code (15 min) |
otp_attempts |
integer | Nombre de tentatives (max 3) |
otp_last_sent_at |
timestamp | Dernière date d'envoi OTP |
signed_at |
timestamp | Date de signature |
signature_image_s3 |
text | Clé S3 de l'image de signature |
consent_text |
text | Texte de consentement accepté |
consent_at |
timestamp | Date du consentement |
ip_signed |
inet | Adresse IP lors de la signature |
user_agent |
text | User-Agent du navigateur |
sign_positions
Positions des champs de signature dans le PDF
| Colonne | Type | Description |
|---|---|---|
id |
uuid | Identifiant unique |
request_id |
uuid | Référence à sign_requests |
role |
text | Employeur ou Salarié |
page |
integer | Numéro de page (1-indexed) |
x, y, w, h |
numeric | Position et dimensions |
kind |
text | signature, text, date, checkbox |
label |
text | Label optionnel |
sign_events
Logs d'audit complets
| Colonne | Type | Description |
|---|---|---|
id |
bigserial | Identifiant auto-incrémenté |
request_id |
uuid | Référence à sign_requests |
signer_id |
uuid | Référence à signers (optionnel) |
ts |
timestamp | Horodatage de l'événement |
event |
text | Type d'événement (voir ci-dessous) |
ip |
text | Adresse IP |
user_agent |
text | User-Agent |
metadata |
jsonb | Données additionnelles |
Types d'événements :
request_created: Demande crééeotp_sent: OTP envoyéotp_verified: OTP vérifié avec succèsotp_verification_failed: OTP incorrectotp_max_attempts_exceeded: Trop de tentativessigned: Signature enregistréeall_signed: Tous les signataires ont signésealing_started: Début du scellagerequest_completed: Demande complétéerequest_cancelled: Demande annulée
sign_assets
Assets finaux (PDF signé, preuves, horodatage)
| Colonne | Type | Description |
|---|---|---|
request_id |
uuid | Référence à sign_requests |
signed_pdf_s3_key |
text | Clé S3 du PDF signé et scellé |
evidence_json_s3_key |
text | Clé S3 du bundle de preuves |
tsa_tsr_s3_key |
text | Clé S3 de la réponse TSA |
pdf_sha256 |
text | Hash SHA-256 du PDF |
tsa_token_sha256 |
text | Hash SHA-256 du token TSA |
sealed_at |
timestamp | Date du scellage |
seal_algo |
text | Algorithme utilisé (RSASSA_PSS_SHA_256) |
seal_kms_key_id |
text | ID de la clé KMS |
tsa_policy_oid |
text | OID de la politique TSA |
tsa_serial |
text | Numéro de série TSA |
retain_until |
timestamp | Date de fin de rétention (10 ans) |
🔌 Endpoints API
1. Créer une demande de signature
POST /api/odentas-sign/requests/create
// Request
{
contractId: string; // ID du contrat dans cddu_contracts
contractRef: string; // Numéro du contrat (ex: CDDU-2025-0102)
pdfS3Key: string; // Clé S3 du PDF source
title: string; // Titre du document
signers: [
{
role: 'Employeur' | 'Salarié';
name: string;
email: string;
}
];
positions: [ // Optionnel
{
role: 'Employeur' | 'Salarié';
page: number; // Numéro de page (1-indexed)
x: number; // Position X (en points)
y: number; // Position Y (en points)
w: number; // Largeur
h: number; // Hauteur
kind?: 'signature'; // Type de champ
label?: string; // Label optionnel
}
];
}
// Response 201
{
success: true;
request: {
id: string;
ref: string;
title: string;
status: 'pending';
created_at: string;
};
signers: [
{
signerId: string;
role: string;
name: string;
email: string;
signatureUrl: string; // URL pour ce signataire
}
];
}
2. Récupérer les détails d'une demande
GET /api/odentas-sign/requests/[id]
// Response 200
{
success: true;
request: {
id: string;
ref: string;
title: string;
source_s3_key: string;
status: string;
created_at: string;
progress: {
total: number;
signed: number;
percentage: number;
};
};
signers: Array<{
id: string;
role: string;
name: string;
email: string;
signed_at: string | null;
signature_image_s3: string | null;
}>;
positions: Array<Position>;
}
3. Envoyer un code OTP
POST /api/odentas-sign/signers/[id]/send-otp
// Response 200
{
success: true;
message: 'Code de vérification envoyé par email';
expires_at: string;
signer: {
name: string;
email: string;
};
}
// Response 429 (trop rapide)
{
error: 'Veuillez attendre X secondes avant de redemander un code';
}
4. Vérifier le code OTP
POST /api/odentas-sign/signers/[id]/verify-otp
// Request
{
otp: string; // Code à 6 chiffres
}
// Response 200
{
success: true;
message: 'Code vérifié avec succès';
sessionToken: string; // JWT valide 30 minutes
signer: {
id: string;
name: string;
email: string;
role: string;
};
request: {
id: string;
ref: string;
title: string;
};
}
// Response 401 (code incorrect)
{
error: 'Code incorrect. X tentative(s) restante(s).';
remainingAttempts: number;
}
5. Enregistrer la signature
POST /api/odentas-sign/signers/[id]/sign
Headers : Authorization: Bearer <sessionToken>
// Request
{
signatureImageBase64: string; // data:image/png;base64,xxx
consentText: string; // Texte de consentement accepté
}
// Response 200
{
success: true;
message: 'Signature enregistrée avec succès';
signed_at: string;
all_signed: boolean; // true si tous ont signé
signer: {
id: string;
name: string;
email: string;
role: string;
};
request: {
id: string;
ref: string;
title: string;
};
}
6. Vérifier le statut d'un signataire
GET /api/odentas-sign/signers/[id]/status
Headers (optionnel) : Authorization: Bearer <sessionToken>
// Response 200
{
success: true;
signer: {
id: string;
role: string;
name: string;
email: string;
has_signed: boolean;
signed_at: string | null;
};
request: {
id: string;
ref: string;
title: string;
status: string;
created_at: string;
progress: {
total: number;
signed: number;
percentage: number;
};
};
other_signers: Array<{
role: string;
name: string;
has_signed: boolean;
}>;
}
7. Annuler une demande
POST /api/odentas-sign/requests/[id]/cancel
// Request
{
reason?: string; // Raison de l'annulation
}
// Response 200
{
success: true;
message: 'Demande annulée avec succès';
request: {
id: string;
ref: string;
status: 'cancelled';
};
}
8. Webhook de completion (interne)
POST /api/odentas-sign/webhooks/completion
Appelé automatiquement quand tous les signataires ont signé. Lance le workflow de scellage PAdES + TSA + Archive.
// Request
{
requestId: string;
}
// Response 200
{
success: true;
message: 'Workflow de scellage lancé';
request: {
id: string;
ref: string;
status: 'completed';
};
evidence_key: string; // Clé S3 du bundle de preuves
}
🔄 Workflow complet
1. Création du contrat
↓
2. Génération du PDF → Upload vers source/
↓
3. Appel API /requests/create
↓
4. Création sign_request + signers + positions
↓
5. Envoi emails avec liens de signature
↓
6. Signataire clique sur le lien
↓
7. Authentification OTP (/send-otp → /verify-otp)
↓
8. Affichage du PDF + capture signature
↓
9. Validation signature (/sign)
↓
10. Upload image vers signatures/
↓
11. Vérification si tous ont signé
↓
12. Si oui : webhook /webhooks/completion
↓
13. Lambda orchestration :
- Injection signatures visuelles dans PDF
- Scellage PAdES (lambda-odentas-pades-sign)
- Horodatage TSA (lambda-tsaStamp)
- Création evidence bundle
- Upload vers signed/ et evidence/
- Copie vers archive avec Object Lock 10 ans
↓
14. Mise à jour sign_assets
↓
15. Emails de confirmation
🔒 Conformité eIDAS
Niveau actuel : SES (Signature Électronique Simple)
✅ Implémenté :
- Authentification par OTP (email vérifié)
- Consentement explicite
- Horodatage qualifié (TSA RFC3161)
- Logs d'audit complets et immuables
- Archivage à 10 ans avec Object Lock
🔨 Pour passer à AES (Signature Électronique Avancée) :
- Ajouter certificat qualifié du signataire
- Lier la signature à un dispositif sécurisé
🔨 Pour passer à QES (Signature Électronique Qualifiée) :
- Utiliser un QSCD (Qualified Signature Creation Device)
- Intégrer avec un PSC (Prestataire de Services de Confiance) qualifié
🛠️ Variables d'environnement
# Supabase
NEXT_PUBLIC_SUPABASE_URL=xxx
SUPABASE_SERVICE_ROLE_KEY=xxx
# S3
AWS_REGION=eu-west-3
ODENTAS_SIGN_BUCKET=odentas-sign
# KMS & TSA
KMS_KEY_ID=xxx
TSA_URL=https://timestamp.sectigo.com
# JWT
JWT_SECRET=xxx # Ou utilise NEXTAUTH_SECRET
# App
NEXT_PUBLIC_APP_URL=https://espace-paie.odentas.fr
📝 TODO Phase 2
- Interface frontend de signature (/app/signer/[requestId]/[signerId])
- Lambda d'orchestration (injection signatures + PAdES + TSA)
- Template email OTP
- Templates emails de notification (completion)
- Migration depuis DocuSeal (mode parallèle)
- Dashboard admin pour suivre les signatures
- API de téléchargement du PDF final
- Webhooks pour notifier le système principal
🧪 Tests
TODO: Ajouter tests unitaires et d'intégration
# Exemple de test avec curl
curl -X POST http://localhost:3000/api/odentas-sign/requests/create \
-H "Content-Type: application/json" \
-d '{
"contractId": "xxx",
"contractRef": "CDDU-2025-0102",
"pdfS3Key": "source/contrat.pdf",
"title": "Contrat de travail CDDU",
"signers": [
{"role": "Employeur", "name": "Jean Dupont", "email": "jean@company.fr"},
{"role": "Salarié", "name": "Marie Martin", "email": "marie@email.fr"}
]
}'
Dernière mise à jour : 27 octobre 2025