- Created `/frontend/app/api/votes/check/route.ts` to handle GET requests for checking if a user has voted in a specific election. - Added error handling for unauthorized access and missing election ID. - Forwarded requests to the backend API and returned appropriate responses. - Updated `/frontend/app/api/votes/history/route.ts` to fetch user's voting history with error handling. - Ensured both endpoints utilize the authorization token for secure access.
517 lines
21 KiB
TypeScript
517 lines
21 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
ChevronDown,
|
|
ChevronUp,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
Lock,
|
|
Zap,
|
|
Copy,
|
|
CheckCheck,
|
|
Shield,
|
|
Activity,
|
|
} from "lucide-react"
|
|
|
|
export interface Block {
|
|
index: number
|
|
prev_hash: string
|
|
timestamp: number
|
|
encrypted_vote: string
|
|
transaction_id: string
|
|
block_hash: string
|
|
signature: string
|
|
}
|
|
|
|
export interface BlockchainData {
|
|
blocks: Block[]
|
|
verification: {
|
|
chain_valid: boolean
|
|
total_blocks: number
|
|
total_votes: number
|
|
}
|
|
}
|
|
|
|
interface BlockchainVisualizerProps {
|
|
data: BlockchainData
|
|
isLoading?: boolean
|
|
isVerifying?: boolean
|
|
onVerify?: () => void
|
|
}
|
|
|
|
export function BlockchainVisualizer({
|
|
data,
|
|
isLoading = false,
|
|
isVerifying = false,
|
|
onVerify,
|
|
}: BlockchainVisualizerProps) {
|
|
const [expandedBlocks, setExpandedBlocks] = useState<number[]>([])
|
|
const [copiedHash, setCopiedHash] = useState<string | null>(null)
|
|
const [animatingBlocks, setAnimatingBlocks] = useState<number[]>([])
|
|
|
|
// Validate data parameter - must be after hooks
|
|
const isValidData =
|
|
data &&
|
|
Array.isArray(data.blocks) &&
|
|
data.verification &&
|
|
typeof data.verification.total_blocks === "number" &&
|
|
typeof data.verification.total_votes === "number"
|
|
|
|
// Animate blocks on load
|
|
useEffect(() => {
|
|
if (!isValidData || !data?.blocks || data.blocks.length === 0) return
|
|
|
|
data.blocks.forEach((_, index) => {
|
|
setTimeout(() => {
|
|
setAnimatingBlocks((prev) => [...prev, index])
|
|
}, index * 100)
|
|
})
|
|
}, [data?.blocks, isValidData])
|
|
|
|
const toggleBlockExpand = (index: number) => {
|
|
setExpandedBlocks((prev) =>
|
|
prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
|
|
)
|
|
}
|
|
|
|
const copyToClipboard = (text: string, hashType: string) => {
|
|
navigator.clipboard.writeText(text)
|
|
setCopiedHash(hashType)
|
|
setTimeout(() => setCopiedHash(null), 2000)
|
|
}
|
|
|
|
const truncateHash = (hash: string | undefined | null, length: number = 16) => {
|
|
// Validation
|
|
if (hash === null || hash === undefined) {
|
|
return "N/A"
|
|
}
|
|
|
|
if (typeof hash !== "string") {
|
|
return "N/A"
|
|
}
|
|
|
|
if (hash.length === 0) {
|
|
return "N/A"
|
|
}
|
|
|
|
const result = hash.length > length ? `${hash.slice(0, length)}...` : hash
|
|
return result
|
|
}
|
|
|
|
const formatTimestamp = (timestamp: number) => {
|
|
return new Date(timestamp * 1000).toLocaleString("fr-FR")
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Card className="bg-gradient-to-br from-slate-900 to-slate-800 border-slate-700">
|
|
<CardContent className="pt-6 flex items-center justify-center py-16">
|
|
<div className="text-center">
|
|
<div className="mb-4 w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto" />
|
|
<p className="text-sm text-slate-300">Chargement de la blockchain...</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
// Validate data after hooks
|
|
if (!isValidData) {
|
|
return (
|
|
<Card className="border-red-200 bg-red-50">
|
|
<CardHeader>
|
|
<CardTitle className="text-red-800">Erreur</CardTitle>
|
|
<CardDescription className="text-red-700">
|
|
Format blockchain invalide ou données non disponibles
|
|
</CardDescription>
|
|
</CardHeader>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Stats Dashboard */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
{/* Total Blocks */}
|
|
<Card className="bg-gradient-to-br from-blue-900/50 to-blue-800/50 border-blue-700/50 hover:border-blue-600/80 transition-all">
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs text-blue-300 font-medium">Blocs</p>
|
|
<p className="text-3xl font-bold text-blue-100 mt-1">
|
|
{data.verification.total_blocks}
|
|
</p>
|
|
</div>
|
|
<Zap className="w-8 h-8 text-yellow-400" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Total Votes */}
|
|
<Card className="bg-gradient-to-br from-green-900/50 to-green-800/50 border-green-700/50 hover:border-green-600/80 transition-all">
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs text-green-300 font-medium">Votes</p>
|
|
<p className="text-3xl font-bold text-green-100 mt-1">
|
|
{data.verification.total_votes}
|
|
</p>
|
|
</div>
|
|
<Lock className="w-8 h-8 text-green-400" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Chain Status */}
|
|
<Card
|
|
className={`bg-gradient-to-br ${
|
|
data.verification.chain_valid
|
|
? "from-emerald-900/50 to-emerald-800/50 border-emerald-700/50 hover:border-emerald-600/80"
|
|
: "from-red-900/50 to-red-800/50 border-red-700/50 hover:border-red-600/80"
|
|
} transition-all`}
|
|
>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs text-gray-300 font-medium">Statut</p>
|
|
<p className="text-sm font-bold text-gray-100 mt-1">
|
|
{data.verification.chain_valid ? "✓ Valide" : "✗ Invalide"}
|
|
</p>
|
|
</div>
|
|
{data.verification.chain_valid ? (
|
|
<CheckCircle className="w-8 h-8 text-emerald-400" />
|
|
) : (
|
|
<AlertCircle className="w-8 h-8 text-red-400" />
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Security Score */}
|
|
<Card className="bg-gradient-to-br from-purple-900/50 to-purple-800/50 border-purple-700/50 hover:border-purple-600/80 transition-all">
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs text-purple-300 font-medium">Sécurité</p>
|
|
<p className="text-3xl font-bold text-purple-100 mt-1">100%</p>
|
|
</div>
|
|
<Shield className="w-8 h-8 text-purple-400" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Verification Button */}
|
|
{onVerify && (
|
|
<Card className="bg-gradient-to-r from-slate-900 to-slate-800 border-slate-700">
|
|
<CardContent className="pt-6">
|
|
<Button
|
|
onClick={onVerify}
|
|
disabled={isVerifying}
|
|
className="w-full bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-700 hover:to-blue-600 text-white font-semibold py-3 rounded-lg transition-all"
|
|
>
|
|
{isVerifying ? (
|
|
<>
|
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
|
|
Vérification en cours...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Activity className="w-4 h-4 mr-2" />
|
|
Vérifier l'Intégrité de la Chaîne
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Blockchain Visualization */}
|
|
<Card className="bg-gradient-to-br from-slate-900 to-slate-800 border-slate-700">
|
|
<CardHeader>
|
|
<CardTitle className="text-2xl bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">
|
|
Chaîne de Blocs
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{data && Array.isArray(data.blocks) && data.blocks.map((block, index) => {
|
|
const isAnimating = animatingBlocks.includes(index)
|
|
const isExpanded = expandedBlocks.includes(index)
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
className={`transition-all duration-500 ${
|
|
isAnimating ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
|
}`}
|
|
>
|
|
{/* Block Card */}
|
|
<button
|
|
onClick={() => toggleBlockExpand(index)}
|
|
className={`w-full p-4 rounded-lg border-2 transition-all duration-300 ${
|
|
isExpanded
|
|
? "bg-gradient-to-r from-blue-900/80 to-purple-900/80 border-blue-500/80 shadow-lg shadow-blue-500/20"
|
|
: "bg-gradient-to-r from-slate-800 to-slate-700 border-slate-600 hover:border-slate-500 hover:shadow-lg hover:shadow-slate-600/20"
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4 flex-1">
|
|
{/* Block Icon */}
|
|
<div
|
|
className={`flex items-center justify-center w-10 h-10 rounded-lg transition-all ${
|
|
block.index === 0
|
|
? "bg-yellow-900/50 text-yellow-400"
|
|
: "bg-green-900/50 text-green-400"
|
|
}`}
|
|
>
|
|
{block.index === 0 ? <Zap size={20} /> : <Lock size={20} />}
|
|
</div>
|
|
|
|
{/* Block Info */}
|
|
<div className="text-left flex-1">
|
|
<div className="text-sm font-bold text-gray-200">
|
|
{block.index === 0 ? "Bloc de Genèse" : `Bloc ${block.index}`}
|
|
</div>
|
|
<div className="text-xs text-gray-400 mt-1">
|
|
{truncateHash(block.transaction_id, 20)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hash Preview */}
|
|
<div className="text-xs text-gray-400 hidden md:block">
|
|
{truncateHash(block.block_hash, 12)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Expand Icon */}
|
|
<div className="ml-2">
|
|
{isExpanded ? (
|
|
<ChevronUp className="w-5 h-5 text-blue-400" />
|
|
) : (
|
|
<ChevronDown className="w-5 h-5 text-gray-400" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Expanded Details */}
|
|
{isExpanded && (
|
|
<div className="mt-2 p-4 bg-gradient-to-b from-slate-800 to-slate-700 rounded-lg border border-slate-600 space-y-4 animate-in fade-in slide-in-from-top-2">
|
|
{/* Block Index */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-xs font-medium text-slate-400 uppercase">
|
|
Index
|
|
</label>
|
|
<div className="text-sm font-mono text-blue-300 mt-1">
|
|
{block.index}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Timestamp */}
|
|
<div>
|
|
<label className="text-xs font-medium text-slate-400 uppercase">
|
|
Timestamp
|
|
</label>
|
|
<div className="text-sm text-slate-300 mt-1">
|
|
{formatTimestamp(block.timestamp)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-slate-600 pt-4">
|
|
{/* Previous Hash */}
|
|
<div className="mb-4">
|
|
<label className="text-xs font-medium text-slate-400 uppercase">
|
|
Hash Précédent
|
|
</label>
|
|
<div className="mt-2 flex items-center gap-2">
|
|
<code className="text-xs bg-slate-900 p-2 rounded flex-1 text-slate-300 font-mono break-all">
|
|
{block.prev_hash}
|
|
</code>
|
|
<button
|
|
onClick={() =>
|
|
copyToClipboard(block.prev_hash, `prev-${index}`)
|
|
}
|
|
className="p-2 hover:bg-slate-600 rounded transition-colors"
|
|
>
|
|
{copiedHash === `prev-${index}` ? (
|
|
<CheckCheck size={16} className="text-green-400" />
|
|
) : (
|
|
<Copy size={16} className="text-slate-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Block Hash */}
|
|
<div className="mb-4">
|
|
<label className="text-xs font-medium text-slate-400 uppercase">
|
|
Hash du Bloc
|
|
</label>
|
|
<div className="mt-2 flex items-center gap-2">
|
|
<code className="text-xs bg-gradient-to-r from-blue-900 to-purple-900 p-2 rounded flex-1 text-blue-300 font-mono break-all font-bold">
|
|
{block.block_hash}
|
|
</code>
|
|
<button
|
|
onClick={() =>
|
|
copyToClipboard(block.block_hash, `block-${index}`)
|
|
}
|
|
className="p-2 hover:bg-slate-600 rounded transition-colors"
|
|
>
|
|
{copiedHash === `block-${index}` ? (
|
|
<CheckCheck size={16} className="text-green-400" />
|
|
) : (
|
|
<Copy size={16} className="text-slate-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Encrypted Vote */}
|
|
{block.encrypted_vote && (
|
|
<div className="mb-4">
|
|
<label className="text-xs font-medium text-slate-400 uppercase">
|
|
Vote Chiffré
|
|
</label>
|
|
<div className="mt-2 flex items-center gap-2">
|
|
<code className="text-xs bg-slate-900 p-2 rounded flex-1 text-slate-300 font-mono break-all">
|
|
{truncateHash(block.encrypted_vote, 60)}
|
|
</code>
|
|
<button
|
|
onClick={() =>
|
|
copyToClipboard(block.encrypted_vote, `vote-${index}`)
|
|
}
|
|
className="p-2 hover:bg-slate-600 rounded transition-colors"
|
|
>
|
|
{copiedHash === `vote-${index}` ? (
|
|
<CheckCheck size={16} className="text-green-400" />
|
|
) : (
|
|
<Copy size={16} className="text-slate-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Transaction ID */}
|
|
<div className="mb-4">
|
|
<label className="text-xs font-medium text-slate-400 uppercase">
|
|
Identifiant de Transaction
|
|
</label>
|
|
<div className="mt-2 flex items-center gap-2">
|
|
<code className="text-xs bg-slate-900 p-2 rounded flex-1 text-slate-300 font-mono">
|
|
{block.transaction_id}
|
|
</code>
|
|
<button
|
|
onClick={() =>
|
|
copyToClipboard(block.transaction_id, `tx-${index}`)
|
|
}
|
|
className="p-2 hover:bg-slate-600 rounded transition-colors"
|
|
>
|
|
{copiedHash === `tx-${index}` ? (
|
|
<CheckCheck size={16} className="text-green-400" />
|
|
) : (
|
|
<Copy size={16} className="text-slate-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Signature */}
|
|
{block.signature && (
|
|
<div className="mb-4">
|
|
<label className="text-xs font-medium text-slate-400 uppercase">
|
|
Signature Numérique
|
|
</label>
|
|
<div className="mt-2 flex items-center gap-2">
|
|
<code className="text-xs bg-slate-900 p-2 rounded flex-1 text-slate-300 font-mono break-all">
|
|
{truncateHash(block.signature, 60)}
|
|
</code>
|
|
<button
|
|
onClick={() =>
|
|
copyToClipboard(block.signature, `sig-${index}`)
|
|
}
|
|
className="p-2 hover:bg-slate-600 rounded transition-colors"
|
|
>
|
|
{copiedHash === `sig-${index}` ? (
|
|
<CheckCheck size={16} className="text-green-400" />
|
|
) : (
|
|
<Copy size={16} className="text-slate-400" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Verification Status */}
|
|
<div className="border-t border-slate-600 pt-4">
|
|
<label className="text-xs font-medium text-slate-400 uppercase mb-2 block">
|
|
Vérification
|
|
</label>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
<div className="flex items-center gap-2 p-2 bg-green-900/30 rounded border border-green-700/50">
|
|
<CheckCircle size={16} className="text-green-400" />
|
|
<span className="text-xs text-green-300 font-medium">
|
|
Hash valide
|
|
</span>
|
|
</div>
|
|
{block.index > 0 && (
|
|
<div className="flex items-center gap-2 p-2 bg-green-900/30 rounded border border-green-700/50">
|
|
<CheckCircle size={16} className="text-green-400" />
|
|
<span className="text-xs text-green-300 font-medium">
|
|
Chaîne liée
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Chain Link Indicator */}
|
|
{data && Array.isArray(data.blocks) && index < data.blocks.length - 1 && (
|
|
<div className="flex justify-center py-2">
|
|
<div className="relative w-1 h-6 bg-gradient-to-b from-blue-500/60 to-transparent rounded-full" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Security Info Panel */}
|
|
<Card className="bg-gradient-to-br from-indigo-900/30 to-blue-900/30 border-indigo-700/50">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg text-indigo-200 flex items-center gap-2">
|
|
<Shield size={20} />
|
|
Information de Sécurité
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="text-sm text-indigo-200 space-y-2">
|
|
<p>
|
|
• <strong>Immuabilité:</strong> Chaque bloc contient le hash du bloc
|
|
précédent. Toute modification invalide toute la chaîne.
|
|
</p>
|
|
<p>
|
|
• <strong>Transparence:</strong> Tous les votes sont enregistrés et
|
|
vérifiables sans révéler le contenu du vote.
|
|
</p>
|
|
<p>
|
|
• <strong>Chiffrement:</strong> Les votes sont chiffrés avec ElGamal. Seul
|
|
le dépouillement utilise les clés privées.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|