- 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é.
326 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|