- 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)
219 lines
6.4 KiB
TypeScript
219 lines
6.4 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { supabaseAdmin, logSignEvent, checkAllSignersSigned } from '@/lib/odentas-sign/supabase';
|
|
import { verifySignatureSession, extractTokenFromHeader } from '@/lib/odentas-sign/jwt';
|
|
import { uploadSignatureImage } from '@/lib/odentas-sign/s3';
|
|
|
|
/**
|
|
* POST /api/odentas-sign/signers/[id]/sign
|
|
*
|
|
* Enregistre la signature d'un signataire
|
|
*/
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: { id: string } }
|
|
) {
|
|
try {
|
|
const signerId = params.id;
|
|
const body = await request.json();
|
|
|
|
// Vérifier le JWT de session
|
|
const authHeader = request.headers.get('authorization');
|
|
const token = extractTokenFromHeader(authHeader);
|
|
|
|
if (!token) {
|
|
return NextResponse.json(
|
|
{ error: 'Token de session manquant' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
const session = verifySignatureSession(token);
|
|
|
|
if (!session || session.signerId !== signerId) {
|
|
return NextResponse.json(
|
|
{ error: 'Session invalide ou expirée' },
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
// Validation des données
|
|
const { signatureImageBase64, consentText } = body;
|
|
|
|
if (!signatureImageBase64) {
|
|
return NextResponse.json(
|
|
{ error: 'Image de signature manquante' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (!consentText) {
|
|
return NextResponse.json(
|
|
{ error: 'Consentement manquant' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Récupérer le signataire
|
|
const { data: signer, error: signerError } = await supabaseAdmin
|
|
.from('signers')
|
|
.select('*, sign_requests(id, ref, title, status)')
|
|
.eq('id', signerId)
|
|
.single();
|
|
|
|
if (signerError || !signer) {
|
|
return NextResponse.json(
|
|
{ error: 'Signataire introuvable' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// Vérifier que la demande est active
|
|
if (signer.sign_requests.status !== 'pending' && signer.sign_requests.status !== 'in_progress') {
|
|
return NextResponse.json(
|
|
{ error: 'Cette demande de signature n\'est plus active' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Vérifier si déjà signé
|
|
if (signer.signed_at) {
|
|
return NextResponse.json(
|
|
{ error: 'Vous avez déjà signé ce document' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
console.log(`[SIGN] Enregistrement signature pour ${signer.email}...`);
|
|
|
|
// Extraire l'IP et le User-Agent
|
|
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null;
|
|
const userAgent = request.headers.get('user-agent') || null;
|
|
|
|
// 1. Upload de l'image de signature vers S3
|
|
let signatureS3Key: string;
|
|
try {
|
|
signatureS3Key = await uploadSignatureImage({
|
|
requestId: signer.sign_requests.id,
|
|
signerId: signer.id,
|
|
imageBase64: signatureImageBase64,
|
|
});
|
|
|
|
console.log(`[SIGN] ✅ Image uploadée: ${signatureS3Key}`);
|
|
} catch (uploadError) {
|
|
console.error('[SIGN] Erreur upload signature:', uploadError);
|
|
return NextResponse.json(
|
|
{ error: 'Erreur lors de l\'upload de la signature' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
// 2. Mettre à jour le signataire dans la base de données
|
|
const now = new Date().toISOString();
|
|
|
|
const { error: updateError } = await supabaseAdmin
|
|
.from('signers')
|
|
.update({
|
|
signature_image_s3: signatureS3Key,
|
|
signed_at: now,
|
|
ip_signed: ipAddress,
|
|
user_agent: userAgent,
|
|
consent_text: consentText,
|
|
consent_at: now,
|
|
})
|
|
.eq('id', signerId);
|
|
|
|
if (updateError) {
|
|
console.error('[SIGN] Erreur mise à jour signataire:', updateError);
|
|
return NextResponse.json(
|
|
{ error: 'Erreur lors de l\'enregistrement de la signature' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
console.log(`[SIGN] ✅ Signataire mis à jour`);
|
|
|
|
// 3. Logger l'événement de signature
|
|
await logSignEvent({
|
|
requestId: signer.sign_requests.id,
|
|
signerId: signer.id,
|
|
event: 'signed',
|
|
ip: ipAddress || undefined,
|
|
userAgent: userAgent || undefined,
|
|
metadata: {
|
|
signature_s3_key: signatureS3Key,
|
|
consent_text: consentText,
|
|
},
|
|
});
|
|
|
|
// 4. Vérifier si tous les signataires ont signé
|
|
const allSigned = await checkAllSignersSigned(signer.sign_requests.id);
|
|
|
|
if (allSigned) {
|
|
console.log(`[SIGN] 🎉 Tous les signataires ont signé ! Déclenchement du workflow de scellage...`);
|
|
|
|
// Logger l'événement de completion
|
|
await logSignEvent({
|
|
requestId: signer.sign_requests.id,
|
|
event: 'all_signed',
|
|
metadata: {
|
|
trigger: 'auto',
|
|
},
|
|
});
|
|
|
|
// Déclencher le workflow de scellage (via webhook ou Lambda)
|
|
try {
|
|
await triggerSealingWorkflow(signer.sign_requests.id);
|
|
} catch (workflowError) {
|
|
console.error('[SIGN] Erreur déclenchement workflow:', workflowError);
|
|
// On ne fait pas échouer la requête, le workflow peut être retenté
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: 'Signature enregistrée avec succès',
|
|
signed_at: now,
|
|
all_signed: allSigned,
|
|
signer: {
|
|
id: signer.id,
|
|
name: signer.name,
|
|
email: signer.email,
|
|
role: signer.role,
|
|
},
|
|
request: {
|
|
id: signer.sign_requests.id,
|
|
ref: signer.sign_requests.ref,
|
|
title: signer.sign_requests.title,
|
|
},
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('[SIGN] Erreur:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Erreur serveur', details: error instanceof Error ? error.message : String(error) },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Déclenche le workflow de scellage quand tous ont signé
|
|
*/
|
|
async function triggerSealingWorkflow(requestId: string): Promise<void> {
|
|
console.log(`[WORKFLOW] Déclenchement du scellage pour request ${requestId}`);
|
|
|
|
// Appeler le webhook de completion
|
|
const webhookUrl = `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/odentas-sign/webhooks/completion`;
|
|
|
|
const response = await fetch(webhookUrl, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ requestId }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Webhook failed: ${response.status}`);
|
|
}
|
|
|
|
console.log(`[WORKFLOW] ✅ Webhook appelé avec succès`);
|
|
}
|