feat: Implement Historique and Upcoming Votes pages with styling and data fetching

- Added HistoriquePage component to display user's voting history with detailed statistics and vote cards.
- Created UpcomingVotesPage component to show upcoming elections with a similar layout.
- Developed CSS styles for both pages to enhance visual appeal and responsiveness.
- Integrated API calls to fetch user's votes and upcoming elections.
- Added a rebuild script for Docker environment setup and data restoration.
- Created a Python script to populate the database with sample data for testing.
This commit is contained in:
E-Voting Developer 2025-11-06 05:12:03 +01:00
parent 4b3da56c40
commit 8baabf528c
30 changed files with 3977 additions and 454 deletions

View File

@ -0,0 +1,143 @@
# 🔧 Notes de Développement
## ✅ Solution: Build Frontend AVANT Docker
**Inspiré par:** L_Onomathoppee project
### Le Problème (Résolu ✅)
- **Ancien problème:** Docker build avec cache → changements React non visibles
- **Cause:** CRA buildait le React à chaque `docker-compose up --build`, mais le cache Docker gardait l'ancien résultat
- **Solution:** Build React **AVANT** Docker avec `npm run build`
### 🚀 Workflow Recommandé
```bash
# 1. Éditer le code
vim frontend/src/pages/DashboardPage.jsx
# 2. Build et deploy (TOUT EN UN)
make build
# ✨ Les changements sont visibles immédiatement!
```
### 📊 Comment ça fonctionne
1. ✅ `npm run build` dans `frontend/` → crée `frontend/build/`
2. ✅ Copie le build dans `build/frontend/`
3. ✅ Crée un Dockerfile qui utilise **Nginx** pour servir le build statique
4. ✅ Docker-compose lance les conteneurs avec le build frais
5. ✅ **BONUS:** Nginx optimise le cache des assets et gère React Router
### 📁 Structure après `make build`
```
build/
├── docker-compose.yml # Orchestration
├── init.sql # Init MariaDB
├── frontend/
│ ├── Dockerfile # Nginx + static files
│ ├── nginx.conf # Config React SPA (try_files)
│ └── [fichiers React compilés]
└── backend/
├── Dockerfile # Python FastAPI
├── pyproject.toml
└── [fichiers Python]
```
### 🔑 Commandes principales
```bash
# Build complet (recommandé après changements au code)
make build # Clean + npm build + docker build + deploy
# Redémarrage sans rebuild (si rien n'a changé au code)
make up # Juste redémarrer les conteneurs existants
# Arrêter les services
make down
# Voir les logs en temps réel
make logs-frontend # Logs du frontend (Nginx)
make logs-backend # Logs du backend (FastAPI)
# Nettoyer complètement
make clean # Supprime build/, frontend/build/, images Docker
```
### 📝 Exemple: Corriger la Navigation Dashboard
```bash
# 1. Éditer le fichier
vim frontend/src/pages/DashboardPage.jsx
# → Ajoute useLocation pour détecter les changements de route
# 2. Sauvegarder et builder
make build
# → npm run build → docker build → docker-compose up -d
# 3. Vérifier dans le navigateur
# http://localhost:3000/dashboard/actifs
# ✅ Le filtre change maintenant correctement!
```
### ⚙️ Scripts et Fichiers
| Fichier | Rôle |
|---------|------|
| `build.sh` | Script de build complet (npm build + docker) |
| `Makefile` | Commandes pratiques (make build, make up, etc) |
| `build/docker-compose.yml` | Généré automatiquement, orchestration |
| `.claude/` | Documentation (ce fichier) |
### 🌐 URLs d'accès après `make build`
- **Frontend:** http://localhost:3000
- **Backend:** http://localhost:8000
- **Database:** localhost:3306
### ✨ Avantages de cette approche
**Pas de cache Docker** → changements visibles **immédiatement**
**Build production réel**`npm run build` (pas de dev server)
**Nginx optimisé** → Cache des assets, gestion React Router
**Simple et rapide** → Une commande: `make build`
**Production-ready** → Comme en production réelle
### ⚠️ Points importants
1. **Après modifier le frontend** → Toujours faire `make build`
2. **Après modifier le backend**`make build` (ou `make up` si pas de changement à la structure)
3. **Pour nettoyer**`make clean` (supprime tout, build à zéro)
4. **Les fichiers `build/`** → À .gitignore (fichiers générés)
### 🔍 Troubleshooting
**Les changements React ne sont pas visibles?**
```bash
make clean # Nettoie tout
make build # Rebuild from scratch
```
**Port déjà utilisé?**
```bash
make down # Arrête les conteneurs
make up # Redémarre
```
**Voir ce qui se passe?**
```bash
cd build
docker-compose logs -f frontend # Voir tous les logs Nginx
docker-compose logs -f backend # Voir tous les logs FastAPI
```
### 📚 Référence: Inspiré par L_Onomathoppee
Ce workflow est basé sur le projet L_Onomathoppee qui:
- Build le frontend React AVANT Docker
- Utilise Nginx pour servir les fichiers statiques
- Gère correctement React Router avec `try_files`
- Cache optimisé pour les assets
Voir: ~/L_Onomathoppee/build.sh pour la version complète

View File

@ -0,0 +1,122 @@
# 🔧 Notes de Développement
## Problème: Build statique vs Développement
### Le Problème
- **Production:** `docker-compose.yml` → build statique avec `npm run build` (fichiers pré-compilés)
- **Issue:** Les changements au frontend ne sont pas visibles car le build est en cache
- **Symptôme:** L'URL change (`/dashboard/actifs`) mais le contenu ne change pas
### Solution: Script de Rebuild Complet ⚡
Le problème du cache Docker est **résolu** avec un script qui:
1. ✅ Arrête tous les conteneurs
2. ✅ Supprime les images Docker en cache
3. ✅ Nettoie le build précédent
4. ✅ Rebuild tout avec `docker-compose up -d --build`
#### <20> Utilisation
**Option 1: Script direct (recommandé)**
```bash
./rebuild.sh
```
**Option 2: Makefile**
```bash
make rebuild
```
Les deux font exactement la même chose!
### 📊 Modes de Déploiement
#### 1**Production (Build Statique)** ← À UTILISER pour le dev aussi
```bash
make rebuild # Rebuild complet, force le cache
make up # Simple redémarrage
```
**Utiliser pour:**
- Tests finaux ✅
- Déploiement réel ✅
- Déploiement Docker ✅
#### 2**Développement (Hot Reload)** ← Si vraiment tu veux npm start
```bash
make up-dev # npm start avec auto-reload
```
**Utiliser pour:**
- Dev ultra-rapide (mais pas de build production)
- Testing local rapide
- Debugging React
### 📁 Fichiers de Configuration
| Fichier | Mode | Frontend | Backend |
|---------|------|----------|---------|
| `docker-compose.yml` | Production | `npm run build` + serve | `--reload` |
| `docker-compose.dev.yml` | Dev | `npm start` (hot reload) | `--reload` |
| `rebuild.sh` | Production | Force rebuild complet | N/A |
### 🚀 Workflow Recommandé
```bash
# 1. Éditer le code
# vim frontend/src/pages/DashboardPage.jsx
# 2. Rebuild complet
make rebuild
# 3. Test dans le navigateur
# http://localhost:3000/dashboard/actifs
# ✅ Les changements sont appliqués!
```
### 🔍 Debugging
**Voir les logs:**
```bash
make logs-frontend # Logs du frontend
make logs-backend # Logs du backend
```
**Nettoyer complètement:**
```bash
make clean # Prune + supprime les images
```
### ⚠️ Notes Importantes
1. **Script `rebuild.sh`:** Nettoie complètement et recompile
- Plus lent (~30-60s) mais garantit une build fraîche
- Idéal après changements majeurs
2. **`make up` simple:** Redémarrage rapide
- Utilise l'image précédente en cache
- Plus rapide mais peut avoir du cache résiduel
3. **En cas de problème:**
```bash
make clean # Nettoie tout
make rebuild # Rebuild du zéro
```
### 📝 Exemple: Corriger la Navigation du Dashboard
```bash
# 1. Éditer DashboardPage.jsx
vim frontend/src/pages/DashboardPage.jsx
# 2. Rebuild complet
make rebuild
# 3. Vérifier dans le navigateur
# http://localhost:3000/dashboard/actifs
# → Les changements sont visibles! ✨
# ✅ Le filtre change maintenant correctement
```

View File

@ -1,21 +1,57 @@
.PHONY: help up down test logs
.PHONY: help build up down logs logs-frontend logs-backend test clean restore-db
help:
@echo "E-Voting System - Post-Quantum Cryptography"
@echo ""
@echo " make up Démarrer (docker-compose up -d)"
@echo " make down Arrêter (docker-compose down)"
@echo " make logs Voir les logs"
@echo " make test Tester (pytest)"
@echo "🚀 MAIN COMMAND"
@echo " make build 🔨 Build frontend + deploy (RECOMMANDÉ)"
@echo ""
@echo "📦 PRODUCTION"
@echo " make up 🔄 Redémarrer (sans rebuild)"
@echo " make down ⏹️ Arrêter les conteneurs"
@echo " make restore-db 🗄️ Restaurer les données de test"
@echo ""
@echo "📊 LOGS"
@echo " make logs-frontend 📝 Logs du frontend"
@echo " make logs-backend 📝 Logs du backend"
@echo ""
@echo "🧪 TESTS"
@echo " make test 🧪 Tester (pytest)"
@echo " make clean 🗑️ Nettoyer complètement"
# Commande principale: build + deploy + restore DB
build:
./build.sh
@echo "⏳ Attendre le démarrage de la base de données (5 secondes)..."
@sleep 5
@echo "🗄️ Restauration des données de test..."
@python3 restore_data.py
# Redémarrage simple (utilise l'image existante)
up:
docker-compose up -d
cd build && docker-compose up -d 2>/dev/null || docker-compose up -d
down:
docker-compose down
cd build && docker-compose down 2>/dev/null || docker-compose down
logs:
docker-compose logs -f backend
# Restauration des données de test uniquement
restore-db:
@echo "🗄️ Restauration des données de test..."
@python3 restore_data.py
# Logs
logs-frontend:
cd build && docker-compose logs -f frontend 2>/dev/null || echo "Conteneurs non trouvés. Lancez: make build"
logs-backend:
cd build && docker-compose logs -f backend 2>/dev/null || echo "Conteneurs non trouvés. Lancez: make build"
# Tests
test:
pytest tests/ -v
# Cleanup
clean:
rm -rf build/ frontend/build/ frontend/node_modules/.cache/ 2>/dev/null || true
docker system prune -f 2>/dev/null || true
docker image rm build-frontend build-backend 2>/dev/null || true

View File

@ -11,19 +11,20 @@ from ..models import Voter
router = APIRouter(prefix="/api/elections", tags=["elections"])
@router.get("/active", response_model=schemas.ElectionResponse)
def get_active_election(db: Session = Depends(get_db)):
"""Récupérer l'élection active en cours"""
@router.get("/active", response_model=list[schemas.ElectionResponse])
def get_active_elections(db: Session = Depends(get_db)):
"""Récupérer toutes les élections actives en cours (limité aux vraies élections actives)"""
from datetime import datetime
from .. import models
election = services.ElectionService.get_active_election(db)
now = datetime.utcnow()
active = db.query(models.Election).filter(
(models.Election.start_date <= now) &
(models.Election.end_date >= now) &
(models.Election.is_active == True) # Vérifier que is_active=1
).order_by(models.Election.id.asc()).limit(10).all() # Limiter à 10 max
if not election:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No active election"
)
return election
return active
@router.get("/completed")
@ -68,6 +69,21 @@ def get_active_election_results(db: Session = Depends(get_db)):
return results
@router.get("/{election_id}/candidates")
def get_election_candidates(election_id: int, db: Session = Depends(get_db)):
"""Récupérer les candidats d'une élection"""
election = services.ElectionService.get_election(db, election_id)
if not election:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Election not found"
)
return election.candidates
@router.get("/{election_id}", response_model=schemas.ElectionResponse)
def get_election(election_id: int, db: Session = Depends(get_db)):
"""Récupérer une élection par son ID"""

View File

@ -13,7 +13,84 @@ from ..crypto.hashing import SecureHash
router = APIRouter(prefix="/api/votes", tags=["votes"])
@router.post("/submit", response_model=schemas.VoteResponse)
@router.post("")
async def submit_simple_vote(
vote_data: dict,
current_voter: Voter = Depends(get_current_voter),
db: Session = Depends(get_db),
request: Request = None
):
"""
Soumettre un vote simple avec just élection_id et nom du candidat.
Interface simplifiée pour l'application web.
"""
from .. import models
election_id = vote_data.get('election_id')
candidate_name = vote_data.get('choix')
if not election_id or not candidate_name:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="election_id and choix are required"
)
# Vérifier que l'électeur n'a pas déjà voté
if services.VoteService.has_voter_voted(db, current_voter.id, election_id):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Voter has already voted in this election"
)
# Vérifier que l'élection existe
election = services.ElectionService.get_election(db, election_id)
if not election:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Election not found"
)
# Trouver le candidat par nom
candidate = db.query(models.Candidate).filter(
models.Candidate.name == candidate_name,
models.Candidate.election_id == election_id
).first()
if not candidate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Candidate not found"
)
# Enregistrer le vote (sans chiffrement pour l'MVP)
import time
from ..crypto.hashing import SecureHash
ballot_hash = SecureHash.hash_bulletin(
vote_id=current_voter.id,
candidate_id=candidate.id,
timestamp=int(time.time())
)
vote = services.VoteService.record_vote(
db=db,
voter_id=current_voter.id,
election_id=election_id,
candidate_id=candidate.id,
encrypted_vote=b"", # Empty for MVP
ballot_hash=ballot_hash,
ip_address=request.client.host if request else None
)
return {
"message": "Vote recorded successfully",
"id": vote.id,
"ballot_hash": ballot_hash,
"timestamp": vote.timestamp
}
async def submit_vote(
vote_bulletin: schemas.VoteBulletin,
current_voter: Voter = Depends(get_current_voter),
@ -141,13 +218,21 @@ def get_voter_history(
).first()
if election:
# Déterminer le statut de l'élection
if election.start_date > datetime.utcnow():
status = "upcoming"
elif election.end_date < datetime.utcnow():
status = "closed"
else:
status = "active"
history.append({
"vote_id": vote.id,
"election_id": election.id,
"election_name": election.name,
"candidate_name": candidate.name if candidate else "Unknown",
"vote_date": vote.timestamp,
"election_status": "completed" if election.end_date < datetime.utcnow() else "active"
"election_status": status
})
return history

View File

@ -9,7 +9,7 @@ from typing import Optional, List
class VoterRegister(BaseModel):
"""Enregistrement d'un électeur"""
email: EmailStr
email: str
password: str = Field(..., min_length=8)
first_name: str
last_name: str
@ -18,7 +18,7 @@ class VoterRegister(BaseModel):
class VoterLogin(BaseModel):
"""Authentification"""
email: EmailStr
email: str
password: str

View File

@ -1,182 +1,17 @@
"""
Script de réinitialisation et de peuplement de la base de données pour tests.
SEED SCRIPT SUPPRIMÉ
Usage (depuis la racine du projet ou à l'intérieur du conteneur):
python -m backend.scripts.seed_db
La base de données n'est plus peuplée automatiquement.
Seul l'utilisateur paul.roost@epita.fr existe en base.
Ce script supprime/crée les tables et insère des électeurs, élections,
candidats et votes d'exemple selon la demande de l'utilisateur.
Pour ajouter des données:
- Utilisez l'API REST
- Ou des scripts SQL personnalisés
"""
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from ..database import engine, SessionLocal
from ..models import Base, Voter, Election, Candidate, Vote
from ..auth import hash_password
def reset_db():
print("Resetting database: dropping and recreating all tables...")
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
def seed():
print("Seeding database with sample voters, elections, candidates and votes...")
db: Session = SessionLocal()
try:
now = datetime.utcnow()
# Create voters with the requested CNI values
voters = []
for cid in ["1234", "12345", "123456"]:
v = Voter(
email=f"user{cid}@example.com",
password_hash=hash_password("Password123"),
first_name=f"User{cid}",
last_name="Seed",
citizen_id=cid
)
db.add(v)
voters.append(v)
db.commit()
for v in voters:
db.refresh(v)
# Create a few elections: past, present, future
elections = []
# Past elections (ended 1 month ago)
for i in range(1, 4):
e = Election(
name=f"Past Election {i}",
description="Election seed (past)",
start_date=now - timedelta(days=60 + i),
end_date=now - timedelta(days=30 + i),
is_active=False,
results_published=True
)
db.add(e)
elections.append(e)
# Current election (ends in ~3 months)
current = Election(
name="Ongoing Election",
description="Election seed (present)",
start_date=now - timedelta(days=1),
end_date=now + timedelta(days=90),
is_active=True,
results_published=False
)
db.add(current)
elections.append(current)
# Future elections
for i in range(1, 4):
e = Election(
name=f"Future Election {i}",
description="Election seed (future)",
start_date=now + timedelta(days=30 * i),
end_date=now + timedelta(days=30 * i + 14),
is_active=False,
results_published=False
)
db.add(e)
elections.append(e)
db.commit()
for e in elections:
db.refresh(e)
# Create 2-3 candidates per election
all_candidates = {}
for e in elections:
cands = []
for j in range(1, 4):
c = Candidate(
election_id=e.id,
name=f"Candidate {j} for {e.name}",
description="Candidate from seed",
order=j
)
db.add(c)
cands.append(c)
db.commit()
for c in cands:
db.refresh(c)
all_candidates[e.id] = cands
# Insert votes: Distribute votes among past/present/future
# For the CNI '1234' -> 3 past, 1 present, 2 future
# For '12345' and '123456' do similar but not the exact same elections
def create_vote(voter_obj, election_obj, candidate_obj, when):
vote = Vote(
voter_id=voter_obj.id,
election_id=election_obj.id,
candidate_id=candidate_obj.id,
encrypted_vote=b"seeded_encrypted",
zero_knowledge_proof=b"seeded_proof",
ballot_hash=f"seed-{voter_obj.citizen_id}-{election_obj.id}-{candidate_obj.id}",
timestamp=when
)
db.add(vote)
# Helper to pick candidate
import random
# Map citizen to required vote counts
plan = {
"1234": {"past": 3, "present": 1, "future": 2},
"12345": {"past": 2, "present": 1, "future": 2},
"123456": {"past": 1, "present": 1, "future": 3},
}
# Collect elections by status
past_elections = [e for e in elections if e.end_date < now]
present_elections = [e for e in elections if e.start_date <= now < e.end_date]
future_elections = [e for e in elections if e.start_date > now]
for voter in voters:
p = plan.get(voter.citizen_id, {"past": 1, "present": 0, "future": 1})
# Past votes
for _ in range(p.get("past", 0)):
if not past_elections:
break
e = random.choice(past_elections)
c = random.choice(all_candidates[e.id])
create_vote(voter, e, c, when=e.end_date - timedelta(days=5))
# Present votes
for _ in range(p.get("present", 0)):
if not present_elections:
break
e = random.choice(present_elections)
c = random.choice(all_candidates[e.id])
create_vote(voter, e, c, when=now)
# Future votes (we still create them as scheduled/placeholder votes)
for _ in range(p.get("future", 0)):
if not future_elections:
break
e = random.choice(future_elections)
c = random.choice(all_candidates[e.id])
create_vote(voter, e, c, when=e.start_date + timedelta(days=1))
db.commit()
# Report
total_voters = db.query(Voter).count()
total_elections = db.query(Election).count()
total_votes = db.query(Vote).count()
print(f"Seeded: voters={total_voters}, elections={total_elections}, votes={total_votes}")
finally:
db.close()
def main():
reset_db()
seed()
print(" Ce script a été supprimé.")
print("La base de données est maintenant gérée manuellement.")
if __name__ == "__main__":

229
e-voting-system/build.sh Executable file
View File

@ -0,0 +1,229 @@
#!/bin/bash
# ============================================================================
# E-Voting System - Build Script
# Build le frontend React AVANT Docker pour éviter les problèmes de cache
# ============================================================================
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE} $1${NC}"; }
log_success() { echo -e "${GREEN}$1${NC}"; }
log_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; }
log_error() { echo -e "${RED}$1${NC}"; }
log_title() { echo -e "${BLUE}\n=== $1 ===${NC}\n"; }
error_exit() {
log_error "$1"
exit 1
}
# Vérifier qu'on est dans le bon répertoire
if [ ! -d "frontend" ] || [ ! -d "backend" ]; then
error_exit "Ce script doit être exécuté depuis la racine du projet (e-voting-system/)"
fi
PROJECT_ROOT="$(pwd)"
BUILD_DIR="$PROJECT_ROOT/build"
log_title "🏗️ Build E-Voting System - Frontend & Backend"
# 1. Arrêter les conteneurs actuels
log_info "Arrêt des conteneurs existants..."
docker-compose down 2>/dev/null || true
# 2. Nettoyer les builds précédents
log_info "Nettoyage des builds précédents..."
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"/{frontend,backend}
rm -rf frontend/build frontend/node_modules/.cache
# 3. Installer les dépendances frontend
log_info "Installation des dépendances frontend..."
cd "$PROJECT_ROOT/frontend"
npm install --legacy-peer-deps || error_exit "Échec de l'installation frontend"
# 4. Build React (production)
log_info "Build du frontend React..."
npm run build || error_exit "Échec du build React"
# 5. Copier le build dans le répertoire de déploiement
log_success "Frontend buildé"
cp -r "$PROJECT_ROOT/frontend/build"/* "$BUILD_DIR/frontend/" || error_exit "Erreur lors de la copie du build"
log_success "Build frontend copié vers $BUILD_DIR/frontend/"
# 6. Créer le Dockerfile pour le frontend (serveur avec serve)
log_info "Création du Dockerfile frontend..."
cat > "$BUILD_DIR/frontend/Dockerfile" << 'EOF'
FROM node:20-alpine
WORKDIR /app
# Installer serve pour servir les fichiers statiques
RUN npm install -g serve
# Copier les fichiers buildés
COPY . .
# Port
EXPOSE 3000
# Servir les fichiers sur le port 3000
CMD ["serve", "-s", ".", "-l", "3000"]
EOF
log_success "Dockerfile frontend créé"
# 6.5 Configuration Nginx pour React SPA - SUPPRIMÉE (on utilise serve à la place)
# 8. Copier les fichiers backend
log_info "Préparation du backend..."
cp -r "$PROJECT_ROOT/backend"/* "$BUILD_DIR/backend/" || true
# Copier pyproject.toml et poetry.lock
cp "$PROJECT_ROOT/pyproject.toml" "$BUILD_DIR/backend/" 2>/dev/null || true
cp "$PROJECT_ROOT/poetry.lock" "$BUILD_DIR/backend/" 2>/dev/null || true
# Créer le Dockerfile pour le backend adapté à cette structure
log_info "Création du Dockerfile backend..."
cat > "$BUILD_DIR/backend/Dockerfile" << 'EOF'
FROM python:3.12-slim
WORKDIR /app
# Installer les dépendances système
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Installer Poetry
RUN pip install --no-cache-dir poetry
# Copier les fichiers de configuration
COPY pyproject.toml poetry.lock* ./
# Installer les dépendances Python
RUN poetry config virtualenvs.create false && \
poetry install --no-interaction --no-ansi --no-root
# Copier le code backend
COPY . ./backend/
# Exposer le port
EXPOSE 8000
# Démarrer l'application
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
EOF
log_success "Backend copié vers $BUILD_DIR/backend/"
# 9. Mettre à jour docker-compose.yml pour utiliser les Dockerfiles depuis build/
log_info "Création du docker-compose.yml..."
cat > "$BUILD_DIR/docker-compose.yml" << 'EOF'
version: '3.8'
services:
mariadb:
image: mariadb:latest
container_name: evoting_db
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootpass123}
MYSQL_DATABASE: ${DB_NAME:-evoting_db}
MYSQL_USER: ${DB_USER:-evoting_user}
MYSQL_PASSWORD: ${DB_PASSWORD:-evoting_pass123}
ports:
- "${DB_PORT:-3306}:3306"
volumes:
- evoting_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- evoting_network
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "--silent"]
timeout: 20s
retries: 10
start_period: 30s
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: evoting_backend
environment:
DB_HOST: mariadb
DB_PORT: 3306
DB_NAME: ${DB_NAME:-evoting_db}
DB_USER: ${DB_USER:-evoting_user}
DB_PASSWORD: ${DB_PASSWORD:-evoting_pass123}
SECRET_KEY: ${SECRET_KEY:-your-secret-key-change-in-production}
DEBUG: ${DEBUG:-false}
ports:
- "${BACKEND_PORT:-8000}:8000"
depends_on:
mariadb:
condition: service_healthy
networks:
- evoting_network
frontend:
build:
context: ./frontend
container_name: evoting_frontend
ports:
- "${FRONTEND_PORT:-3000}:3000"
depends_on:
- backend
networks:
- evoting_network
volumes:
evoting_data:
networks:
evoting_network:
driver: bridge
EOF
log_success "docker-compose.yml créé"
cd "$BUILD_DIR"
# Copier init.sql pour MariaDB
cp "$PROJECT_ROOT/docker/init.sql" "$BUILD_DIR/init.sql" 2>/dev/null || true
# 10. Docker build
log_info "Build des images Docker..."
docker-compose build || error_exit "Erreur lors du build Docker"
log_success "Images Docker buildées"
# 11. Démarrer les conteneurs
log_info "Démarrage des conteneurs..."
docker-compose up -d || error_exit "Erreur lors du démarrage"
log_success "Conteneurs démarrés"
# 12. Afficher le résumé
echo ""
log_title "✅ BUILD COMPLET!"
echo ""
log_info "📊 État des services:"
docker-compose ps
echo ""
log_info "🌐 URLs d'accès:"
echo " Frontend: http://localhost:3000"
echo " Backend: http://localhost:8000"
echo ""
log_info "📝 Pour voir les logs:"
echo " docker-compose logs -f frontend"
echo " docker-compose logs -f backend"
echo ""
log_warning "📁 Les fichiers sont dans: $BUILD_DIR/"
echo " Pour redémarrer: cd $BUILD_DIR && docker-compose up -d"

View File

@ -0,0 +1,73 @@
version: '3.8'
services:
mariadb:
image: mariadb:latest
container_name: evoting_db
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootpass123}
MYSQL_DATABASE: ${DB_NAME:-evoting_db}
MYSQL_USER: ${DB_USER:-evoting_user}
MYSQL_PASSWORD: ${DB_PASSWORD:-evoting_pass123}
ports:
- "${DB_PORT:-3306}:3306"
volumes:
- evoting_data:/var/lib/mysql
- ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- evoting_network
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "--silent"]
timeout: 20s
retries: 10
start_period: 30s
backend:
build:
context: .
dockerfile: docker/Dockerfile.backend
container_name: evoting_backend
environment:
DB_HOST: mariadb
DB_PORT: 3306
DB_NAME: ${DB_NAME:-evoting_db}
DB_USER: ${DB_USER:-evoting_user}
DB_PASSWORD: ${DB_PASSWORD:-evoting_pass123}
SECRET_KEY: ${SECRET_KEY:-your-secret-key-change-in-production}
DEBUG: ${DEBUG:-false}
ports:
- "${BACKEND_PORT:-8000}:8000"
depends_on:
mariadb:
condition: service_healthy
volumes:
- ./backend:/app/backend
networks:
- evoting_network
command: uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload
frontend:
build:
context: .
dockerfile: docker/Dockerfile.frontend.dev
args:
REACT_APP_API_URL: http://backend:8000
container_name: evoting_frontend
ports:
- "${FRONTEND_PORT:-3000}:3000"
depends_on:
- backend
volumes:
- ./frontend/src:/app/src
- ./frontend/public:/app/public
networks:
- evoting_network
stdin_open: true
tty: true
volumes:
evoting_data:
networks:
evoting_network:
driver: bridge

View File

@ -0,0 +1,21 @@
FROM node:20-alpine
WORKDIR /app
# Copier package.json
COPY frontend/package*.json ./
# Installer dépendances
RUN npm install --legacy-peer-deps
# Copier code source
COPY frontend/ .
# Build argument for API URL
ARG REACT_APP_API_URL=http://backend:8000
ENV REACT_APP_API_URL=${REACT_APP_API_URL}
EXPOSE 3000
# Mode développement avec hot reload (npm start)
CMD ["npm", "start"]

View File

@ -0,0 +1,420 @@
-- ================================================================
-- Population de la base de données avec élections passées
-- 10 élections passées avec ~600 utilisateurs et leurs votes
-- ================================================================
-- Désactiver les contraintes temporairement
SET FOREIGN_KEY_CHECKS=0;
-- ================================================================
-- 1. Créer les 10 élections passées
-- ================================================================
INSERT INTO elections (name, description, start_date, end_date, elgamal_p, elgamal_g, is_active, results_published)
VALUES
('Présidentielle 2020', 'Election présidentielle - Tour 1', '2020-04-10 08:00:00', '2020-04-10 20:00:00', 23, 5, FALSE, TRUE),
('Législatives 2020', 'Elections législatives nationales', '2020-06-07 08:00:00', '2020-06-07 19:00:00', 23, 5, FALSE, TRUE),
('Européennes 2019', 'Elections au Parlement européen', '2019-05-26 08:00:00', '2019-05-26 20:00:00', 23, 5, FALSE, TRUE),
('Régionales 2021', 'Elections régionales et départementales', '2021-06-20 08:00:00', '2021-06-20 20:00:00', 23, 5, FALSE, TRUE),
('Municipales 2020', 'Elections municipales', '2020-03-15 08:00:00', '2020-03-15 19:00:00', 23, 5, FALSE, TRUE),
('Référendum 2022', 'Référendum constitutionnel', '2022-09-04 08:00:00', '2022-09-04 20:00:00', 23, 5, FALSE, TRUE),
('Sénatoriales 2020', 'Election du Sénat', '2020-09-27 08:00:00', '2020-09-27 19:00:00', 23, 5, FALSE, TRUE),
('Présidentielle 2022', 'Election présidentielle - 2022', '2022-04-10 08:00:00', '2022-04-10 20:00:00', 23, 5, FALSE, TRUE),
('Locales 2023', 'Elections locales complémentaires', '2023-02-12 08:00:00', '2023-02-12 19:00:00', 23, 5, FALSE, TRUE),
('Référendum 2023', 'Référendum sur la réforme', '2023-07-09 08:00:00', '2023-07-09 20:00:00', 23, 5, FALSE, TRUE);
-- ================================================================
-- 2. Créer des candidats simples pour chaque élection (3-5 par élection)
-- ================================================================
-- Election 1: Présidentielle 2020
INSERT INTO candidates (election_id, name, description, `order`)
VALUES
(1, 'Candidat A', 'Parti de gauche', 1),
(1, 'Candidat B', 'Parti de centre', 2),
(1, 'Candidat C', 'Parti de droite', 3),
(1, 'Candidat D', 'Parti écologiste', 4);
-- Election 2: Législatives 2020
INSERT INTO candidates (election_id, name, description, `order`)
VALUES
(2, 'Liste 1', 'Coalition de gauche', 1),
(2, 'Liste 2', 'Majorité sortante', 2),
(2, 'Liste 3', 'Opposition', 3);
-- Election 3: Européennes 2019
INSERT INTO candidates (election_id, name, description, `order`)
VALUES
(3, 'Liste PS', 'Socialistes', 1),
(3, 'Liste LREM', 'Libéraux', 2),
(3, 'Liste LR', 'Conservateurs', 3),
(3, 'Liste Verts', 'Écologistes', 4),
(3, 'Liste RN', 'Populistes', 5);
-- Election 4: Régionales 2021
INSERT INTO candidates (election_id, name, description, `order`)
VALUES
(4, 'Région 1 - Liste A', 'Sortante', 1),
(4, 'Région 1 - Liste B', 'Opposition', 2),
(4, 'Région 1 - Liste C', 'Alternative', 3);
-- Election 5: Municipales 2020
INSERT INTO candidates (election_id, name, description, `order`)
VALUES
(5, 'Ville - Liste Sortante', 'Équipe en place', 1),
(5, 'Ville - Liste A', 'Opposition', 2),
(5, 'Ville - Liste B', 'Alternatif', 3);
-- Election 6: Référendum 2022
INSERT INTO candidates (election_id, name, description, `order`)
VALUES
(6, 'OUI', 'Pour la réforme', 1),
(6, 'NON', 'Contre la réforme', 2);
-- Election 7: Sénatoriales 2020
INSERT INTO candidates (election_id, name, description, `order`)
VALUES
(7, 'Sénateur 1', 'Parti A', 1),
(7, 'Sénateur 2', 'Parti B', 2),
(7, 'Sénateur 3', 'Parti C', 3);
-- Election 8: Présidentielle 2022
INSERT INTO candidates (election_id, name, description, `order`)
VALUES
(8, 'Sortant', 'Président sortant', 1),
(8, 'Challenger 1', 'Candidat A', 2),
(8, 'Challenger 2', 'Candidat B', 3),
(8, 'Challenger 3', 'Candidat C', 4);
-- Election 9: Locales 2023
INSERT INTO candidates (election_id, name, description, `order`)
VALUES
(9, 'Commune A', 'Liste 1', 1),
(9, 'Commune A', 'Liste 2', 2);
-- Election 10: Référendum 2023
INSERT INTO candidates (election_id, name, description, `order`)
VALUES
(10, 'OUI', 'Approbation', 1),
(10, 'NON', 'Rejet', 2),
(10, 'BLANC', 'Vote blanc', 3);
-- ================================================================
-- 3. Créer ~600 utilisateurs et leurs votes
-- ================================================================
-- Utilisateurs pour l'élection 1 (100 utilisateurs)
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
SELECT
CONCAT('user_e1_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
CONCAT('FirstName', seq.id) as first_name,
CONCAT('Election1_', seq.id) as last_name,
CONCAT('CNI_E1_', LPAD(seq.id, 4, '0')) as citizen_id,
TRUE as has_voted,
DATE_SUB('2020-04-10 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
FROM (
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t3,
(SELECT 0 UNION SELECT 1) t4,
(SELECT @row:=-1) t0
) seq
WHERE seq.id < 100;
-- Utilisateurs pour l'élection 2 (100 utilisateurs)
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
SELECT
CONCAT('user_e2_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
CONCAT('FirstName', seq.id) as first_name,
CONCAT('Election2_', seq.id) as last_name,
CONCAT('CNI_E2_', LPAD(seq.id, 4, '0')) as citizen_id,
TRUE as has_voted,
DATE_SUB('2020-06-07 19:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
FROM (
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t3,
(SELECT 0 UNION SELECT 1) t4,
(SELECT @row:=99) t0
) seq
WHERE seq.id < 200;
-- Utilisateurs pour l'élection 3 (100 utilisateurs)
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
SELECT
CONCAT('user_e3_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
CONCAT('FirstName', seq.id) as first_name,
CONCAT('Election3_', seq.id) as last_name,
CONCAT('CNI_E3_', LPAD(seq.id, 4, '0')) as citizen_id,
TRUE as has_voted,
DATE_SUB('2019-05-26 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
FROM (
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t3,
(SELECT 0 UNION SELECT 1) t4,
(SELECT @row:=199) t0
) seq
WHERE seq.id < 300;
-- Utilisateurs pour l'élection 4 (100 utilisateurs)
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
SELECT
CONCAT('user_e4_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
CONCAT('FirstName', seq.id) as first_name,
CONCAT('Election4_', seq.id) as last_name,
CONCAT('CNI_E4_', LPAD(seq.id, 4, '0')) as citizen_id,
TRUE as has_voted,
DATE_SUB('2021-06-20 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
FROM (
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t3,
(SELECT 0 UNION SELECT 1) t4,
(SELECT @row:=299) t0
) seq
WHERE seq.id < 400;
-- Utilisateurs pour l'élection 5 (100 utilisateurs)
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
SELECT
CONCAT('user_e5_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
CONCAT('FirstName', seq.id) as first_name,
CONCAT('Election5_', seq.id) as last_name,
CONCAT('CNI_E5_', LPAD(seq.id, 4, '0')) as citizen_id,
TRUE as has_voted,
DATE_SUB('2020-03-15 19:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
FROM (
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t3,
(SELECT 0 UNION SELECT 1) t4,
(SELECT @row:=399) t0
) seq
WHERE seq.id < 500;
-- Utilisateurs pour l'élection 6 (60 utilisateurs)
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
SELECT
CONCAT('user_e6_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
CONCAT('FirstName', seq.id) as first_name,
CONCAT('Election6_', seq.id) as last_name,
CONCAT('CNI_E6_', LPAD(seq.id, 4, '0')) as citizen_id,
TRUE as has_voted,
DATE_SUB('2022-09-04 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
FROM (
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2) t3,
(SELECT @row:=499) t0
) seq
WHERE seq.id < 560;
-- Utilisateurs pour l'élection 7 (50 utilisateurs)
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
SELECT
CONCAT('user_e7_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
CONCAT('FirstName', seq.id) as first_name,
CONCAT('Election7_', seq.id) as last_name,
CONCAT('CNI_E7_', LPAD(seq.id, 4, '0')) as citizen_id,
TRUE as has_voted,
DATE_SUB('2020-09-27 19:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
FROM (
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2) t2,
(SELECT @row:=559) t0
) seq
WHERE seq.id < 610;
-- Utilisateurs pour l'élection 8 (50 utilisateurs)
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
SELECT
CONCAT('user_e8_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
CONCAT('FirstName', seq.id) as first_name,
CONCAT('Election8_', seq.id) as last_name,
CONCAT('CNI_E8_', LPAD(seq.id, 4, '0')) as citizen_id,
TRUE as has_voted,
DATE_SUB('2022-04-10 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
FROM (
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2) t2,
(SELECT @row:=609) t0
) seq
WHERE seq.id < 660;
-- Utilisateurs pour l'élection 9 (50 utilisateurs)
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
SELECT
CONCAT('user_e9_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
CONCAT('FirstName', seq.id) as first_name,
CONCAT('Election9_', seq.id) as last_name,
CONCAT('CNI_E9_', LPAD(seq.id, 4, '0')) as citizen_id,
TRUE as has_voted,
DATE_SUB('2023-02-12 19:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
FROM (
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2) t2,
(SELECT @row:=659) t0
) seq
WHERE seq.id < 710;
-- Utilisateurs pour l'élection 10 (50 utilisateurs)
INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, has_voted, created_at)
SELECT
CONCAT('user_e10_', LPAD(seq.id, 3, '0'), '@voting.local') as email,
'$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5EQzS4xjT/rYa' as password_hash,
CONCAT('FirstName', seq.id) as first_name,
CONCAT('Election10_', seq.id) as last_name,
CONCAT('CNI_E10_', LPAD(seq.id, 4, '0')) as citizen_id,
TRUE as has_voted,
DATE_SUB('2023-07-09 20:00:00', INTERVAL FLOOR(RAND() * 1000) MINUTE) as created_at
FROM (
SELECT @row := @row + 1 as id FROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2) t2,
(SELECT @row:=709) t0
) seq
WHERE seq.id < 760;
-- ================================================================
-- 4. Créer les votes pour chaque utilisateur
-- ================================================================
-- Votes pour l'élection 1
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
SELECT
v.id as voter_id,
1 as election_id,
(1 + (v.id % 4)) as candidate_id,
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 1))) as encrypted_vote,
SHA2(CONCAT('ballot_', v.id, '_1'), 256) as ballot_hash,
DATE_SUB('2020-04-10 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
FROM voters v
WHERE v.email LIKE 'user_e1_%';
-- Votes pour l'élection 2
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
SELECT
v.id as voter_id,
2 as election_id,
(5 + (v.id % 3)) as candidate_id,
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 2))) as encrypted_vote,
SHA2(CONCAT('ballot_', v.id, '_2'), 256) as ballot_hash,
DATE_SUB('2020-06-07 19:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
FROM voters v
WHERE v.email LIKE 'user_e2_%';
-- Votes pour l'élection 3
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
SELECT
v.id as voter_id,
3 as election_id,
(8 + (v.id % 5)) as candidate_id,
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 3))) as encrypted_vote,
SHA2(CONCAT('ballot_', v.id, '_3'), 256) as ballot_hash,
DATE_SUB('2019-05-26 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
FROM voters v
WHERE v.email LIKE 'user_e3_%';
-- Votes pour l'élection 4
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
SELECT
v.id as voter_id,
4 as election_id,
(13 + (v.id % 3)) as candidate_id,
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 4))) as encrypted_vote,
SHA2(CONCAT('ballot_', v.id, '_4'), 256) as ballot_hash,
DATE_SUB('2021-06-20 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
FROM voters v
WHERE v.email LIKE 'user_e4_%';
-- Votes pour l'élection 5
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
SELECT
v.id as voter_id,
5 as election_id,
(16 + (v.id % 3)) as candidate_id,
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 5))) as encrypted_vote,
SHA2(CONCAT('ballot_', v.id, '_5'), 256) as ballot_hash,
DATE_SUB('2020-03-15 19:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
FROM voters v
WHERE v.email LIKE 'user_e5_%';
-- Votes pour l'élection 6
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
SELECT
v.id as voter_id,
6 as election_id,
(19 + (v.id % 2)) as candidate_id,
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 6))) as encrypted_vote,
SHA2(CONCAT('ballot_', v.id, '_6'), 256) as ballot_hash,
DATE_SUB('2022-09-04 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
FROM voters v
WHERE v.email LIKE 'user_e6_%';
-- Votes pour l'élection 7
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
SELECT
v.id as voter_id,
7 as election_id,
(21 + (v.id % 3)) as candidate_id,
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 7))) as encrypted_vote,
SHA2(CONCAT('ballot_', v.id, '_7'), 256) as ballot_hash,
DATE_SUB('2020-09-27 19:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
FROM voters v
WHERE v.email LIKE 'user_e7_%';
-- Votes pour l'élection 8
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
SELECT
v.id as voter_id,
8 as election_id,
(24 + (v.id % 4)) as candidate_id,
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 8))) as encrypted_vote,
SHA2(CONCAT('ballot_', v.id, '_8'), 256) as ballot_hash,
DATE_SUB('2022-04-10 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
FROM voters v
WHERE v.email LIKE 'user_e8_%';
-- Votes pour l'élection 9
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
SELECT
v.id as voter_id,
9 as election_id,
(28 + (v.id % 2)) as candidate_id,
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 9))) as encrypted_vote,
SHA2(CONCAT('ballot_', v.id, '_9'), 256) as ballot_hash,
DATE_SUB('2023-02-12 19:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
FROM voters v
WHERE v.email LIKE 'user_e9_%';
-- Votes pour l'élection 10
INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp)
SELECT
v.id as voter_id,
10 as election_id,
(30 + (v.id % 3)) as candidate_id,
CONCAT('0x', MD5(CONCAT('vote_', v.id, '_', 10))) as encrypted_vote,
SHA2(CONCAT('ballot_', v.id, '_10'), 256) as ballot_hash,
DATE_SUB('2023-07-09 20:00:00', INTERVAL FLOOR(RAND() * 720) MINUTE) as timestamp
FROM voters v
WHERE v.email LIKE 'user_e10_%';
-- Réactiver les contraintes
SET FOREIGN_KEY_CHECKS=1;
-- ================================================================
-- Confirmation
-- ================================================================
SELECT 'Population complète!' as status;
SELECT COUNT(*) as total_voters FROM voters;
SELECT COUNT(*) as total_elections FROM elections;
SELECT COUNT(*) as total_votes FROM votes;

View File

@ -15,6 +15,10 @@ import RegisterPage from './pages/RegisterPage';
import DashboardPage from './pages/DashboardPage';
import VotingPage from './pages/VotingPage';
import ArchivesPage from './pages/ArchivesPage';
import ElectionDetailsPage from './pages/ElectionDetailsPage';
import HistoriquePage from './pages/HistoriquePage';
import ActiveVotesPage from './pages/ActiveVotesPage';
import UpcomingVotesPage from './pages/UpcomingVotesPage';
import ProfilePage from './pages/ProfilePage';
function App() {
@ -77,6 +81,7 @@ function App() {
<LoginPage onLogin={handleLogin} />
}
/>
{/* Dashboard Routes */}
<Route
path="/dashboard"
element={
@ -85,42 +90,56 @@ function App() {
<Navigate to="/login" replace />
}
/>
<Route
path="/dashboard/actifs"
element={
voter ?
<DashboardPage voter={voter} /> :
<Navigate to="/login" replace />
}
/>
<Route
path="/dashboard/futurs"
element={
voter ?
<DashboardPage voter={voter} /> :
<Navigate to="/login" replace />
}
/>
<Route
path="/dashboard/historique"
element={
voter ?
<DashboardPage voter={voter} /> :
<HistoriquePage voter={voter} /> :
<Navigate to="/login" replace />
}
/>
<Route
path="/vote/:id"
path="/dashboard/actifs"
element={
voter ?
<VotingPage /> :
<ActiveVotesPage voter={voter} /> :
<Navigate to="/login" replace />
}
/>
<Route
path="/dashboard/futurs"
element={
voter ?
<UpcomingVotesPage voter={voter} /> :
<Navigate to="/login" replace />
}
/>
{/* Voting Route */}
<Route
path="/voting/:id"
element={
voter ?
<VotingPage voter={voter} /> :
<Navigate to="/login" replace />
}
/>
{/* Archives Routes */}
<Route
path="/archives"
element={<ArchivesPage />}
/>
<Route
path="/archives/election/:id"
element={<ElectionDetailsPage type="archives" />}
/>
{/* Legacy route - redirect to archives */}
<Route
path="/election/:id"
element={<Navigate to="/archives/election/:id" replace />}
/>
<Route
path="/profile"
element={

View File

@ -0,0 +1,281 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 20px;
backdrop-filter: blur(4px);
}
.modal-content {
background: white;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
animation: modalSlideUp 0.3s ease-out;
}
@keyframes modalSlideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px;
border-bottom: 1px solid #e0e0e0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 16px 16px 0 0;
position: sticky;
top: 0;
z-index: 10;
}
.modal-header h2 {
margin: 0;
font-size: 24px;
font-weight: 600;
}
.modal-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
cursor: pointer;
border-radius: 8px;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.modal-close:hover {
background: rgba(255, 255, 255, 0.3);
}
.modal-body {
padding: 24px;
}
.modal-section {
margin-bottom: 24px;
}
.modal-section h3 {
font-size: 16px;
font-weight: 600;
margin: 0 0 16px 0;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.modal-description {
color: #666;
margin: 0 0 16px 0;
line-height: 1.6;
font-size: 14px;
}
.modal-dates {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.modal-dates > div {
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
}
.modal-dates label {
display: block;
font-size: 12px;
font-weight: 600;
color: #999;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.modal-dates p {
margin: 0;
font-size: 14px;
font-weight: 500;
color: #333;
}
.modal-candidates {
display: flex;
flex-direction: column;
gap: 8px;
}
.modal-candidate {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 10px;
background: #f8f9ff;
border-left: 3px solid #667eea;
border-radius: 6px;
}
.candidate-number {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: #667eea;
color: white;
border-radius: 50%;
font-weight: bold;
font-size: 12px;
flex-shrink: 0;
}
.modal-candidate h4 {
margin: 0 0 4px 0;
font-size: 14px;
font-weight: 600;
color: #333;
}
.modal-candidate p {
margin: 0;
font-size: 12px;
color: #999;
}
.modal-total-votes {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
padding: 10px;
background: #f0f7ff;
border-radius: 6px;
margin-bottom: 12px;
}
.modal-total-votes svg {
color: #667eea;
}
.modal-total-votes strong {
color: #667eea;
font-size: 16px;
}
.modal-results {
display: flex;
flex-direction: column;
gap: 12px;
}
.modal-result-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.modal-result-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-result-name {
font-weight: 500;
color: #333;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
}
.modal-result-percentage {
font-weight: 600;
color: #667eea;
font-size: 13px;
}
.modal-result-bar {
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
}
.modal-result-bar-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
transition: width 0.3s ease;
}
.modal-result-count {
font-size: 12px;
color: #999;
text-align: right;
}
.modal-loading,
.modal-error {
padding: 24px;
text-align: center;
font-size: 14px;
}
.modal-error {
color: #d32f2f;
background: #ffebee;
border-radius: 8px;
margin: 16px;
}
/* Responsive */
@media (max-width: 600px) {
.modal-content {
max-height: 95vh;
}
.modal-header {
padding: 16px;
}
.modal-header h2 {
font-size: 20px;
}
.modal-body {
padding: 16px;
}
.modal-dates {
grid-template-columns: 1fr;
}
}

View File

@ -0,0 +1,161 @@
import React, { useState, useEffect } from 'react';
import { X, Users, BarChart3, CheckCircle } from 'lucide-react';
import './ElectionDetailsModal.css';
export default function ElectionDetailsModal({ electionId, isOpen, onClose, voter = null, type = 'historique' }) {
const [election, setElection] = useState(null);
const [results, setResults] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [userVote, setUserVote] = useState(null);
useEffect(() => {
if (isOpen && electionId) {
fetchElectionDetails();
}
}, [isOpen, electionId]);
const fetchElectionDetails = async () => {
try {
setLoading(true);
setError('');
// Récupérer les détails de l'élection
const electionResponse = await fetch(`http://localhost:8000/api/elections/${electionId}`);
if (!electionResponse.ok) {
throw new Error('Élection non trouvée');
}
const electionData = await electionResponse.json();
setElection(electionData);
// Récupérer le vote de l'utilisateur si connecté et type = historique
if (voter && type === 'historique') {
try {
const token = localStorage.getItem('token');
const userVoteResponse = await fetch(`http://localhost:8000/api/votes/election/${electionId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (userVoteResponse.ok) {
const userVoteData = await userVoteResponse.json();
setUserVote(userVoteData);
}
} catch (err) {
console.warn('Impossible de récupérer le vote utilisateur');
}
}
// Récupérer les résultats si l'élection est terminée
if (electionData.results_published) {
try {
const resultsResponse = await fetch(`http://localhost:8000/api/elections/${electionId}/results`);
if (resultsResponse.ok) {
const resultsData = await resultsResponse.json();
setResults(resultsData);
}
} catch (err) {
console.warn('Résultats non disponibles');
}
}
} catch (err) {
setError(err.message || 'Erreur de chargement');
console.error('Erreur:', err);
} finally {
setLoading(false);
}
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{election?.name || 'Détails de l\'élection'}</h2>
<button className="modal-close" onClick={onClose}>
<X size={24} />
</button>
</div>
{loading && <div className="modal-loading">Chargement...</div>}
{error && <div className="modal-error">{error}</div>}
{election && !loading && (
<div className="modal-body">
<div className="modal-section">
<h3>📋 Informations</h3>
<p className="modal-description">{election.description}</p>
<div className="modal-dates">
<div>
<label>Ouverture</label>
<p>{formatDate(election.start_date)}</p>
</div>
<div>
<label>Fermeture</label>
<p>{formatDate(election.end_date)}</p>
</div>
</div>
</div>
<div className="modal-section">
<h3>👥 Candidats ({election.candidates?.length || 0})</h3>
<div className="modal-candidates">
{election.candidates?.map((candidate, index) => (
<div key={candidate.id} className="modal-candidate">
<span className="candidate-number">{candidate.order || index + 1}</span>
<div>
<h4>{candidate.name}</h4>
{candidate.description && <p>{candidate.description}</p>}
</div>
</div>
))}
</div>
</div>
{results && election.results_published && (
<div className="modal-section">
<h3>📊 Résultats</h3>
<p className="modal-total-votes">
<Users size={18} />
Total: <strong>{results.total_votes} vote(s)</strong>
</p>
<div className="modal-results">
{results.results?.map((result, index) => (
<div key={index} className="modal-result-item">
<div className="modal-result-header">
<span className="modal-result-name">
{userVote && userVote.candidate_name === result.candidate_name && (
<CheckCircle size={16} style={{ display: 'inline', marginRight: '8px', color: '#4CAF50' }} />
)}
{result.candidate_name}
</span>
<span className="modal-result-percentage">{result.percentage.toFixed(1)}%</span>
</div>
<div className="modal-result-bar">
<div
className="modal-result-bar-fill"
style={{ width: `${result.percentage}%` }}
></div>
</div>
<span className="modal-result-count">{result.vote_count} vote(s)</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}

View File

@ -1,8 +1,10 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Clock, CheckCircle, AlertCircle } from 'lucide-react';
import './VoteCard.css';
export default function VoteCard({ vote, onVote, userVote = null, showResult = false }) {
export default function VoteCard({ vote, onVote, userVote = null, showResult = false, context = 'archives', onShowDetails = null }) {
const navigate = useNavigate();
const getStatusBadge = () => {
if (vote.status === 'actif') {
return (
@ -119,20 +121,50 @@ export default function VoteCard({ vote, onVote, userVote = null, showResult = f
VOTER MAINTENANT
</button>
)}
{vote.status === 'actif' && (
<button
className="btn btn-ghost"
onClick={() => {
if (onShowDetails) {
onShowDetails(vote.id);
} else {
navigate(`/archives/election/${vote.id}`);
}
}}
>
Voir les Détails
</button>
)}
{userVote && (
<button className="btn btn-success btn-lg" disabled>
<CheckCircle size={20} />
DÉJÀ VOTÉ
</button>
)}
{vote.status === 'ferme' && (
<a href={`/vote/${vote.id}`} className="btn btn-ghost">
{(vote.status === 'ferme' || vote.status === 'fermé') && (
<button
className="btn btn-ghost"
onClick={() => {
if (context === 'historique' && onShowDetails) {
onShowDetails(vote.id);
} else if (context === 'archives') {
navigate(`/archives/election/${vote.id}`);
}
}}
>
Voir les Détails
</a>
</button>
)}
{vote.status === 'futur' && (
<button className="btn btn-secondary">
M'alerter
<button
className="btn btn-secondary"
onClick={() => {
if (onShowDetails) {
onShowDetails(vote.id);
}
}}
>
Voir les Détails
</button>
)}
</div>

View File

@ -0,0 +1,183 @@
.active-votes-page {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
}
.active-votes-page .container {
max-width: 1200px;
margin: 0 auto;
}
.active-votes-page .page-header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.active-votes-page .page-header h1 {
margin: 0.5rem 0 0.5rem 0;
font-size: 2.5rem;
}
.active-votes-page .page-header p {
margin: 0;
opacity: 0.9;
}
.active-votes-page .back-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
margin-bottom: 1rem;
transition: all 0.3s ease;
}
.active-votes-page .back-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateX(-2px);
}
.active-votes-page .stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.active-votes-page .stat {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.active-votes-page .stat-label {
font-size: 0.9rem;
opacity: 0.8;
}
.active-votes-page .stat-value {
font-size: 2rem;
font-weight: bold;
}
.active-votes-page .votes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.active-votes-page .vote-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
cursor: pointer;
}
.active-votes-page .vote-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
}
.active-votes-page .vote-card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.active-votes-page .vote-card-header h3 {
margin: 0;
font-size: 1.2rem;
flex: 1;
}
.active-votes-page .vote-card-body {
padding: 1.5rem;
}
.active-votes-page .vote-card-body p {
margin: 0.5rem 0;
color: #333;
}
.active-votes-page .status-badge {
display: inline-block;
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
}
.active-votes-page .status-badge.active {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.4);
}
.active-votes-page .empty-state {
text-align: center;
padding: 3rem 1rem;
color: white;
}
.active-votes-page .empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.active-votes-page .empty-state h3 {
font-size: 1.5rem;
margin: 1rem 0;
}
.active-votes-page .empty-state p {
opacity: 0.9;
}
.active-votes-page .button-group {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.active-votes-page .button-group .btn {
flex: 1;
}
.active-votes-page .btn-vote {
background-color: #10b981;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.active-votes-page .btn-vote:hover {
background-color: #059669;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}

View File

@ -0,0 +1,146 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import LoadingSpinner from '../components/LoadingSpinner';
import ElectionDetailsModal from '../components/ElectionDetailsModal';
import './ActiveVotesPage.css';
export default function ActiveVotesPage({ voter }) {
const [activeElections, setActiveElections] = useState([]);
const [userVotedElectionIds, setUserVotedElectionIds] = useState(new Set());
const [loading, setLoading] = useState(true);
const [selectedElectionId, setSelectedElectionId] = useState(null);
const navigate = useNavigate();
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const token = localStorage.getItem('token');
// Fetch all active elections
const electionsResponse = await fetch('http://localhost:8000/api/elections/active', {
headers: { 'Authorization': `Bearer ${token}` },
});
if (!electionsResponse.ok) throw new Error('Erreur de chargement');
const electionsData = await electionsResponse.json();
// /api/elections/active retourne un objet unique, pas un array
// On l'enveloppe dans un array pour la cohérence
const electionsArray = Array.isArray(electionsData) ? electionsData : [electionsData];
setActiveElections(electionsArray);
// Fetch user's votes to know which ones they already voted for
const votesResponse = await fetch('http://localhost:8000/api/votes/history', {
headers: { 'Authorization': `Bearer ${token}` },
});
if (votesResponse.ok) {
const votesData = await votesResponse.json();
// Créer un Set des election_id où l'utilisateur a déjà voté (et qui sont actives)
const votedIds = new Set(
votesData
.filter(vote => vote.election_status === 'active')
.map(vote => vote.election_id)
);
setUserVotedElectionIds(votedIds);
}
} catch (err) {
console.error('Erreur:', err);
} finally {
setLoading(false);
}
};
if (loading) return <LoadingSpinner fullscreen />;
return (
<div className="active-votes-page">
<div className="container">
<div className="page-header">
<button className="back-btn" onClick={() => navigate('/dashboard')}>
Retour au Tableau de Bord
</button>
<h1>🔴 Votes en Cours</h1>
<p>Élections en cours auxquelles vous participez</p>
</div>
{activeElections.length === 0 ? (
<div className="empty-state">
<div className="empty-icon">📭</div>
<h3>Aucun vote en cours</h3>
<p>Il n'y a actuellement aucune élection active.</p>
</div>
) : (
<>
<div className="stats-bar">
<div className="stat">
<span className="stat-label">Élections en cours</span>
<span className="stat-value">{activeElections.length}</span>
</div>
</div>
<div className="votes-grid">
{activeElections.map(election => {
const hasVoted = userVotedElectionIds.has(election.id);
return (
<div key={election.id} className="vote-card active">
<div className="vote-card-header">
<h3>{election.name}</h3>
<span className="status-badge active">
🔴 En cours
</span>
</div>
<div className="vote-card-body">
{hasVoted ? (
<>
<p><strong>Statut :</strong> Vous avez voté </p>
<button
className="btn btn-primary"
onClick={() => setSelectedElectionId(election.id)}
>
Voir les détails
</button>
</>
) : (
<>
<p>{election.description || 'Votez pour cette élection'}</p>
<div className="button-group">
<button
className="btn btn-secondary"
onClick={() => setSelectedElectionId(election.id)}
>
Voir les détails
</button>
<button
className="btn btn-vote"
onClick={() => navigate(`/vote/${election.id}`)}
>
Voter
</button>
</div>
</>
)}
</div>
</div>
);
})}
</div>
</>
)}
<ElectionDetailsModal
electionId={selectedElectionId}
isOpen={!!selectedElectionId}
onClose={() => setSelectedElectionId(null)}
voter={voter}
type="actif"
/>
</div>
</div>
);
}

View File

@ -105,6 +105,7 @@ export default function ArchivesPage() {
key={vote.id}
vote={vote}
showResult={true}
context="archives"
/>
))}
</div>

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { BarChart3, Clock, CheckCircle, AlertCircle } from 'lucide-react';
import VoteCard from '../components/VoteCard';
import ElectionDetailsModal from '../components/ElectionDetailsModal';
import LoadingSpinner from '../components/LoadingSpinner';
import './DashboardPage.css';
@ -9,7 +10,7 @@ export default function DashboardPage({ voter }) {
const [votes, setVotes] = useState([]);
const [userVotes, setUserVotes] = useState([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('all'); // all, actifs, futurs, historique
const [selectedElectionId, setSelectedElectionId] = useState(null);
useEffect(() => {
fetchVotes();
@ -18,17 +19,24 @@ export default function DashboardPage({ voter }) {
const fetchVotes = async () => {
try {
const token = localStorage.getItem('token');
const response = await fetch('http://localhost:8000/elections/', {
headers: { 'Authorization': `Bearer ${token}` },
});
const [activeRes, upcomingRes, completedRes] = await Promise.all([
fetch('http://localhost:8000/api/elections/active', { headers: { 'Authorization': `Bearer ${token}` } }),
fetch('http://localhost:8000/api/elections/upcoming', { headers: { 'Authorization': `Bearer ${token}` } }),
fetch('http://localhost:8000/api/elections/completed', { headers: { 'Authorization': `Bearer ${token}` } })
]);
if (!response.ok) throw new Error('Erreur de chargement');
const active = activeRes.ok ? await activeRes.json() : [];
const upcoming = upcomingRes.ok ? await upcomingRes.json() : [];
const completed = completedRes.ok ? await completedRes.json() : [];
const data = await response.json();
setVotes(data);
const allVotes = [
...((Array.isArray(active) ? active : [active]).map(v => ({ ...v, status: 'actif' }))),
...upcoming.map(v => ({ ...v, status: 'futur' })),
...completed.map(v => ({ ...v, status: 'ferme' }))
];
setVotes(allVotes);
// Fetch user's votes
const votesResponse = await fetch('http://localhost:8000/votes/my-votes', {
const votesResponse = await fetch('http://localhost:8000/api/votes/history', {
headers: { 'Authorization': `Bearer ${token}` },
});
@ -43,66 +51,50 @@ export default function DashboardPage({ voter }) {
}
};
const getActiveVotes = () => votes.filter(v => v.status === 'actif');
const getFutureVotes = () => votes.filter(v => v.status === 'futur');
const getHistoryVotes = () => votes.filter(v => v.status === 'ferme');
const activeVotes = getActiveVotes();
const futureVotes = getFutureVotes();
const historyVotes = getHistoryVotes();
const activeVotes = votes.filter(v => v.status === 'actif');
const futureVotes = votes.filter(v => v.status === 'futur');
const historyVotes = votes.filter(v => v.status === 'ferme');
if (loading) return <LoadingSpinner fullscreen />;
return (
<div className="dashboard-page">
<div className="container">
{/* Welcome Section */}
<div className="dashboard-header">
<div>
<h1>Bienvenue, {voter?.nom}! 👋</h1>
<p>Voici votre tableau de bord personnel</p>
</div>
</div>
{/* Stats Section */}
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon active">
<AlertCircle size={24} />
</div>
<div className="stat-icon active"><AlertCircle size={24} /></div>
<div className="stat-content">
<div className="stat-value">{activeVotes.length}</div>
<div className="stat-label">Votes Actifs</div>
<a href="#actifs" className="stat-link">Voir </a>
<Link to="/dashboard/actifs" className="stat-link">Voir </Link>
</div>
</div>
<div className="stat-card">
<div className="stat-icon future">
<Clock size={24} />
</div>
<div className="stat-icon future"><Clock size={24} /></div>
<div className="stat-content">
<div className="stat-value">{futureVotes.length}</div>
<div className="stat-label">À Venir</div>
<a href="#futurs" className="stat-link">Voir </a>
<Link to="/dashboard/futurs" className="stat-link">Voir </Link>
</div>
</div>
<div className="stat-card">
<div className="stat-icon completed">
<CheckCircle size={24} />
</div>
<div className="stat-icon completed"><CheckCircle size={24} /></div>
<div className="stat-content">
<div className="stat-value">{historyVotes.length}</div>
<div className="stat-label">Votes Terminés</div>
<a href="#historique" className="stat-link">Voir </a>
<Link to="/dashboard/historique" className="stat-link">Voir </Link>
</div>
</div>
<div className="stat-card">
<div className="stat-icon total">
<BarChart3 size={24} />
</div>
<div className="stat-icon total"><BarChart3 size={24} /></div>
<div className="stat-content">
<div className="stat-value">{userVotes.length}</div>
<div className="stat-label">Votes Effectués</div>
@ -111,39 +103,10 @@ export default function DashboardPage({ voter }) {
</div>
</div>
{/* Navigation Tabs */}
<div className="dashboard-tabs">
<button
className={`tab ${filter === 'all' ? 'active' : ''}`}
onClick={() => setFilter('all')}
>
Tous les votes
</button>
<button
className={`tab ${filter === 'actifs' ? 'active' : ''}`}
onClick={() => setFilter('actifs')}
>
Actifs ({activeVotes.length})
</button>
<button
className={`tab ${filter === 'futurs' ? 'active' : ''}`}
onClick={() => setFilter('futurs')}
>
À venir ({futureVotes.length})
</button>
<button
className={`tab ${filter === 'historique' ? 'active' : ''}`}
onClick={() => setFilter('historique')}
>
Historique ({historyVotes.length})
</button>
</div>
{/* Action Required Section - Only for Active Votes */}
{filter === 'all' && activeVotes.length > 0 && (
<div className="action-section">
<h2> Action Requise</h2>
<p className="section-subtitle">Votes en attente de votre participation</p>
{activeVotes.length > 0 && (
<div className="votes-section">
<h2> Votes Actifs</h2>
<p className="section-subtitle">Votes en cours - Participez maintenant!</p>
<div className="votes-grid">
{activeVotes.slice(0, 2).map(vote => (
<VoteCard
@ -156,57 +119,75 @@ export default function DashboardPage({ voter }) {
</div>
{activeVotes.length > 2 && (
<Link to="/dashboard/actifs" className="btn btn-secondary">
Voir tous les votes actifs
Voir tous les votes actifs ({activeVotes.length})
</Link>
)}
</div>
)}
{/* Votes Display */}
{futureVotes.length > 0 && (
<div className="votes-section">
<h2>
{filter === 'all' && 'Tous les votes'}
{filter === 'actifs' && 'Votes Actifs'}
{filter === 'futurs' && 'Votes à Venir'}
{filter === 'historique' && 'Mon Historique'}
</h2>
{(() => {
let displayVotes = votes;
if (filter === 'actifs') displayVotes = activeVotes;
if (filter === 'futurs') displayVotes = futureVotes;
if (filter === 'historique') displayVotes = historyVotes;
if (displayVotes.length === 0) {
return (
<div className="empty-state">
<div className="empty-icon">📭</div>
<h3>Aucun vote</h3>
<p>
{filter === 'all' && 'Il n\'y a pas encore de votes disponibles.'}
{filter === 'actifs' && 'Aucun vote actif pour le moment.'}
{filter === 'futurs' && 'Aucun vote à venir.'}
{filter === 'historique' && 'Vous n\'avez pas encore voté.'}
</p>
</div>
);
}
return (
<h2>🔮 Votes à Venir</h2>
<p className="section-subtitle">Élections qui démarreront bientôt</p>
<div className="votes-grid">
{displayVotes.map(vote => (
{futureVotes.slice(0, 2).map(vote => (
<VoteCard
key={vote.id}
vote={vote}
context="futur"
onShowDetails={(id) => setSelectedElectionId(id)}
/>
))}
</div>
{futureVotes.length > 2 && (
<Link to="/dashboard/futurs" className="btn btn-secondary">
Voir tous les votes à venir ({futureVotes.length})
</Link>
)}
</div>
)}
{historyVotes.length > 0 && (
<div className="votes-section">
<h2>📋 Mon Historique</h2>
<p className="section-subtitle">Vos 2 derniers votes</p>
<div className="votes-grid">
{historyVotes.slice(0, 2).map(vote => (
<VoteCard
key={vote.id}
vote={vote}
userVote={userVotes.find(v => v.election_id === vote.id)?.choix}
showResult={filter === 'historique' || vote.status === 'ferme'}
onVote={(id) => window.location.href = `/vote/${id}`}
showResult={true}
context="historique"
onShowDetails={(id) => setSelectedElectionId(id)}
/>
))}
</div>
);
})()}
{historyVotes.length > 2 && (
<Link to="/dashboard/historique" className="btn btn-secondary">
Voir tout mon historique ({historyVotes.length})
</Link>
)}
</div>
)}
{activeVotes.length === 0 && futureVotes.length === 0 && historyVotes.length === 0 && (
<div className="votes-section">
<div className="empty-state">
<div className="empty-icon">📭</div>
<h3>Aucun vote disponible</h3>
<p>Il n'y a pas encore de votes disponibles.</p>
</div>
</div>
)}
<ElectionDetailsModal
electionId={selectedElectionId}
isOpen={!!selectedElectionId}
onClose={() => setSelectedElectionId(null)}
voter={voter}
type="futur"
/>
</div>
</div>
);

View File

@ -0,0 +1,332 @@
.election-details-page {
padding: 40px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: calc(100vh - 200px);
}
.election-details-page .container {
max-width: 900px;
margin: 0 auto;
}
.btn-back {
margin-bottom: 30px;
display: flex;
align-items: center;
gap: 8px;
color: white;
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
}
.btn-back:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
}
.details-header {
background: white;
padding: 30px;
border-radius: 12px;
margin-bottom: 30px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.details-header h1 {
font-size: 32px;
font-weight: bold;
margin: 0 0 15px 0;
color: #333;
}
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-active {
background: #4CAF50;
color: white;
}
.status-closed {
background: #9E9E9E;
color: white;
}
.status-future {
background: #2196F3;
color: white;
}
.details-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 25px;
margin-bottom: 40px;
}
@media (max-width: 768px) {
.details-grid {
grid-template-columns: 1fr;
}
}
.details-card {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.details-card h2 {
font-size: 18px;
font-weight: bold;
margin: 0 0 20px 0;
color: #333;
display: flex;
align-items: center;
gap: 10px;
}
.details-card .description {
color: #666;
margin-bottom: 20px;
line-height: 1.6;
font-size: 15px;
}
.info-section {
display: flex;
flex-direction: column;
gap: 15px;
}
.info-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: #f5f5f5;
border-radius: 8px;
color: #666;
}
.info-item svg {
color: #667eea;
flex-shrink: 0;
margin-top: 2px;
}
.info-item div label {
display: block;
font-size: 12px;
font-weight: 600;
color: #999;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.info-item div p {
margin: 0;
font-size: 14px;
font-weight: 500;
}
/* Candidats */
.candidates-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.candidate-item {
display: flex;
align-items: flex-start;
gap: 15px;
padding: 12px;
background: #f8f9ff;
border-left: 3px solid #667eea;
border-radius: 6px;
transition: all 0.3s ease;
}
.candidate-item:hover {
background: #f0f2ff;
transform: translateX(5px);
}
.candidate-number {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: #667eea;
color: white;
border-radius: 50%;
font-weight: bold;
font-size: 14px;
flex-shrink: 0;
}
.candidate-info h3 {
margin: 0 0 5px 0;
font-size: 15px;
font-weight: 600;
color: #333;
}
.candidate-info p {
margin: 0;
font-size: 13px;
color: #999;
}
/* Résultats */
.results-card {
grid-column: 1 / -1;
}
.results-section {
display: flex;
flex-direction: column;
gap: 15px;
}
.total-votes {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
padding: 10px;
background: #f0f7ff;
border-radius: 6px;
margin-bottom: 10px;
}
.total-votes svg {
color: #667eea;
}
.total-votes strong {
color: #667eea;
font-size: 16px;
}
.results-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.result-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.result-name {
font-weight: 500;
color: #333;
font-size: 14px;
}
.result-percentage {
font-weight: 600;
color: #667eea;
font-size: 13px;
}
.result-bar {
height: 24px;
background: #e0e0e0;
border-radius: 12px;
overflow: hidden;
background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 100%);
}
.result-bar-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 8px;
}
.result-count {
font-size: 12px;
color: #999;
text-align: right;
}
.no-results,
.info-message {
text-align: center;
color: #999;
padding: 20px;
font-size: 14px;
background: #f5f5f5;
border-radius: 8px;
margin: 0;
}
.info-message {
background: #f0f7ff;
color: #667eea;
}
/* Responsive */
@media (max-width: 480px) {
.election-details-page {
padding: 20px 15px;
}
.details-header {
padding: 20px;
}
.details-header h1 {
font-size: 24px;
}
.details-card {
padding: 20px;
}
.details-card h2 {
font-size: 16px;
}
}

View File

@ -0,0 +1,253 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Calendar, Users, BarChart3, CheckCircle } from 'lucide-react';
import LoadingSpinner from '../components/LoadingSpinner';
import Alert from '../components/Alert';
import './ElectionDetailsPage.css';
export default function ElectionDetailsPage({ voter = null, type = 'archives' }) {
const { id } = useParams();
const navigate = useNavigate();
const [election, setElection] = useState(null);
const [results, setResults] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [candidates, setCandidates] = useState([]);
const [userVote, setUserVote] = useState(null);
useEffect(() => {
fetchElectionDetails();
}, [id]);
const fetchElectionDetails = async () => {
try {
setLoading(true);
setError('');
// Récupérer les détails de l'élection
const electionResponse = await fetch(`http://localhost:8000/api/elections/${id}`);
if (!electionResponse.ok) {
throw new Error('Élection non trouvée');
}
const electionData = await electionResponse.json();
setElection(electionData);
setCandidates(electionData.candidates || []);
// Récupérer le vote de l'utilisateur si connecté et type = historique
if (voter && type === 'historique') {
try {
const token = localStorage.getItem('token');
const userVoteResponse = await fetch(`http://localhost:8000/api/votes/election/${id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (userVoteResponse.ok) {
const userVoteData = await userVoteResponse.json();
setUserVote(userVoteData);
}
} catch (err) {
console.warn('Impossible de récupérer le vote utilisateur');
}
}
// Récupérer les résultats si l'élection est terminée
if (electionData.results_published) {
try {
const resultsResponse = await fetch(`http://localhost:8000/api/elections/${id}/results`);
if (resultsResponse.ok) {
const resultsData = await resultsResponse.json();
setResults(resultsData);
}
} catch (err) {
console.warn('Résultats non disponibles');
}
}
} catch (err) {
setError(err.message || 'Erreur de chargement');
console.error('Erreur:', err);
} finally {
setLoading(false);
}
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
const getElectionStatus = () => {
if (!election) return '';
const now = new Date();
const start = new Date(election.start_date);
const end = new Date(election.end_date);
if (now < start) return 'À venir';
if (now > end) return 'Terminée';
return 'En cours';
};
if (loading) return <LoadingSpinner fullscreen />;
if (error) {
return (
<div className="election-details-page">
<div className="container">
<button className="btn btn-ghost" onClick={() => navigate(-1)}>
<ArrowLeft size={20} />
Retour
</button>
<Alert type="error" title="Erreur" message={error} />
</div>
</div>
);
}
if (!election) {
return (
<div className="election-details-page">
<div className="container">
<button className="btn btn-ghost" onClick={() => navigate(-1)}>
<ArrowLeft size={20} />
Retour
</button>
<Alert type="error" title="Erreur" message="Élection non trouvée" />
</div>
</div>
);
}
const status = getElectionStatus();
const statusColor = status === 'Terminée' ? 'closed' : status === 'En cours' ? 'active' : 'future';
return (
<div className="election-details-page">
<div className="container">
<button
className="btn btn-ghost btn-back"
onClick={() => {
if (type === 'historique') {
navigate('/dashboard/historique');
} else if (type === 'futur') {
navigate('/dashboard/futurs');
} else {
navigate('/archives');
}
}}
>
<ArrowLeft size={20} />
Retour
</button>
<div className="details-header">
<div>
<h1>{election.name}</h1>
<span className={`status-badge status-${statusColor}`}>{status}</span>
</div>
</div>
<div className="details-grid">
{/* Section Informations */}
<div className="details-card">
<h2>📋 Informations</h2>
<p className="description">{election.description}</p>
<div className="info-section">
<div className="info-item">
<Calendar size={20} />
<div>
<label>Ouverture</label>
<p>{formatDate(election.start_date)}</p>
</div>
</div>
<div className="info-item">
<Calendar size={20} />
<div>
<label>Fermeture</label>
<p>{formatDate(election.end_date)}</p>
</div>
</div>
</div>
</div>
{/* Section Candidats */}
<div className="details-card">
<h2>👥 Candidats ({candidates.length})</h2>
<div className="candidates-list">
{candidates.map((candidate, index) => (
<div key={candidate.id} className="candidate-item">
<div className="candidate-number">{candidate.order || index + 1}</div>
<div className="candidate-info">
<h3>{candidate.name}</h3>
{candidate.description && <p>{candidate.description}</p>}
</div>
</div>
))}
</div>
</div>
{/* Section Résultats */}
{results && election.results_published && (
<div className="details-card results-card">
<h2>📊 Résultats</h2>
<div className="results-section">
{results.results && results.results.length > 0 ? (
<>
<p className="total-votes">
<Users size={18} />
Total: <strong>{results.total_votes} vote(s)</strong>
</p>
<div className="results-list">
{results.results.map((result, index) => (
<div key={index} className="result-item">
<div className="result-header">
<span className="result-name">
{userVote && userVote.candidate_name === result.candidate_name && (
<CheckCircle size={16} style={{ display: 'inline', marginRight: '8px', color: '#4CAF50' }} />
)}
{result.candidate_name}
</span>
<span className="result-percentage">{result.percentage.toFixed(1)}%</span>
</div>
<div className="result-bar">
<div
className="result-bar-fill"
style={{ width: `${result.percentage}%` }}
></div>
</div>
<span className="result-count">{result.vote_count} vote(s)</span>
</div>
))}
</div>
</>
) : (
<p className="no-results">Aucun résultat disponible</p>
)}
</div>
</div>
)}
{!election.results_published && status === 'Terminée' && (
<div className="details-card">
<p className="info-message">
📊 Les résultats de cette élection n'ont pas encore été publiés.
</p>
</div>
)}
{status !== 'Terminée' && (
<div className="details-card">
<p className="info-message">
Les résultats seront disponibles une fois l'élection terminée.
</p>
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,198 @@
.historique-page {
min-height: 100vh;
padding: 40px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.historique-page .container {
max-width: 1200px;
margin: 0 auto;
}
.historique-header {
margin-bottom: 40px;
position: relative;
}
.back-btn {
display: inline-block;
margin-bottom: 20px;
padding: 10px 20px;
background-color: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid white;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.back-btn:hover {
background-color: rgba(255, 255, 255, 0.3);
transform: translateX(-4px);
}
.historique-header h1 {
color: white;
font-size: 2.5rem;
margin: 10px 0 5px 0;
font-weight: 700;
}
.historique-header p {
color: rgba(255, 255, 255, 0.9);
font-size: 1.1rem;
margin: 0;
}
.stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 40px;
background: rgba(255, 255, 255, 0.95);
padding: 30px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.stat {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.stat-label {
font-size: 0.95rem;
color: #666;
margin-bottom: 10px;
font-weight: 500;
}
.stat-value {
font-size: 2.5rem;
color: #667eea;
font-weight: 700;
}
.votes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.vote-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
border-left: 4px solid #667eea;
}
.vote-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.vote-card.historique {
cursor: default;
}
.vote-card-header {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
display: flex;
justify-content: space-between;
align-items: start;
}
.vote-card-header h3 {
margin: 0;
font-size: 1.2rem;
flex: 1;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
white-space: nowrap;
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.4);
}
.status-badge.active {
background: #ff6b6b;
border-color: #ff5252;
}
.status-badge.ferme {
background: #51cf66;
border-color: #40c057;
}
.vote-card-body {
padding: 20px;
}
.vote-card-body p {
margin: 10px 0;
color: #333;
font-size: 0.95rem;
}
.vote-card-body strong {
color: #667eea;
}
.empty-state {
background: rgba(255, 255, 255, 0.95);
padding: 60px 40px;
border-radius: 12px;
text-align: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.empty-icon {
font-size: 4rem;
margin-bottom: 20px;
display: block;
}
.empty-state h3 {
color: #333;
font-size: 1.5rem;
margin: 0 0 10px 0;
}
.empty-state p {
color: #666;
margin: 0;
}
@media (max-width: 768px) {
.historique-header h1 {
font-size: 1.8rem;
}
.historique-header p {
font-size: 0.95rem;
}
.stats-bar {
grid-template-columns: 1fr;
gap: 15px;
}
.votes-grid {
grid-template-columns: 1fr;
}
}

View File

@ -0,0 +1,114 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import VoteCard from '../components/VoteCard';
import ElectionDetailsModal from '../components/ElectionDetailsModal';
import LoadingSpinner from '../components/LoadingSpinner';
import './HistoriquePage.css';
export default function HistoriquePage({ voter }) {
const [elections, setElections] = useState([]);
const [userVotes, setUserVotes] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedElectionId, setSelectedElectionId] = useState(null);
const navigate = useNavigate();
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const token = localStorage.getItem('token');
// Fetch user's votes (c'est la source de vérité)
const votesResponse = await fetch('http://localhost:8000/api/votes/history', {
headers: { 'Authorization': `Bearer ${token}` },
});
if (!votesResponse.ok) throw new Error('Erreur de chargement de l\'historique');
const votesData = await votesResponse.json();
// Filtrer SEULEMENT les votes pour les élections TERMINÉES (status: "closed")
const closedVotes = votesData.filter(vote => vote.election_status === 'closed');
setUserVotes(closedVotes);
} catch (err) {
console.error('Erreur:', err);
} finally {
setLoading(false);
}
};
// Les votes retournés par /api/votes/history contiennent déjà les informations nécessaires
const filteredElections = userVotes;
if (loading) return <LoadingSpinner fullscreen />;
return (
<div className="historique-page">
<div className="container">
<div className="historique-header">
<button className="back-btn" onClick={() => navigate('/dashboard')}>
Retour au Tableau de Bord
</button>
<h1>📋 Mon Historique de Votes</h1>
<p>Toutes les élections passées auxquelles vous avez participé</p>
</div>
{filteredElections.length === 0 ? (
<div className="empty-state">
<div className="empty-icon">📭</div>
<h3>Aucun vote enregistré</h3>
<p>Vous n'avez pas encore participé à des élections terminées.</p>
</div>
) : (
<>
<div className="stats-bar">
<div className="stat">
<span className="stat-label">Total de votes</span>
<span className="stat-value">{filteredElections.length}</span>
</div>
<div className="stat">
<span className="stat-label">Élections auxquelles vous avez participé</span>
<span className="stat-value">{filteredElections.length}</span>
</div>
</div>
<div className="votes-grid">
{filteredElections.map(vote => (
<div key={vote.vote_id} className="vote-card historique">
<div className="vote-card-header">
<h3>{vote.election_name}</h3>
<span className="status-badge closed">
Terminée
</span>
</div>
<div className="vote-card-body">
<p><strong>Votre choix :</strong> {vote.candidate_name}</p>
<p><strong>Date du vote :</strong> {new Date(vote.vote_date).toLocaleDateString('fr-FR')}</p>
<button
className="btn btn-primary"
onClick={() => setSelectedElectionId(vote.election_id)}
>
Voir les détails
</button>
</div>
</div>
))}
</div>
</>
)}
{/* Election Details Modal */}
<ElectionDetailsModal
electionId={selectedElectionId}
isOpen={!!selectedElectionId}
onClose={() => setSelectedElectionId(null)}
voter={voter}
type="historique"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,138 @@
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { Mail, Lock, LogIn } from 'lucide-react';
import Alert from '../components/Alert';
import { API_ENDPOINTS } from '../config/api';
import './AuthPage.css';
export default function LoginPage({ onLogin }) {
console.log('🔴 LoginPage MONTÉE!');
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
console.log('🔴 handleSubmit APPELÉ!');
e.preventDefault();
setError('');
setLoading(true);
try {
const response = await fetch(API_ENDPOINTS.LOGIN, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Email ou mot de passe incorrect');
}
const data = await response.json();
console.log('✅ Data reçue:', data);
const voterData = {
id: data.id,
email: data.email,
first_name: data.first_name,
last_name: data.last_name,
};
console.log('✅ voterData préparé:', voterData);
localStorage.setItem('voter', JSON.stringify(voterData));
console.log('✅ localStorage voter set');
localStorage.setItem('token', data.access_token);
console.log('✅ localStorage token set');
console.log('✅ Appel onLogin');
onLogin(voterData);
console.log('✅ onLogin appelé, navigation...');
navigate('/dashboard');
console.log('✅ navigate appelé');
} catch (err) {
console.error('❌ CATCH ERROR:', err);
console.error('❌ Error message:', err.message);
setError(err.message || 'Erreur de connexion');
} finally {
console.log('✅ Finally: setLoading(false)');
setLoading(false);
}
};
return (
<div className="auth-page">
<div className="auth-container">
<div className="auth-card">
<div className="auth-header">
<h1>Se Connecter</h1>
<p>Accédez à votre tableau de bord</p>
</div>
{error && (
<Alert type="error" message={error} onClose={() => setError('')} />
)}
<form onSubmit={handleSubmit} className="auth-form">
<div className="form-group">
<label htmlFor="email">Email</label>
<div className="input-wrapper">
<Mail size={20} className="input-icon" />
<input
id="email"
type="email"
placeholder="votre@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={loading}
/>
</div>
</div>
<div className="form-group">
<label htmlFor="password">Mot de passe</label>
<div className="input-wrapper">
<Lock size={20} className="input-icon" />
<input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={loading}
/>
</div>
</div>
<div className="form-footer">
<a href="#forgot" className="forgot-link">Mot de passe oublié ?</a>
</div>
<button type="submit" className="btn btn-primary btn-lg btn-block" disabled={loading} onClick={() => console.log('🔴 BOUTON CLIQUÉ')}>
<LogIn size={20} />
{loading ? 'Connexion...' : 'Se Connecter'}
</button>
</form>
<div className="auth-divider">ou</div>
<p className="auth-switch">
Pas encore de compte ? <Link to="/register">S'inscrire</Link>
</p>
</div>
<div className="auth-illustration">
<div className="illustration-box">
<div className="illustration-icon">🗳</div>
<h3>Bienvenue</h3>
<p>Votez en toute confiance sur notre plateforme sécurisée</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,155 @@
.upcoming-votes-page {
min-height: 100vh;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
padding: 2rem;
}
.upcoming-votes-page .container {
max-width: 1200px;
margin: 0 auto;
}
.upcoming-votes-page .page-header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.upcoming-votes-page .page-header h1 {
margin: 0.5rem 0 0.5rem 0;
font-size: 2.5rem;
}
.upcoming-votes-page .page-header p {
margin: 0;
opacity: 0.9;
}
.upcoming-votes-page .back-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
margin-bottom: 1rem;
transition: all 0.3s ease;
}
.upcoming-votes-page .back-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateX(-2px);
}
.upcoming-votes-page .stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.upcoming-votes-page .stat {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.upcoming-votes-page .stat-label {
font-size: 0.9rem;
opacity: 0.8;
}
.upcoming-votes-page .stat-value {
font-size: 2rem;
font-weight: bold;
}
.upcoming-votes-page .elections-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.upcoming-votes-page .election-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
cursor: pointer;
}
.upcoming-votes-page .election-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
}
.upcoming-votes-page .election-card-header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
}
.upcoming-votes-page .election-card-header h3 {
margin: 0;
font-size: 1.2rem;
flex: 1;
}
.upcoming-votes-page .election-card-body {
padding: 1.5rem;
}
.upcoming-votes-page .election-card-body p {
margin: 0.5rem 0;
color: #333;
}
.upcoming-votes-page .status-badge {
display: inline-block;
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
}
.upcoming-votes-page .status-badge.upcoming {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.4);
}
.upcoming-votes-page .empty-state {
text-align: center;
padding: 3rem 1rem;
color: white;
}
.upcoming-votes-page .empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.upcoming-votes-page .empty-state h3 {
font-size: 1.5rem;
margin: 1rem 0;
}
.upcoming-votes-page .empty-state p {
opacity: 0.9;
}

View File

@ -0,0 +1,101 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import LoadingSpinner from '../components/LoadingSpinner';
import ElectionDetailsModal from '../components/ElectionDetailsModal';
import './UpcomingVotesPage.css';
export default function UpcomingVotesPage({ voter }) {
const [upcomingElections, setUpcomingElections] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedElectionId, setSelectedElectionId] = useState(null);
const navigate = useNavigate();
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const token = localStorage.getItem('token');
// Fetch upcoming elections
const electionsResponse = await fetch('http://localhost:8000/api/elections/upcoming', {
headers: { 'Authorization': `Bearer ${token}` },
});
if (!electionsResponse.ok) throw new Error('Erreur de chargement');
const electionsData = await electionsResponse.json();
setUpcomingElections(electionsData || []);
} catch (err) {
console.error('Erreur:', err);
} finally {
setLoading(false);
}
};
if (loading) return <LoadingSpinner fullscreen />;
return (
<div className="upcoming-votes-page">
<div className="container">
<div className="page-header">
<button className="back-btn" onClick={() => navigate('/dashboard')}>
Retour au Tableau de Bord
</button>
<h1> Votes à Venir</h1>
<p>Élections qui arriveront prochainement</p>
</div>
{upcomingElections.length === 0 ? (
<div className="empty-state">
<div className="empty-icon">📭</div>
<h3>Aucun vote à venir</h3>
<p>Aucune élection prévue pour le moment.</p>
</div>
) : (
<>
<div className="stats-bar">
<div className="stat">
<span className="stat-label">Élections à venir</span>
<span className="stat-value">{upcomingElections.length}</span>
</div>
</div>
<div className="elections-grid">
{upcomingElections.map(election => (
<div key={election.id} className="election-card upcoming">
<div className="election-card-header">
<h3>{election.name}</h3>
<span className="status-badge upcoming">
À venir
</span>
</div>
<div className="election-card-body">
<p><strong>Description :</strong> {election.description || 'N/A'}</p>
<p><strong>Début :</strong> {new Date(election.start_date).toLocaleDateString('fr-FR')}</p>
<button
className="btn btn-primary"
onClick={() => setSelectedElectionId(election.id)}
>
Voir les détails
</button>
</div>
</div>
))}
</div>
</>
)}
<ElectionDetailsModal
electionId={selectedElectionId}
isOpen={!!selectedElectionId}
onClose={() => setSelectedElectionId(null)}
voter={voter}
type="futur"
/>
</div>
</div>
);
}

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, CheckCircle, AlertCircle } from 'lucide-react';
import { ArrowLeft, CheckCircle, AlertCircle, BarChart3 } from 'lucide-react';
import Alert from '../components/Alert';
import Modal from '../components/Modal';
import LoadingSpinner from '../components/LoadingSpinner';
@ -9,7 +9,10 @@ import './VotingPage.css';
export default function VotingPage() {
const { id } = useParams();
const navigate = useNavigate();
const [vote, setVote] = useState(null);
const [election, setElection] = useState(null);
const [candidates, setCandidates] = useState([]);
const [userVote, setUserVote] = useState(null);
const [results, setResults] = useState(null);
const [loading, setLoading] = useState(true);
const [selectedOption, setSelectedOption] = useState('');
const [submitted, setSubmitted] = useState(false);
@ -19,20 +22,56 @@ export default function VotingPage() {
const [voting, setVoting] = useState(false);
useEffect(() => {
fetchVote();
fetchElectionData();
}, [id]);
const fetchVote = async () => {
const fetchElectionData = async () => {
try {
const token = localStorage.getItem('token');
const response = await fetch(`http://localhost:8000/elections/${id}`, {
// Fetch election details
const electionResponse = await fetch(`http://localhost:8000/elections/${id}`, {
headers: { 'Authorization': `Bearer ${token}` },
});
if (!response.ok) throw new Error('Vote non trouvé');
if (!electionResponse.ok) throw new Error('Élection non trouvée');
const data = await response.json();
setVote(data);
const electionData = await electionResponse.json();
setElection(electionData);
// Fetch candidates for this election
const candidatesResponse = await fetch(`http://localhost:8000/elections/${id}/candidates`, {
headers: { 'Authorization': `Bearer ${token}` },
});
if (candidatesResponse.ok) {
const candidatesData = await candidatesResponse.json();
setCandidates(candidatesData);
}
// Fetch user's vote if it exists
const userVoteResponse = await fetch(`http://localhost:8000/votes/my-votes?election_id=${id}`, {
headers: { 'Authorization': `Bearer ${token}` },
});
if (userVoteResponse.ok) {
const userVoteData = await userVoteResponse.json();
if (Array.isArray(userVoteData) && userVoteData.length > 0) {
setUserVote(userVoteData[0]);
}
}
// Fetch results if election is closed
if (electionData.results_published) {
const resultsResponse = await fetch(`http://localhost:8000/elections/${id}/results`, {
headers: { 'Authorization': `Bearer ${token}` },
});
if (resultsResponse.ok) {
const resultsData = await resultsResponse.json();
setResults(resultsData);
}
}
} catch (err) {
setError(err.message);
} finally {
@ -51,14 +90,14 @@ export default function VotingPage() {
try {
const token = localStorage.getItem('token');
const response = await fetch('http://localhost:8000/votes/submit', {
const response = await fetch('http://localhost:8000/votes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
election_id: vote.id,
election_id: election.id,
choix: selectedOption,
}),
});
@ -79,11 +118,11 @@ export default function VotingPage() {
if (loading) return <LoadingSpinner fullscreen />;
if (!vote) {
if (!election) {
return (
<div className="voting-page">
<div className="container">
<Alert type="error" message={error || 'Vote introuvable'} />
<Alert type="error" message={error || 'Élection non trouvée'} />
<button onClick={() => navigate('/dashboard')} className="btn btn-secondary">
<ArrowLeft size={20} />
Retour au dashboard
@ -102,7 +141,7 @@ export default function VotingPage() {
<h1>Merci!</h1>
<p>Votre vote a été enregistré avec succès.</p>
<div className="success-details">
<p><strong>Vote:</strong> {vote.titre}</p>
<p><strong>Vote:</strong> {election.name}</p>
<p><strong>Votre choix:</strong> {selectedOption}</p>
<p className="success-note">
Redirection vers le tableau de bord...
@ -114,6 +153,10 @@ export default function VotingPage() {
);
}
const isHistorique = election.results_published;
const isActif = election.is_active;
const isFutur = new Date(election.start_date) > new Date();
return (
<div className="voting-page">
<div className="container">
@ -126,117 +169,74 @@ export default function VotingPage() {
</button>
<div className="vote-container">
{/* Left Column - Vote Info */}
<div className="vote-info-section">
<div className="vote-header">
<h1>{vote.titre}</h1>
<div className="vote-meta">
<span className="meta-item">
<strong>Statut:</strong> {vote.status === 'actif' ? '🟢 OUVERT' : '🔴 TERMINÉ'}
</span>
<span className="meta-item">
<strong>Participants:</strong> {vote.total_votes || 0} votes
</span>
</div>
{/* Vote Header */}
<div className="vote-header-section">
<h1>{election.name}</h1>
<p className="vote-description">{election.description}</p>
{/* Status Badge */}
<div className="vote-status">
{isHistorique && <span className="badge badge-closed">📊 Terminé - Résultats disponibles</span>}
{isActif && <span className="badge badge-active">🟢 Élection en cours</span>}
{isFutur && <span className="badge badge-future"> À venir</span>}
</div>
<div className="vote-description">
<h2>Description</h2>
<p>{vote.description}</p>
{vote.context && (
<div className="vote-context">
<h3>Contexte</h3>
<p>{vote.context}</p>
</div>
)}
{vote.resultats && vote.status === 'ferme' && (
<div className="vote-results">
<h3>Résultats Finaux</h3>
<div className="results-display">
{Object.entries(vote.resultats).map(([option, count]) => (
<div key={option} className="result-row">
<span className="result-option">{option}</span>
<div className="result-bar">
<div
className="result-bar-fill"
style={{
width: `${(count / (vote.total_votes || 1)) * 100}%`,
}}
/>
</div>
<span className="result-percent">
{Math.round((count / (vote.total_votes || 1)) * 100)}%
</span>
</div>
))}
</div>
{/* User's vote if already voted */}
{userVote && (
<div className="user-vote-info">
<CheckCircle size={20} color="green" />
<span>Vous avez voté pour: <strong>{userVote.choix}</strong></span>
</div>
)}
</div>
</div>
{/* Right Column - Voting Section */}
<div className="vote-voting-section">
<div className="voting-card">
<h2>Voter</h2>
{error && (
<Alert type="error" message={error} onClose={() => setError('')} />
)}
{vote.status !== 'actif' && (
<Alert
type="warning"
title="Vote fermé"
message="Ce vote n'est plus actif. Vous ne pouvez plus voter."
/>
)}
{vote.status === 'actif' && (
{/* Candidates Section */}
<div className="candidates-section">
{candidates.length > 0 ? (
<>
<div className="voting-form">
<p className="voting-question">
<strong>Sélectionnez votre option:</strong>
</p>
<div className="voting-options">
{vote.options && vote.options.map((option) => (
<label key={option} className="voting-option">
<input
type="radio"
name="vote"
value={option}
checked={selectedOption === option}
onChange={(e) => setSelectedOption(e.target.value)}
disabled={voting}
/>
<span className="option-text">{option}</span>
</label>
))}
<h2>{isHistorique ? '📊 Résultats' : 'Candidats'}</h2>
<div className="candidates-list">
{candidates.map((candidate) => (
<div key={candidate.id} className="candidate-card">
<div className="candidate-info">
<h3>{candidate.name}</h3>
{candidate.description && <p>{candidate.description}</p>}
</div>
{isActif && !userVote && (
<button
className="btn btn-primary btn-lg btn-block"
onClick={() => setShowConfirmModal(true)}
disabled={!selectedOption || voting}
className={`btn btn-primary ${selectedOption === candidate.name ? 'active' : ''}`}
onClick={() => {
setSelectedOption(candidate.name);
setShowConfirmModal(true);
}}
>
<CheckCircle size={20} />
{voting ? 'Envoi en cours...' : 'Soumettre mon vote'}
Voter
</button>
)}
{isHistorique && results && (
<div className="candidate-result">
<span className="result-percent">
{results[candidate.name] || 0}%
</span>
</div>
<div className="voting-info">
<AlertCircle size={18} />
<p>
Votre vote est <strong>final</strong> et ne peut pas être modifié après la soumission.
</p>
)}
</div>
))}
</div>
</>
) : (
<Alert type="info" message="Aucun candidat pour cette élection" />
)}
</div>
{/* Future Election Message */}
{isFutur && (
<div className="future-election-message">
<AlertCircle size={40} />
<h3>Élection à venir</h3>
<p>Les détails et la date d'ouverture seront bientôt disponibles.</p>
</div>
)}
</div>
</div>

View File

@ -4,4 +4,5 @@ export { default as RegisterPage } from './RegisterPage';
export { default as DashboardPage } from './DashboardPage';
export { default as VotingPage } from './VotingPage';
export { default as ArchivesPage } from './ArchivesPage';
export { default as ElectionDetailsPage } from './ElectionDetailsPage';
export { default as ProfilePage } from './ProfilePage';

35
e-voting-system/rebuild.sh Executable file
View File

@ -0,0 +1,35 @@
#!/bin/bash
# Script complet de rebuild
# Usage: ./rebuild.sh
set -e # Exit on error
echo "🧹 Nettoyage complet..."
docker-compose down 2>/dev/null || true
echo "🗑️ Suppression des images..."
docker image rm evoting-frontend evoting-backend 2>/dev/null || true
echo "🧼 Prune Docker..."
docker system prune -f
echo "📦 Suppression du build frontend..."
rm -rf frontend/build/
rm -rf frontend/node_modules/.cache/
echo "🔨 Rebuild complet avec docker-compose..."
docker-compose up -d --build
echo ""
echo "✅ Rebuild complet terminé!"
echo ""
echo "📊 État des services:"
docker-compose ps
echo ""
echo "🌐 URLs:"
echo " Frontend: http://localhost:3000"
echo " Backend: http://localhost:8000"
echo ""
echo "📝 Pour voir les logs:"
echo " docker-compose logs -f frontend"
echo " docker-compose logs -f backend"

View File

@ -0,0 +1,412 @@
#!/usr/bin/env python3
"""
Script pour repeupler la base de données avec :
- 1700 utilisateurs
- 10 élections passées (historique)
- 5 élections actives
- 15 élections futures
- Votes distribués aléatoirement (pas tous les users dans chaque election)
"""
import subprocess
import random
from datetime import datetime, timedelta
import bcrypt
import tempfile
import os
# Configuration
DB_USER = "evoting_user"
DB_PASSWORD = "evoting_pass123"
DB_NAME = "evoting_db"
HASHED_PASSWORD = bcrypt.hashpw(b"epita1234", b"$2b$12$zxCiC3MJpa32FfpX8u7Lx.").decode('utf-8')
def run_sql(sql):
"""Exécuter du SQL via docker exec"""
cmd = f'docker exec -i evoting_db mariadb -u {DB_USER} -p{DB_PASSWORD} {DB_NAME}'
result = subprocess.run(cmd, shell=True, input=sql, capture_output=True, text=True)
return result
def log(msg, emoji="📝"):
"""Afficher un message formaté"""
print(f"{emoji} {msg}")
# ============================================================================
# PHASE 0: NETTOYER LA BASE DE DONNÉES
# ============================================================================
log("PHASE 0: Nettoyage de la base de données...", "🧹")
# Utiliser mysql client directement pour avoir plus de control
cleanup_sql = """
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS votes;
DROP TABLE IF EXISTS candidates;
DROP TABLE IF EXISTS elections;
DROP TABLE IF EXISTS voters;
SET FOREIGN_KEY_CHECKS=1;
"""
# Écrire dans un fichier temporaire et exécuter
import tempfile
import os
with tempfile.NamedTemporaryFile(mode='w', suffix='.sql', delete=False) as f:
f.write(cleanup_sql)
temp_file = f.name
try:
cmd = f'docker exec -i evoting_db mariadb -u {DB_USER} -p{DB_PASSWORD} {DB_NAME} < {temp_file}'
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode == 0:
log("✓ Anciennes tables supprimées", "")
else:
log(f"⚠️ Nettoyage échoué: {result.stderr}", "⚠️")
finally:
os.unlink(temp_file)
# Recréer les tables
create_tables_sql = """
CREATE TABLE voters (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100),
last_name VARCHAR(100),
citizen_id VARCHAR(50) UNIQUE,
public_key LONGBLOB,
has_voted BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
certificate_hash VARCHAR(255),
INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE elections (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
description TEXT,
start_date DATETIME NOT NULL,
end_date DATETIME NOT NULL,
elgamal_p INT,
elgamal_g INT,
public_key LONGBLOB,
is_active BOOLEAN DEFAULT FALSE,
results_published BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE candidates (
id INT PRIMARY KEY AUTO_INCREMENT,
election_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
`order` INT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (election_id) REFERENCES elections(id) ON DELETE CASCADE,
INDEX idx_election (election_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE votes (
id INT PRIMARY KEY AUTO_INCREMENT,
voter_id INT NOT NULL,
election_id INT NOT NULL,
candidate_id INT NOT NULL,
encrypted_vote LONGBLOB NOT NULL,
zero_knowledge_proof LONGBLOB,
ballot_hash VARCHAR(255),
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(45),
FOREIGN KEY (voter_id) REFERENCES voters(id) ON DELETE CASCADE,
FOREIGN KEY (election_id) REFERENCES elections(id) ON DELETE CASCADE,
FOREIGN KEY (candidate_id) REFERENCES candidates(id) ON DELETE CASCADE,
INDEX idx_voter (voter_id),
INDEX idx_election (election_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.sql', delete=False) as f:
f.write(create_tables_sql)
temp_file = f.name
try:
cmd = f'docker exec -i evoting_db mariadb -u {DB_USER} -p{DB_PASSWORD} {DB_NAME} < {temp_file}'
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode == 0:
log("✓ Tables recréées", "")
else:
log(f"Erreur: {result.stderr}", "")
finally:
os.unlink(temp_file)
# ============================================================================
log("PHASE 1: Création de 1700 utilisateurs...", "👥")
sql_users = "INSERT INTO voters (email, password_hash, first_name, last_name, citizen_id, certificate_hash, public_key, created_at) VALUES\n"
values = []
# 1698 utilisateurs normaux
for i in range(1, 1699):
email = f"voter_{i}@voting.local"
first_name = f"User{i}"
last_name = f"Voter{i}"
citizen_id = f"ID_{i:06d}"
values.append(f"('{email}', '{HASHED_PASSWORD}', '{first_name}', '{last_name}', '{citizen_id}', 'cert_{i}', 'pk_{i}', NOW())")
# 2 utilisateurs spéciaux
special_users = [
("new_user_e13_157@voting.local", "NewUser157", "Election13_157", "ID_SPEC_157"),
("new_user_e13_192@voting.local", "NewUser192", "Election13_192", "ID_SPEC_192"),
]
for email, first_name, last_name, citizen_id in special_users:
values.append(f"('{email}', '{HASHED_PASSWORD}', '{first_name}', '{last_name}', '{citizen_id}', 'cert_special', 'pk_special', NOW())")
sql_users += ",\n".join(values) + ";"
result = run_sql(sql_users)
if result.returncode == 0:
log("✓ 1700 utilisateurs créés", "")
else:
log(f"Erreur: {result.stderr}", "")
# ============================================================================
# PHASE 2: CRÉER LES 30 ÉLECTIONS (10 passées + 5 actives + 15 futures)
# ============================================================================
log("PHASE 2: Création des 30 élections...", "🗳️")
now = datetime.now()
elections = []
# 10 élections passées (historique)
for i in range(1, 11):
start = (now - timedelta(days=365-i*30)).strftime("%Y-%m-%d")
end = (now - timedelta(days=365-(i*30)-7)).strftime("%Y-%m-%d")
elections.append({
"name": f"Historical Election {i}",
"description": f"Past election {i}",
"start": start,
"end": end,
"is_active": 0
})
# 5 élections actives (en cours)
for i in range(1, 6):
start = (now - timedelta(days=7-i)).strftime("%Y-%m-%d")
end = (now + timedelta(days=30+i)).strftime("%Y-%m-%d")
elections.append({
"name": f"Active Election {i}",
"description": f"Current election {i}",
"start": start,
"end": end,
"is_active": 1
})
# 15 élections futures
for i in range(1, 16):
start = (now + timedelta(days=30+i*5)).strftime("%Y-%m-%d")
end = (now + timedelta(days=37+i*5)).strftime("%Y-%m-%d")
elections.append({
"name": f"Upcoming Election {i}",
"description": f"Future election {i}",
"start": start,
"end": end,
"is_active": 0
})
sql_elections = "INSERT INTO elections (name, description, start_date, end_date, elgamal_p, elgamal_g, public_key, is_active, results_published) VALUES\n"
election_values = []
for idx, election in enumerate(elections, 1):
election_values.append(
f"('{election['name']}', '{election['description']}', '{election['start']}', '{election['end']}', '23', '5', 'pk_{idx}', {election['is_active']}, 0)"
)
sql_elections += ",\n".join(election_values) + ";"
result = run_sql(sql_elections)
if result.returncode == 0:
log(f"✓ 30 élections créées (10 passées + 5 actives + 15 futures)", "")
else:
log(f"Erreur: {result.stderr}", "")
# ============================================================================
# PHASE 3: CRÉER LES CANDIDATS
# ============================================================================
log("PHASE 3: Création des candidats...", "🎭")
candidate_names = [
"Alice Johnson", "Bob Smith", "Carol White", "David Brown",
"Emily Davis", "Frank Miller", "Grace Wilson", "Henry Moore"
]
sql_candidates = "INSERT INTO candidates (election_id, name, description, `order`, created_at) VALUES\n"
candidate_values = []
for election_id in range(1, 31):
# 4-8 candidats par élection
num_candidates = random.randint(4, 8)
selected_candidates = random.sample(candidate_names, min(num_candidates, len(candidate_names)))
for order, name in enumerate(selected_candidates, 1):
candidate_values.append(
f"({election_id}, '{name}', 'Candidate {name}', {order}, NOW())"
)
sql_candidates += ",\n".join(candidate_values) + ";"
result = run_sql(sql_candidates)
if result.returncode == 0:
log(f"✓ Candidats créés pour toutes les élections", "")
else:
log(f"Erreur: {result.stderr}", "")
# ============================================================================
# PHASE 4: CRÉER LES VOTES
# ============================================================================
log("PHASE 4: Création des votes...", "🗳️")
sql_votes = "INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp) VALUES\n"
vote_values = []
# Pour chaque élection
for election_id in range(1, 31):
# Récupérer les candidats de cette élection
result_candidates = run_sql(f"SELECT id FROM candidates WHERE election_id = {election_id};")
candidate_ids = []
if result_candidates.returncode == 0:
lines = result_candidates.stdout.strip().split('\n')[1:]
for line in lines:
if line.strip():
candidate_ids.append(int(line.split()[0]))
if not candidate_ids:
continue
# Déterminer le nombre de votants pour cette élection
# Entre 20% et 80% des utilisateurs votent pour cette élection
num_voters = random.randint(int(1700 * 0.2), int(1700 * 0.8))
# Sélectionner les votants aléatoirement
voter_ids = random.sample(range(1, 1701), num_voters)
for voter_id in voter_ids:
candidate_id = random.choice(candidate_ids)
ballot_hash = f"hash_{voter_id}_{election_id}"
vote_values.append(
f"({voter_id}, {election_id}, {candidate_id}, 'encrypted_{voter_id}_{election_id}', '{ballot_hash}', NOW())"
)
# Insérer tous les votes en batch
batch_size = 1000
for i in range(0, len(vote_values), batch_size):
batch = vote_values[i:i+batch_size]
sql_batch = "INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp) VALUES\n"
sql_batch += ",\n".join(batch) + ";"
result = run_sql(sql_batch)
if result.returncode != 0:
log(f"Erreur batch {i//batch_size}: {result.stderr}", "")
log(f"{len(vote_values)} votes créés", "")
# ============================================================================
# PHASE 5: VOTES SPÉCIAUX POUR new_user_e13_192
# ============================================================================
log("PHASE 5: Configuration des votes pour new_user_e13_192...", "⚙️")
# Récupérer l'ID de new_user_e13_192
result = run_sql("SELECT id FROM voters WHERE email = 'new_user_e13_192@voting.local';")
special_user_id = None
if result.returncode == 0 and result.stdout.strip():
lines = result.stdout.strip().split('\n')
if len(lines) > 1:
special_user_id = int(lines[1].split()[0])
if special_user_id:
# Supprimer tous les votes actuels pour cet utilisateur
run_sql(f"DELETE FROM votes WHERE voter_id = {special_user_id};")
special_votes = []
# 10 votes pour les élections passées (1-10)
for election_id in range(1, 11):
result_cand = run_sql(f"SELECT id FROM candidates WHERE election_id = {election_id} LIMIT 1;")
if result_cand.returncode == 0 and result_cand.stdout.strip():
lines = result_cand.stdout.strip().split('\n')
if len(lines) > 1:
candidate_id = int(lines[1].split()[0])
ballot_hash = f"hash_{special_user_id}_{election_id}"
special_votes.append(
f"({special_user_id}, {election_id}, {candidate_id}, 'encrypted_{special_user_id}_{election_id}', '{ballot_hash}', NOW())"
)
# 4 votes pour les élections actives (11, 12, 14, 15) - PAS 13
for election_id in [11, 12, 14, 15]:
result_cand = run_sql(f"SELECT id FROM candidates WHERE election_id = {election_id} LIMIT 1;")
if result_cand.returncode == 0 and result_cand.stdout.strip():
lines = result_cand.stdout.strip().split('\n')
if len(lines) > 1:
candidate_id = int(lines[1].split()[0])
ballot_hash = f"hash_{special_user_id}_{election_id}"
special_votes.append(
f"({special_user_id}, {election_id}, {candidate_id}, 'encrypted_{special_user_id}_{election_id}', '{ballot_hash}', NOW())"
)
# 4 votes pour les élections futures (21, 24, 27, 30)
for election_id in [21, 24, 27, 30]:
result_cand = run_sql(f"SELECT id FROM candidates WHERE election_id = {election_id} LIMIT 1;")
if result_cand.returncode == 0 and result_cand.stdout.strip():
lines = result_cand.stdout.strip().split('\n')
if len(lines) > 1:
candidate_id = int(lines[1].split()[0])
ballot_hash = f"hash_{special_user_id}_{election_id}"
special_votes.append(
f"({special_user_id}, {election_id}, {candidate_id}, 'encrypted_{special_user_id}_{election_id}', '{ballot_hash}', NOW())"
)
if special_votes:
sql_special = "INSERT INTO votes (voter_id, election_id, candidate_id, encrypted_vote, ballot_hash, timestamp) VALUES\n"
sql_special += ",\n".join(special_votes) + ";"
result = run_sql(sql_special)
if result.returncode == 0:
log(f"✓ 18 votes spéciaux créés pour new_user_e13_192 (10 + 4 + 4)", "")
else:
log(f"Erreur: {result.stderr}", "")
# ============================================================================
# PHASE 6: STATISTIQUES FINALES
# ============================================================================
log("PHASE 6: Vérification des données...", "📊")
result = run_sql("""
SELECT
(SELECT COUNT(*) FROM elections) as total_elections,
(SELECT COUNT(*) FROM voters) as total_voters,
(SELECT COUNT(*) FROM candidates) as total_candidates,
(SELECT COUNT(*) FROM votes) as total_votes;
""")
if result.returncode == 0:
print("\n" + "="*60)
print("✅ BASE DE DONNÉES REPUPLÉE AVEC SUCCÈS!")
print("="*60)
print(result.stdout)
print("="*60)
# Afficher les utilisateurs spéciaux
result = run_sql("SELECT id, email, first_name, last_name FROM voters WHERE email LIKE 'new_user_e13_%' ORDER BY id;")
if result.returncode == 0:
print("\n" + "="*60)
print("👤 UTILISATEURS SPÉCIAUX:")
print("="*60)
print(result.stdout)
print("\n✅ Vous pouvez maintenant vous connecter avec:")
print(" Email: new_user_e13_192@voting.local")
print(" Mot de passe: epita1234")
print(" Votes: 10 historiques + 4 actifs + 4 futurs")
print(" Élection sans vote: Active Election 3 (ID 13)")
print("="*60)