E-Voting Developer 5bebad45b8 Initial commit: Complete e-voting system with cryptography
- FastAPI backend with JWT authentication
- ElGamal, RSA-PSS, ZK-proofs crypto modules
- HTML5/JS frontend SPA
- MariaDB database with 5 tables
- Docker Compose with 3 services (frontend, backend, mariadb)
- Comprehensive tests for cryptography
- Typst technical report (30+ pages)
- Makefile with development commands
2025-11-03 16:13:08 +01:00

557 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Système de Vote Électronique Sécurisé</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 800px;
width: 90%;
padding: 40px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 16px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
}
input, select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
.button {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
}
.button:active {
transform: translateY(0);
}
.alert {
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
}
.alert.error {
background: #fee;
color: #c00;
border-left: 4px solid #c00;
}
.alert.success {
background: #efe;
color: #0a0;
border-left: 4px solid #0a0;
}
.view {
display: none;
}
.view.active {
display: block;
}
.nav-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.nav-buttons button {
flex: 1;
padding: 10px;
background: #f0f0f0;
border: 2px solid #ddd;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.nav-buttons button:hover {
background: #e0e0e0;
border-color: #667eea;
}
.candidates {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
margin-bottom: 20px;
}
.candidate-card {
padding: 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.candidate-card:hover {
border-color: #667eea;
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.2);
}
.candidate-card.selected {
border-color: #667eea;
background: #f8f9ff;
}
.candidate-card h3 {
color: #333;
margin-bottom: 8px;
}
.candidate-card p {
color: #666;
font-size: 14px;
}
.results {
margin-top: 20px;
}
.result-item {
margin-bottom: 15px;
}
.result-bar {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.result-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
}
.result-info {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 14px;
}
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<!-- Login View -->
<div id="loginView" class="view active">
<div class="header">
<h1>🗳️ Vote Électronique Sécurisé</h1>
<p>Système de vote avec chiffrement cryptographique</p>
</div>
<div id="loginError" class="alert error" style="display: none;"></div>
<div class="form-group">
<label for="loginEmail">Email</label>
<input type="email" id="loginEmail" placeholder="voter@example.com">
</div>
<div class="form-group">
<label for="loginPassword">Mot de passe</label>
<input type="password" id="loginPassword" placeholder="••••••••">
</div>
<button class="button" onclick="handleLogin()">Se connecter</button>
<div class="nav-buttons">
<button onclick="switchView('registerView')">Créer un compte</button>
</div>
</div>
<!-- Register View -->
<div id="registerView" class="view">
<div class="header">
<h1>📝 Inscription</h1>
<p>Créer un compte pour voter</p>
</div>
<div id="registerError" class="alert error" style="display: none;"></div>
<div class="form-group">
<label for="registerFirstName">Prénom</label>
<input type="text" id="registerFirstName" placeholder="Jean">
</div>
<div class="form-group">
<label for="registerLastName">Nom</label>
<input type="text" id="registerLastName" placeholder="Dupont">
</div>
<div class="form-group">
<label for="registerEmail">Email</label>
<input type="email" id="registerEmail" placeholder="jean.dupont@example.com">
</div>
<div class="form-group">
<label for="registerCitizenId">Identifiant Citoyen (CNI)</label>
<input type="text" id="registerCitizenId" placeholder="12345678">
</div>
<div class="form-group">
<label for="registerPassword">Mot de passe</label>
<input type="password" id="registerPassword" placeholder="••••••••">
</div>
<button class="button" onclick="handleRegister()">S'inscrire</button>
<div class="nav-buttons">
<button onclick="switchView('loginView')">Retour au login</button>
</div>
</div>
<!-- Vote View -->
<div id="voteView" class="view">
<div class="header">
<h1>🗳️ Participation au Vote</h1>
<p id="electionTitle">Élection en cours...</p>
</div>
<div id="voteError" class="alert error" style="display: none;"></div>
<div id="voteSuccess" class="alert success" style="display: none;"></div>
<div id="candidatesList" class="candidates"></div>
<button class="button" onclick="handleVote()">Voter pour ce candidat</button>
<div class="nav-buttons">
<button onclick="handleLogout()">Se déconnecter</button>
<button onclick="switchView('resultsView')">Voir les résultats</button>
</div>
</div>
<!-- Results View -->
<div id="resultsView" class="view">
<div class="header">
<h1>📊 Résultats du Vote</h1>
<p id="resultsTitle">Résultats de l'élection...</p>
</div>
<div id="resultsList" class="results"></div>
<div class="nav-buttons">
<button onclick="handleLogout()">Se déconnecter</button>
<button onclick="switchView('voteView')">Retour au vote</button>
</div>
</div>
</div>
<script>
const API_URL = 'http://localhost:8000/api';
let currentToken = localStorage.getItem('token');
let currentVoter = null;
let selectedCandidate = null;
let currentElection = null;
// Initialiser l'affichage
if (currentToken) {
switchView('voteView');
loadElection();
}
function switchView(viewName) {
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
document.getElementById(viewName).classList.add('active');
}
async function handleLogin() {
const email = document.getElementById('loginEmail').value;
const password = document.getElementById('loginPassword').value;
const errorDiv = document.getElementById('loginError');
if (!email || !password) {
errorDiv.textContent = 'Veuillez remplir tous les champs';
errorDiv.style.display = 'block';
return;
}
try {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Identifiants invalides');
}
const data = await response.json();
currentToken = data.access_token;
localStorage.setItem('token', currentToken);
errorDiv.style.display = 'none';
loadElection();
switchView('voteView');
} catch (error) {
errorDiv.textContent = error.message;
errorDiv.style.display = 'block';
}
}
async function handleRegister() {
const firstName = document.getElementById('registerFirstName').value;
const lastName = document.getElementById('registerLastName').value;
const email = document.getElementById('registerEmail').value;
const citizenId = document.getElementById('registerCitizenId').value;
const password = document.getElementById('registerPassword').value;
const errorDiv = document.getElementById('registerError');
if (!firstName || !lastName || !email || !citizenId || !password) {
errorDiv.textContent = 'Veuillez remplir tous les champs';
errorDiv.style.display = 'block';
return;
}
try {
const response = await fetch(`${API_URL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
first_name: firstName,
last_name: lastName,
email,
citizen_id: citizenId,
password
})
});
if (!response.ok) {
throw new Error('Erreur lors de l\'inscription');
}
errorDiv.style.display = 'none';
alert('Inscription réussie! Connectez-vous maintenant.');
switchView('loginView');
} catch (error) {
errorDiv.textContent = error.message;
errorDiv.style.display = 'block';
}
}
async function loadElection() {
try {
const response = await fetch(`${API_URL}/elections/active`, {
headers: { 'Authorization': `Bearer ${currentToken}` }
});
if (!response.ok) {
throw new Error('Aucune élection active');
}
currentElection = await response.json();
document.getElementById('electionTitle').textContent = currentElection.name;
document.getElementById('resultsTitle').textContent = currentElection.name;
const candidatesList = document.getElementById('candidatesList');
candidatesList.innerHTML = '';
currentElection.candidates.forEach(candidate => {
const card = document.createElement('div');
card.className = 'candidate-card';
card.onclick = () => selectCandidate(candidate.id, card);
card.innerHTML = `
<h3>${candidate.name}</h3>
<p>${candidate.description || 'Aucune description'}</p>
`;
candidatesList.appendChild(card);
});
} catch (error) {
alert('Erreur: ' + error.message);
handleLogout();
}
}
function selectCandidate(candidateId, element) {
document.querySelectorAll('.candidate-card').forEach(c => c.classList.remove('selected'));
element.classList.add('selected');
selectedCandidate = candidateId;
}
async function handleVote() {
if (!selectedCandidate) {
document.getElementById('voteError').textContent = 'Veuillez sélectionner un candidat';
document.getElementById('voteError').style.display = 'block';
return;
}
// Pour ce prototype, on simule le chiffrement ElGamal
// En production, ce serait fait en JavaScript avec la cryptographie
const encryptedVote = btoa(JSON.stringify({
candidate_id: selectedCandidate,
timestamp: Date.now()
}));
try {
const response = await fetch(`${API_URL}/votes/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({
election_id: currentElection.id,
candidate_id: selectedCandidate,
encrypted_vote: encryptedVote
})
});
if (!response.ok) {
throw new Error('Erreur lors du vote');
}
const result = await response.json();
document.getElementById('voteSuccess').textContent = `Vote enregistré! Bulletin: ${result.ballot_hash.substring(0, 16)}...`;
document.getElementById('voteSuccess').style.display = 'block';
document.getElementById('voteError').style.display = 'none';
setTimeout(() => switchView('resultsView'), 2000);
} catch (error) {
document.getElementById('voteError').textContent = error.message;
document.getElementById('voteError').style.display = 'block';
}
}
async function loadResults() {
try {
const response = await fetch(`${API_URL}/elections/${currentElection.id}/results`, {
headers: { 'Authorization': `Bearer ${currentToken}` }
});
if (!response.ok) {
throw new Error('Résultats non disponibles');
}
const results = await response.json();
const resultsList = document.getElementById('resultsList');
resultsList.innerHTML = '';
results.results.forEach(result => {
const percentage = result.percentage || 0;
const item = document.createElement('div');
item.className = 'result-item';
item.innerHTML = `
<div class="result-info">
<span><strong>${result.candidate_name}</strong></span>
<span>${result.vote_count} votes (${percentage.toFixed(1)}%)</span>
</div>
<div class="result-bar">
<div class="result-fill" style="width: ${percentage}%"></div>
</div>
`;
resultsList.appendChild(item);
});
} catch (error) {
document.getElementById('resultsList').innerHTML = `<p style="color: #c00;">${error.message}</p>`;
}
}
function handleLogout() {
currentToken = null;
localStorage.removeItem('token');
switchView('loginView');
document.getElementById('loginEmail').value = '';
document.getElementById('loginPassword').value = '';
}
// Charger les résultats quand on change de vue
const observer = new MutationObserver(() => {
if (document.getElementById('resultsView').classList.contains('active')) {
loadResults();
}
});
observer.observe(document.getElementById('resultsView'), { attributes: true });
</script>
</body>
</html>