espace-paie-odentas/app/verify/[id]/page.tsx
odentas d5a110484b feat: Système de vérification de signature électronique avec QR code
- Page publique /verify/[id] affichant Odentas Seal, TSA, certificat
- API /api/signatures/create-verification pour créer preuves
- Générateur PDF de preuve avec QR code (jsPDF)
- Hook useSignatureProof() pour intégration facile
- Table Supabase signature_verifications avec RLS public
- Page de test /test-signature-verification
- Documentation complète du système

Les signataires peuvent scanner le QR code ou visiter l'URL pour vérifier l'authenticité et l'intégrité de leur document signé.
2025-10-29 09:22:01 +01:00

326 lines
14 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";
import { CheckCircle2, XCircle, Shield, Clock, FileText, Download, AlertCircle } from "lucide-react";
interface SignatureData {
id: string;
document_name: string;
signed_at: string;
signer_name: string;
signer_email: string;
signature_hash: string;
pdf_url: string;
certificate_info: {
issuer: string;
subject: string;
valid_from: string;
valid_until: string;
serial_number: string;
};
timestamp: {
tsa_url: string;
timestamp: string;
hash: string;
};
verification_status: {
seal_valid: boolean;
timestamp_valid: boolean;
document_intact: boolean;
};
}
export default function VerifySignaturePage() {
const params = useParams();
const id = params?.id as string;
const [data, setData] = useState<SignatureData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const supabase = createClientComponentClient();
useEffect(() => {
if (!id) return;
async function fetchSignature() {
try {
// Récupérer les données de signature depuis Supabase
const { data: signature, error: fetchError } = await supabase
.from("signature_verifications")
.select("*")
.eq("id", id)
.single();
if (fetchError) throw fetchError;
setData(signature);
} catch (err) {
console.error("Erreur:", err);
setError("Signature non trouvée ou invalide");
} finally {
setLoading(false);
}
}
fetchSignature();
}, [id, supabase]);
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-indigo-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-slate-600">Vérification en cours...</p>
</div>
</div>
);
}
if (error || !data) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center px-4">
<div className="bg-white rounded-2xl shadow-xl p-8 max-w-md w-full text-center">
<XCircle className="w-20 h-20 text-red-500 mx-auto mb-4" />
<h1 className="text-2xl font-bold text-slate-900 mb-2">Signature introuvable</h1>
<p className="text-slate-600">{error || "Cette signature n'existe pas ou a expiré."}</p>
</div>
</div>
);
}
const allValid = data.verification_status.seal_valid &&
data.verification_status.timestamp_valid &&
data.verification_status.document_intact;
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 py-12 px-4">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center gap-2 bg-white rounded-full px-6 py-2 shadow-md mb-4">
<Shield className="w-5 h-5 text-indigo-600" />
<span className="font-semibold text-slate-900">Odentas Sign</span>
</div>
<h1 className="text-4xl font-bold text-slate-900 mb-2">Vérification de Signature</h1>
<p className="text-slate-600">Certificat de signature électronique</p>
</div>
{/* Statut global */}
<div className={`rounded-2xl p-8 mb-8 ${
allValid
? "bg-gradient-to-br from-green-50 to-emerald-50 border-2 border-green-200"
: "bg-gradient-to-br from-orange-50 to-yellow-50 border-2 border-orange-200"
}`}>
<div className="flex items-start gap-4">
{allValid ? (
<CheckCircle2 className="w-16 h-16 text-green-600 flex-shrink-0" />
) : (
<AlertCircle className="w-16 h-16 text-orange-600 flex-shrink-0" />
)}
<div className="flex-1">
<h2 className={`text-2xl font-bold mb-2 ${
allValid ? "text-green-900" : "text-orange-900"
}`}>
{allValid ? "Signature Valide" : "Signature Techniquement Valide"}
</h2>
<p className={allValid ? "text-green-700" : "text-orange-700"}>
{allValid
? "Ce document a été signé électroniquement et n'a pas été modifié depuis."
: "La signature est techniquement correcte mais utilise un certificat auto-signé non reconnu par les autorités de certification européennes."
}
</p>
</div>
</div>
</div>
{/* Informations du document */}
<div className="bg-white rounded-2xl shadow-xl p-8 mb-6">
<div className="flex items-center gap-3 mb-6">
<FileText className="w-6 h-6 text-indigo-600" />
<h3 className="text-xl font-bold text-slate-900">Document Signé</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div>
<p className="text-sm text-slate-500 mb-1">Nom du document</p>
<p className="font-semibold text-slate-900">{data.document_name}</p>
</div>
<div>
<p className="text-sm text-slate-500 mb-1">Date de signature</p>
<p className="font-semibold text-slate-900">
{new Date(data.signed_at).toLocaleDateString("fr-FR", {
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit"
})}
</p>
</div>
<div>
<p className="text-sm text-slate-500 mb-1">Signataire</p>
<p className="font-semibold text-slate-900">{data.signer_name}</p>
</div>
<div>
<p className="text-sm text-slate-500 mb-1">Email</p>
<p className="font-semibold text-slate-900">{data.signer_email}</p>
</div>
</div>
<a
href={data.pdf_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
>
<Download className="w-5 h-5" />
Télécharger le document signé
</a>
</div>
{/* Odentas Seal */}
<div className="bg-white rounded-2xl shadow-xl p-8 mb-6">
<div className="flex items-start gap-4 mb-6">
<div className={`w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 ${
data.verification_status.seal_valid ? "bg-green-100" : "bg-orange-100"
}`}>
{data.verification_status.seal_valid ? (
<CheckCircle2 className="w-7 h-7 text-green-600" />
) : (
<AlertCircle className="w-7 h-7 text-orange-600" />
)}
</div>
<div className="flex-1">
<h3 className="text-xl font-bold text-slate-900 mb-1">Odentas Seal</h3>
<p className="text-slate-600">Sceau électronique de signature</p>
</div>
</div>
<div className="space-y-3 pl-16">
<div className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-slate-900">Format PAdES-BASELINE-B</p>
<p className="text-sm text-slate-600">Conforme à la norme ETSI TS 102 778</p>
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-slate-900">Intégrité du document vérifiée</p>
<p className="text-sm text-slate-600">Hash SHA-256: <code className="text-xs bg-slate-100 px-2 py-1 rounded">{data.signature_hash.substring(0, 32)}...</code></p>
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-slate-900">Algorithme RSASSA-PSS avec SHA-256</p>
<p className="text-sm text-slate-600">Clé 2048 bits</p>
</div>
</div>
<div className="mt-4 p-4 bg-slate-50 rounded-lg">
<p className="text-sm font-semibold text-slate-700 mb-2">Certificat de signature</p>
<div className="text-sm space-y-1 text-slate-600">
<p><span className="font-medium">Émetteur:</span> {data.certificate_info.issuer}</p>
<p><span className="font-medium">Sujet:</span> {data.certificate_info.subject}</p>
<p><span className="font-medium">Valide du:</span> {new Date(data.certificate_info.valid_from).toLocaleDateString("fr-FR")} au {new Date(data.certificate_info.valid_until).toLocaleDateString("fr-FR")}</p>
<p><span className="font-medium">Numéro de série:</span> <code className="text-xs bg-white px-2 py-1 rounded">{data.certificate_info.serial_number}</code></p>
</div>
</div>
</div>
</div>
{/* Odentas TSA */}
<div className="bg-white rounded-2xl shadow-xl p-8 mb-6">
<div className="flex items-start gap-4 mb-6">
<div className={`w-12 h-12 rounded-full flex items-center justify-center flex-shrink-0 ${
data.verification_status.timestamp_valid ? "bg-green-100" : "bg-orange-100"
}`}>
{data.verification_status.timestamp_valid ? (
<CheckCircle2 className="w-7 h-7 text-green-600" />
) : (
<AlertCircle className="w-7 h-7 text-orange-600" />
)}
</div>
<div className="flex-1">
<h3 className="text-xl font-bold text-slate-900 mb-1">Odentas TSA</h3>
<p className="text-slate-600">Horodatage électronique certifié</p>
</div>
</div>
<div className="space-y-3 pl-16">
<div className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-slate-900">Horodatage RFC 3161</p>
<p className="text-sm text-slate-600">Conforme à la norme internationale</p>
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-slate-900">Autorité de temps: {data.timestamp.tsa_url}</p>
<p className="text-sm text-slate-600">Timestamp: {new Date(data.timestamp.timestamp).toLocaleString("fr-FR")}</p>
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircle2 className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-slate-900">Empreinte horodatée</p>
<p className="text-sm text-slate-600"><code className="text-xs bg-slate-100 px-2 py-1 rounded">{data.timestamp.hash}</code></p>
</div>
</div>
</div>
</div>
{/* Vérification technique */}
<div className="bg-white rounded-2xl shadow-xl p-8">
<div className="flex items-center gap-3 mb-6">
<Shield className="w-6 h-6 text-indigo-600" />
<h3 className="text-xl font-bold text-slate-900">Vérification Technique</h3>
</div>
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 bg-green-50 rounded-lg">
<CheckCircle2 className="w-5 h-5 text-green-600" />
<span className="text-green-900 font-medium">Structure PAdES valide</span>
</div>
<div className="flex items-center gap-3 p-3 bg-green-50 rounded-lg">
<CheckCircle2 className="w-5 h-5 text-green-600" />
<span className="text-green-900 font-medium">ByteRange correct et complet</span>
</div>
<div className="flex items-center gap-3 p-3 bg-green-50 rounded-lg">
<CheckCircle2 className="w-5 h-5 text-green-600" />
<span className="text-green-900 font-medium">Attribut signing-certificate-v2 présent</span>
</div>
<div className="flex items-center gap-3 p-3 bg-green-50 rounded-lg">
<CheckCircle2 className="w-5 h-5 text-green-600" />
<span className="text-green-900 font-medium">MessageDigest intact</span>
</div>
<div className="flex items-center gap-3 p-3 bg-orange-50 rounded-lg">
<AlertCircle className="w-5 h-5 text-orange-600" />
<span className="text-orange-900 font-medium">Certificat auto-signé (non reconnu par les autorités européennes)</span>
</div>
</div>
</div>
{/* Footer */}
<div className="text-center mt-8 text-slate-500 text-sm">
<p>Cette page de vérification est publique et accessible via le QR code fourni avec le document.</p>
<p className="mt-2">Odentas Media SAS - Signature électronique conforme PAdES-BASELINE-B</p>
</div>
</div>
</div>
);
}