- 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)
496 lines
12 KiB
Markdown
496 lines
12 KiB
Markdown
# 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`
|
|
|
|
```typescript
|
|
// 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]`
|
|
|
|
```typescript
|
|
// 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`
|
|
|
|
```typescript
|
|
// 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`
|
|
|
|
```typescript
|
|
// 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>`
|
|
|
|
```typescript
|
|
// 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>`
|
|
|
|
```typescript
|
|
// 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`
|
|
|
|
```typescript
|
|
// 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.
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```env
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|