espace-paie-odentas/ODENTAS_SIGN_API.md
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

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éée
  • otp_sent : OTP envoyé
  • otp_verified : OTP vérifié avec succès
  • otp_verification_failed : OTP incorrect
  • otp_max_attempts_exceeded : Trop de tentatives
  • signed : Signature enregistrée
  • all_signed : Tous les signataires ont signé
  • sealing_started : Début du scellage
  • request_completed : Demande complétée
  • request_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