CIA/e-voting-system/frontend/components/voting-interface.tsx
E-Voting Developer dfdf159198 fix: ElGamal encryption, vote deduplication, and frontend data validation
- Fixed ElGamal class instantiation in votes.py (ElGamalEncryption instead of ElGamal)
- Fixed public key serialization in admin.py (use public_key_bytes property)
- Implemented database migration with SQL-based key generation
- Added vote deduplication endpoint: GET /api/votes/check
- Protected all array accesses with type validation in frontend
- Fixed vote parameter type handling (string to number conversion)
- Removed all debug console logs for production
- Created missing dynamic route for vote history details

Fixes:
- JavaScript error: "can't access property length, e is undefined"
- Vote deduplication not preventing form display
- Frontend data validation issues
- Missing dynamic routes
2025-11-08 00:05:19 +01:00

375 lines
12 KiB
TypeScript

"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { AlertCircle, CheckCircle, Loader2 } from "lucide-react"
import { createSignedBallot, PublicKeysResponse } from "@/lib/crypto-client"
export interface Candidate {
id: number
name: string
description?: string
}
export interface VotingInterfaceProps {
electionId: number
candidates: Candidate[]
publicKeys?: PublicKeysResponse
onVoteSubmitted?: (success: boolean, transactionId?: string) => void
}
type VotingStep = "select" | "confirm" | "submitting" | "success" | "error"
/**
* Voting Interface Component
* Handles ballot creation, encryption, and submission
*/
export function VotingInterface({
electionId,
candidates,
publicKeys,
onVoteSubmitted,
}: VotingInterfaceProps) {
const [step, setStep] = useState<VotingStep>("select")
const [selectedCandidate, setSelectedCandidate] = useState<number | null>(null)
const [error, setError] = useState<string>("")
const [transactionId, setTransactionId] = useState<string>("")
const [loading, setLoading] = useState(false)
/**
* Handle candidate selection
*/
const handleSelectCandidate = (candidateId: number) => {
setSelectedCandidate(candidateId)
setStep("confirm")
setError("")
}
/**
* Handle back button to reselect
*/
const handleBack = () => {
setSelectedCandidate(null)
setStep("select")
setError("")
}
/**
* Handle vote submission with encryption and signing
*/
const handleSubmitVote = async () => {
if (selectedCandidate === null) {
setError("Veuillez sélectionner un candidat")
return
}
setLoading(true)
setError("")
try {
// 1. Get voter context from API
const voterResponse = await fetch("/api/auth/profile", {
headers: {
"Authorization": `Bearer ${localStorage.getItem("auth_token")}`
}
})
if (!voterResponse.ok) {
throw new Error("Impossible de récupérer le profil du votant")
}
const voterData = await voterResponse.json()
const voterId = voterData.id.toString()
// 2. Get or use provided public keys
let keysToUse = publicKeys
if (!keysToUse) {
// Fetch public keys from server
const keysResponse = await fetch(`/api/votes/public-keys?election_id=${electionId}`)
if (!keysResponse.ok) {
throw new Error("Impossible de récupérer les clés publiques")
}
keysToUse = await keysResponse.json()
}
if (!keysToUse?.elgamal_pubkey) {
throw new Error("Clés publiques non disponibles")
}
// 3. Create signed ballot with client-side encryption
// For MVP: Use simple vote encoding (0 or 1)
// Selected candidate = 1, others = 0
const voteValue = selectedCandidate ? 1 : 0
const ballot = createSignedBallot(
voteValue,
voterId,
keysToUse.elgamal_pubkey,
"" // Empty private key for signing in MVP
)
// 4. Submit encrypted ballot
setStep("submitting")
const submitResponse = await fetch("/api/votes/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.getItem("auth_token")}`
},
body: JSON.stringify({
election_id: electionId,
candidate_id: selectedCandidate,
encrypted_vote: ballot.encrypted_vote,
zkp_proof: ballot.zkp_proof,
signature: ballot.signature,
timestamp: ballot.timestamp
})
})
if (!submitResponse.ok) {
const errorData = await submitResponse.json()
throw new Error(errorData.detail || "Erreur lors de la soumission du vote")
}
const result = await submitResponse.json()
// 5. Success
setTransactionId(result.transaction_id || result.id)
setStep("success")
// Notify parent component
if (onVoteSubmitted) {
onVoteSubmitted(true, result.transaction_id || result.id)
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Erreur inconnue"
setError(errorMessage)
setStep("error")
if (onVoteSubmitted) {
onVoteSubmitted(false)
}
} finally {
setLoading(false)
}
}
/**
* Reset to allow new vote (in testing mode only)
*/
const handleReset = () => {
setSelectedCandidate(null)
setStep("select")
setError("")
setTransactionId("")
}
return (
<div className="w-full max-w-2xl mx-auto">
{/* Selection Step */}
{step === "select" && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold">Sélectionnez votre vote</h2>
<p className="text-muted-foreground mt-2">
Choisissez le candidat ou l'option pour lequel vous souhaitez voter
</p>
</div>
<div className="grid gap-4">
{candidates && candidates.length > 0 ? (
candidates.map((candidate) => (
<Card
key={candidate.id}
className="cursor-pointer hover:border-accent hover:bg-accent/5 transition-colors"
onClick={() => handleSelectCandidate(candidate.id)}
>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">{candidate.name}</h3>
{candidate.description && (
<p className="text-sm text-muted-foreground mt-1">
{candidate.description}
</p>
)}
</div>
<div className="h-6 w-6 rounded-full border-2 border-muted-foreground" />
</div>
</CardContent>
</Card>
))
) : (
<p className="text-muted-foreground text-center py-8">
Aucun candidat disponible pour cette élection
</p>
)}
</div>
{error && (
<Card className="border-red-500 bg-red-50">
<CardContent className="pt-6 flex gap-4">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-800">{error}</p>
</CardContent>
</Card>
)}
</div>
)}
{/* Confirmation Step */}
{step === "confirm" && selectedCandidate !== null && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold">Confirmez votre vote</h2>
<p className="text-muted-foreground mt-2">
Veuillez vérifier votre sélection avant de soumettre
</p>
</div>
<Card className="bg-accent/10 border-accent">
<CardHeader>
<CardTitle>Vote sélectionné</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<p className="font-semibold">
{candidates.find((c) => c.id === selectedCandidate)?.name}
</p>
<p className="text-sm text-muted-foreground">
{candidates.find((c) => c.id === selectedCandidate)?.description}
</p>
</div>
</CardContent>
</Card>
<Card className="bg-blue-50 border-blue-200">
<CardContent className="pt-6">
<p className="text-sm text-blue-900">
<strong>Sécurité du vote:</strong> Votre bulletin sera chiffré avec
ElGamal homomorphe avant transmission. Seul le décompte final sera connu,
pas votre vote individuel.
</p>
</CardContent>
</Card>
<div className="flex gap-3">
<Button variant="outline" onClick={handleBack} disabled={loading}>
Retour
</Button>
<Button
onClick={handleSubmitVote}
disabled={loading}
className="flex-1"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Envoi en cours...
</>
) : (
"Confirmer et voter"
)}
</Button>
</div>
{error && (
<Card className="border-red-500 bg-red-50">
<CardContent className="pt-6 flex gap-4">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-800">{error}</p>
</CardContent>
</Card>
)}
</div>
)}
{/* Submitting Step */}
{step === "submitting" && (
<Card>
<CardContent className="pt-6 flex flex-col items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-accent mb-4" />
<h3 className="font-semibold">Soumission du vote en cours...</h3>
<p className="text-sm text-muted-foreground mt-2">
Votre bulletin est en cours de chiffrement et d'enregistrement
</p>
</CardContent>
</Card>
)}
{/* Success Step */}
{step === "success" && (
<div className="space-y-6">
<Card className="bg-green-50 border-green-200">
<CardContent className="pt-6">
<div className="flex gap-4">
<CheckCircle className="h-6 w-6 text-green-600 flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-semibold text-green-900">Vote enregistré avec succès</h3>
<p className="text-sm text-green-800 mt-1">
Votre bulletin chiffré a é ajouté à la blockchain électorale
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Détails du vote</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<label className="text-xs font-medium text-muted-foreground">
Identifiant de transaction
</label>
<p className="font-mono text-sm break-all bg-muted p-2 rounded mt-1">
{transactionId}
</p>
</div>
<div className="text-sm text-muted-foreground">
<p> Bulletin chiffré avec ElGamal</p>
<p> Signature Dilithium appliquée</p>
<p> Enregistré dans la blockchain</p>
</div>
</CardContent>
</Card>
<Button className="w-full" onClick={handleReset}>
Fermer
</Button>
</div>
)}
{/* Error Step */}
{step === "error" && (
<div className="space-y-6">
<Card className="border-red-500 bg-red-50">
<CardContent className="pt-6">
<div className="flex gap-4">
<AlertCircle className="h-6 w-6 text-red-600 flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-semibold text-red-900">Erreur lors de la soumission</h3>
<p className="text-sm text-red-800 mt-1">{error}</p>
</div>
</div>
</CardContent>
</Card>
<div className="flex gap-3">
<Button variant="outline" onClick={handleBack}>
Retour
</Button>
<Button onClick={handleReset} className="flex-1">
Réessayer
</Button>
</div>
</div>
)}
</div>
)
}