CIA/e-voting-system/frontend/components/blockchain-viewer.tsx
Alexis Bruneteau dde0164b27 feat: Implement Phase 4 - Blockchain Visualization
Add comprehensive blockchain viewer with:
- BlockchainViewer component: Display blocks in expandable cards
- Hash visualization: Show SHA-256 hashes for each block
- Chain verification: Visual integrity status and verification button
- Block details: Expand to see full block information
  - Index, timestamp, previous hash, block hash
  - Encrypted vote data, transaction ID
  - Digital signatures
- Election selector: View blockchain for different elections
- Mock data: Demo blockchain included for testing
- Responsive design: Works on mobile and desktop

UI Features:
  ✓ Block expansion/collapse with icon indicators
  ✓ Genesis block highlighted with  icon
  ✓ Vote blocks marked with 🔒 icon
  ✓ Chain link visual indicators
  ✓ Hash truncation with full display on expand
  ✓ Status indicators: Chain valid/invalid
  ✓ Security information panel
  ✓ Statistics: Total blocks, votes, integrity status

Integration:
  ✓ Fetch elections list from API
  ✓ Fetch blockchain state for selected election
  ✓ Verify blockchain integrity
  ✓ Handle empty blockchain state
  ✓ Error handling with user feedback
  ✓ Loading states during API calls

Routes:
  ✓ /dashboard/blockchain - Main blockchain viewer
  ✓ Added to sidebar navigation
  ✓ 13 total routes now (added 1 new)

Frontend Build:
  ✓ No TypeScript errors
  ✓ Zero unused imports
  ✓ Production build successful
  ✓ All routes prerendered

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 01:59:46 +01:00

310 lines
12 KiB
TypeScript

"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { ChevronDown, ChevronUp, CheckCircle, AlertCircle, Lock, Zap } 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 BlockchainViewerProps {
data: BlockchainData
isLoading?: boolean
isVerifying?: boolean
onVerify?: () => void
}
export function BlockchainViewer({
data,
isLoading = false,
isVerifying = false,
onVerify,
}: BlockchainViewerProps) {
const [expandedBlocks, setExpandedBlocks] = useState<number[]>([])
const toggleBlockExpand = (index: number) => {
setExpandedBlocks((prev) =>
prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
)
}
const truncateHash = (hash: string, length: number = 16) => {
return hash.length > length ? `${hash.slice(0, length)}...` : hash
}
const formatTimestamp = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString("fr-FR")
}
if (isLoading) {
return (
<Card>
<CardContent className="pt-6 flex items-center justify-center py-12">
<div className="text-center">
<div className="mb-4 w-8 h-8 border-4 border-accent border-t-transparent rounded-full animate-spin mx-auto" />
<p className="text-sm text-muted-foreground">Chargement de la blockchain...</p>
</div>
</CardContent>
</Card>
)
}
return (
<div className="space-y-6">
{/* Verification Summary */}
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>État de la Blockchain</span>
<div className="flex items-center gap-2">
{data.verification.chain_valid ? (
<div className="flex items-center gap-2 text-green-600">
<CheckCircle className="w-5 h-5" />
<span className="text-sm font-medium">Valide</span>
</div>
) : (
<div className="flex items-center gap-2 text-red-600">
<AlertCircle className="w-5 h-5" />
<span className="text-sm font-medium">Invalide</span>
</div>
)}
</div>
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4">
{/* Total Blocks */}
<div className="p-4 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground">Nombre de Blocs</div>
<div className="text-2xl font-bold mt-1">{data.verification.total_blocks}</div>
<div className="text-xs text-muted-foreground mt-1">
Dont 1 bloc de genèse
</div>
</div>
{/* Total Votes */}
<div className="p-4 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground">Votes Enregistrés</div>
<div className="text-2xl font-bold mt-1">{data.verification.total_votes}</div>
<div className="text-xs text-muted-foreground mt-1">
Votes chiffrés
</div>
</div>
{/* Integrity Check */}
<div className="p-4 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground">Intégrité</div>
<div className="text-2xl font-bold mt-1">
{data.verification.chain_valid ? "✓" : "✗"}
</div>
<div className="text-xs text-muted-foreground mt-1">
Chaîne de hachage valide
</div>
</div>
</div>
{/* Verify Button */}
{onVerify && (
<Button
onClick={onVerify}
disabled={isVerifying}
className="mt-4 w-full"
variant="outline"
>
{isVerifying ? (
<>
<div className="w-4 h-4 border-2 border-accent border-t-transparent rounded-full animate-spin mr-2" />
Vérification en cours...
</>
) : (
<>
<Lock className="w-4 h-4 mr-2" />
Vérifier l'Intégrité
</>
)}
</Button>
)}
</CardContent>
</Card>
{/* Chain Visualization */}
<Card>
<CardHeader>
<CardTitle>Chaîne de Blocs</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{data.blocks.map((block, index) => (
<div key={index}>
{/* Block Header */}
<button
onClick={() => toggleBlockExpand(index)}
className="w-full p-4 bg-muted rounded-lg hover:bg-muted/80 transition-colors text-left flex items-center justify-between group"
>
<div className="flex items-center gap-3 flex-1">
{/* Block Type Indicator */}
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-background">
{block.index === 0 ? (
<Zap className="w-4 h-4 text-yellow-600" />
) : (
<Lock className="w-4 h-4 text-green-600" />
)}
</div>
{/* Block Info */}
<div className="flex-1">
<div className="text-sm font-medium">
{block.index === 0 ? "Bloc de Genèse" : `Bloc ${block.index}`}
</div>
<div className="text-xs text-muted-foreground mt-1">
TX ID: <span className="font-mono">{truncateHash(block.transaction_id)}</span>
</div>
</div>
{/* Timestamp */}
<div className="text-xs text-muted-foreground text-right hidden md:block">
{formatTimestamp(block.timestamp)}
</div>
</div>
{/* Expand Icon */}
<div className="ml-2">
{expandedBlocks.includes(index) ? (
<ChevronUp className="w-5 h-5 text-muted-foreground" />
) : (
<ChevronDown className="w-5 h-5 text-muted-foreground" />
)}
</div>
</button>
{/* Block Details */}
{expandedBlocks.includes(index) && (
<div className="mt-2 p-4 bg-background rounded-lg border border-border space-y-3">
{/* Index */}
<div>
<div className="text-xs font-medium text-muted-foreground">Index</div>
<div className="text-sm font-mono mt-1">{block.index}</div>
</div>
{/* Timestamp */}
<div>
<div className="text-xs font-medium text-muted-foreground">Timestamp</div>
<div className="text-sm mt-1">{formatTimestamp(block.timestamp)}</div>
</div>
{/* Previous Hash */}
<div>
<div className="text-xs font-medium text-muted-foreground">Hash Précédent</div>
<div className="text-xs font-mono bg-muted p-2 rounded mt-1 break-all">
{block.prev_hash}
</div>
</div>
{/* Block Hash */}
<div>
<div className="text-xs font-medium text-muted-foreground">Hash du Bloc</div>
<div className="text-xs font-mono bg-muted p-2 rounded mt-1 break-all text-accent font-bold">
{block.block_hash}
</div>
</div>
{/* Encrypted Vote */}
{block.encrypted_vote && (
<div>
<div className="text-xs font-medium text-muted-foreground">Vote Chiffré</div>
<div className="text-xs font-mono bg-muted p-2 rounded mt-1 break-all">
{truncateHash(block.encrypted_vote, 64)}
</div>
</div>
)}
{/* Transaction ID */}
<div>
<div className="text-xs font-medium text-muted-foreground">Identifiant de Transaction</div>
<div className="text-xs font-mono bg-muted p-2 rounded mt-1">
{block.transaction_id}
</div>
</div>
{/* Signature */}
{block.signature && (
<div>
<div className="text-xs font-medium text-muted-foreground">Signature</div>
<div className="text-xs font-mono bg-muted p-2 rounded mt-1 break-all">
{truncateHash(block.signature, 64)}
</div>
</div>
)}
{/* Chain Verification Status */}
<div className="pt-2 border-t border-border">
<div className="text-xs font-medium text-muted-foreground mb-2">Vérification</div>
<div className="flex gap-2">
<div className="flex-1 p-2 bg-green-50 dark:bg-green-950 rounded text-xs">
<span className="text-green-700 dark:text-green-300 font-medium"> Hash valide</span>
</div>
{block.index > 0 && (
<div className="flex-1 p-2 bg-green-50 dark:bg-green-950 rounded text-xs">
<span className="text-green-700 dark:text-green-300 font-medium"> Chaîne liée</span>
</div>
)}
</div>
</div>
</div>
)}
{/* Chain Link Indicator */}
{index < data.blocks.length - 1 && (
<div className="flex justify-center py-2">
<div className="w-0.5 h-4 bg-gradient-to-b from-muted to-background" />
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
{/* Security Info */}
<Card className="bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800">
<CardHeader>
<CardTitle className="text-base text-blue-900 dark:text-blue-100">
Information de Sécurité
</CardTitle>
</CardHeader>
<CardContent className="text-sm text-blue-800 dark:text-blue-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>
)
}