diff --git a/e-voting-system/.claude/BUILD_SOLUTION.md b/e-voting-system/.claude/BUILD_SOLUTION.md new file mode 100644 index 0000000..ea57087 --- /dev/null +++ b/e-voting-system/.claude/BUILD_SOLUTION.md @@ -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 diff --git a/e-voting-system/.claude/DEV_NOTES.md b/e-voting-system/.claude/DEV_NOTES.md new file mode 100644 index 0000000..a0ce574 --- /dev/null +++ b/e-voting-system/.claude/DEV_NOTES.md @@ -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` + +#### ïżœ 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 +``` + diff --git a/e-voting-system/Makefile b/e-voting-system/Makefile index 76845ec..f993341 100644 --- a/e-voting-system/Makefile +++ b/e-voting-system/Makefile @@ -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 diff --git a/e-voting-system/backend/routes/elections.py b/e-voting-system/backend/routes/elections.py index 061b806..053e97e 100644 --- a/e-voting-system/backend/routes/elections.py +++ b/e-voting-system/backend/routes/elections.py @@ -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""" diff --git a/e-voting-system/backend/routes/votes.py b/e-voting-system/backend/routes/votes.py index 084c7f3..2687e69 100644 --- a/e-voting-system/backend/routes/votes.py +++ b/e-voting-system/backend/routes/votes.py @@ -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 diff --git a/e-voting-system/backend/schemas.py b/e-voting-system/backend/schemas.py index 803350c..848cad1 100644 --- a/e-voting-system/backend/schemas.py +++ b/e-voting-system/backend/schemas.py @@ -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 diff --git a/e-voting-system/backend/scripts/seed_db.py b/e-voting-system/backend/scripts/seed_db.py index 6a51f70..e6e8d89 100644 --- a/e-voting-system/backend/scripts/seed_db.py +++ b/e-voting-system/backend/scripts/seed_db.py @@ -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__": diff --git a/e-voting-system/build.sh b/e-voting-system/build.sh new file mode 100755 index 0000000..f4e60ef --- /dev/null +++ b/e-voting-system/build.sh @@ -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" diff --git a/e-voting-system/docker-compose.dev.yml b/e-voting-system/docker-compose.dev.yml new file mode 100644 index 0000000..f53cb13 --- /dev/null +++ b/e-voting-system/docker-compose.dev.yml @@ -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 diff --git a/e-voting-system/docker/Dockerfile.frontend.dev b/e-voting-system/docker/Dockerfile.frontend.dev new file mode 100644 index 0000000..d980470 --- /dev/null +++ b/e-voting-system/docker/Dockerfile.frontend.dev @@ -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"] diff --git a/e-voting-system/docker/populate_past_elections.sql b/e-voting-system/docker/populate_past_elections.sql new file mode 100644 index 0000000..9084c32 --- /dev/null +++ b/e-voting-system/docker/populate_past_elections.sql @@ -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; diff --git a/e-voting-system/frontend/src/App.js b/e-voting-system/frontend/src/App.js index facd4d7..a15bf2e 100644 --- a/e-voting-system/frontend/src/App.js +++ b/e-voting-system/frontend/src/App.js @@ -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() { } /> + {/* Dashboard Routes */} } /> - : - - } - /> - : - - } - /> : + : } /> : + : } /> + : + + } + /> + + {/* Voting Route */} + : + + } + /> + + {/* Archives Routes */} } /> + } + /> + + {/* Legacy route - redirect to archives */} + } + /> 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; + } +} diff --git a/e-voting-system/frontend/src/components/ElectionDetailsModal.jsx b/e-voting-system/frontend/src/components/ElectionDetailsModal.jsx new file mode 100644 index 0000000..54ef91b --- /dev/null +++ b/e-voting-system/frontend/src/components/ElectionDetailsModal.jsx @@ -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 ( +
+
e.stopPropagation()}> +
+

{election?.name || 'Détails de l\'élection'}

+ +
+ + {loading &&
Chargement...
} + {error &&
{error}
} + + {election && !loading && ( +
+
+

📋 Informations

+

{election.description}

+
+
+ +

{formatDate(election.start_date)}

+
+
+ +

{formatDate(election.end_date)}

+
+
+
+ +
+

đŸ‘„ Candidats ({election.candidates?.length || 0})

+
+ {election.candidates?.map((candidate, index) => ( +
+ {candidate.order || index + 1} +
+

{candidate.name}

+ {candidate.description &&

{candidate.description}

} +
+
+ ))} +
+
+ + {results && election.results_published && ( +
+

📊 RĂ©sultats

+

+ + Total: {results.total_votes} vote(s) +

+
+ {results.results?.map((result, index) => ( +
+
+ + {userVote && userVote.candidate_name === result.candidate_name && ( + + )} + {result.candidate_name} + + {result.percentage.toFixed(1)}% +
+
+
+
+ {result.vote_count} vote(s) +
+ ))} +
+
+ )} +
+ )} +
+
+ ); +} diff --git a/e-voting-system/frontend/src/components/VoteCard.jsx b/e-voting-system/frontend/src/components/VoteCard.jsx index ec5b998..c365dff 100644 --- a/e-voting-system/frontend/src/components/VoteCard.jsx +++ b/e-voting-system/frontend/src/components/VoteCard.jsx @@ -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 )} + {vote.status === 'actif' && ( + + )} {userVote && ( )} - {vote.status === 'ferme' && ( - + {(vote.status === 'ferme' || vote.status === 'fermĂ©') && ( + )} {vote.status === 'futur' && ( - )} diff --git a/e-voting-system/frontend/src/pages/ActiveVotesPage.css b/e-voting-system/frontend/src/pages/ActiveVotesPage.css new file mode 100644 index 0000000..dce4b59 --- /dev/null +++ b/e-voting-system/frontend/src/pages/ActiveVotesPage.css @@ -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); +} + diff --git a/e-voting-system/frontend/src/pages/ActiveVotesPage.jsx b/e-voting-system/frontend/src/pages/ActiveVotesPage.jsx new file mode 100644 index 0000000..a7ff993 --- /dev/null +++ b/e-voting-system/frontend/src/pages/ActiveVotesPage.jsx @@ -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 ; + + return ( +
+
+
+ +

🔮 Votes en Cours

+

Élections en cours auxquelles vous participez

+
+ + {activeElections.length === 0 ? ( +
+
📭
+

Aucun vote en cours

+

Il n'y a actuellement aucune élection active.

+
+ ) : ( + <> +
+
+ Élections en cours + {activeElections.length} +
+
+ +
+ {activeElections.map(election => { + const hasVoted = userVotedElectionIds.has(election.id); + + return ( +
+
+

{election.name}

+ + 🔮 En cours + +
+
+ {hasVoted ? ( + <> +

Statut : Vous avez votĂ© ✓

+ + + ) : ( + <> +

{election.description || 'Votez pour cette élection'}

+
+ + +
+ + )} +
+
+ ); + })} +
+ + )} + + setSelectedElectionId(null)} + voter={voter} + type="actif" + /> +
+
+ ); +} diff --git a/e-voting-system/frontend/src/pages/ArchivesPage.jsx b/e-voting-system/frontend/src/pages/ArchivesPage.jsx index 77ac51d..802cf95 100644 --- a/e-voting-system/frontend/src/pages/ArchivesPage.jsx +++ b/e-voting-system/frontend/src/pages/ArchivesPage.jsx @@ -105,6 +105,7 @@ export default function ArchivesPage() { key={vote.id} vote={vote} showResult={true} + context="archives" /> ))} diff --git a/e-voting-system/frontend/src/pages/DashboardPage.jsx b/e-voting-system/frontend/src/pages/DashboardPage.jsx index bec6408..110f611 100644 --- a/e-voting-system/frontend/src/pages/DashboardPage.jsx +++ b/e-voting-system/frontend/src/pages/DashboardPage.jsx @@ -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 ; return (
- {/* Welcome Section */}
-
-

Bienvenue, {voter?.nom}! 👋

-

Voici votre tableau de bord personnel

-
+

Bienvenue, {voter?.nom}! 👋

+

Voici votre tableau de bord personnel

- {/* Stats Section */}
-
- -
+
{activeVotes.length}
Votes Actifs
- Voir → + Voir →
-
- -
+
{futureVotes.length}
À Venir
- Voir → + Voir →
-
- -
+
{historyVotes.length}
Votes Terminés
- Voir → + Voir →
-
- -
+
{userVotes.length}
Votes Effectués
@@ -111,39 +103,10 @@ export default function DashboardPage({ voter }) {
- {/* Navigation Tabs */} -
- - - - -
- - {/* Action Required Section - Only for Active Votes */} - {filter === 'all' && activeVotes.length > 0 && ( -
-

⚡ Action Requise

-

Votes en attente de votre participation

+ {activeVotes.length > 0 && ( +
+

⚡ Votes Actifs

+

Votes en cours - Participez maintenant!

{activeVotes.slice(0, 2).map(vote => ( {activeVotes.length > 2 && ( - Voir tous les votes actifs + Voir tous les votes actifs ({activeVotes.length}) )}
)} - {/* Votes Display */} -
-

- {filter === 'all' && 'Tous les votes'} - {filter === 'actifs' && 'Votes Actifs'} - {filter === 'futurs' && 'Votes Ă  Venir'} - {filter === 'historique' && 'Mon Historique'} -

+ {futureVotes.length > 0 && ( +
+

🔼 Votes à Venir

+

Élections qui dĂ©marreront bientĂŽt

+
+ {futureVotes.slice(0, 2).map(vote => ( + setSelectedElectionId(id)} + /> + ))} +
+ {futureVotes.length > 2 && ( + + Voir tous les votes Ă  venir ({futureVotes.length}) + + )} +
+ )} - {(() => { - let displayVotes = votes; - if (filter === 'actifs') displayVotes = activeVotes; - if (filter === 'futurs') displayVotes = futureVotes; - if (filter === 'historique') displayVotes = historyVotes; + {historyVotes.length > 0 && ( +
+

📋 Mon Historique

+

Vos 2 derniers votes

+
+ {historyVotes.slice(0, 2).map(vote => ( + v.election_id === vote.id)?.choix} + showResult={true} + context="historique" + onShowDetails={(id) => setSelectedElectionId(id)} + /> + ))} +
+ {historyVotes.length > 2 && ( + + Voir tout mon historique ({historyVotes.length}) + + )} +
+ )} - if (displayVotes.length === 0) { - return ( -
-
📭
-

Aucun vote

-

- {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é.'} -

-
- ); - } + {activeVotes.length === 0 && futureVotes.length === 0 && historyVotes.length === 0 && ( +
+
+
📭
+

Aucun vote disponible

+

Il n'y a pas encore de votes disponibles.

+
+
+ )} - return ( -
- {displayVotes.map(vote => ( - v.election_id === vote.id)?.choix} - showResult={filter === 'historique' || vote.status === 'ferme'} - onVote={(id) => window.location.href = `/vote/${id}`} - /> - ))} -
- ); - })()} -
+ setSelectedElectionId(null)} + voter={voter} + type="futur" + />
); diff --git a/e-voting-system/frontend/src/pages/ElectionDetailsPage.css b/e-voting-system/frontend/src/pages/ElectionDetailsPage.css new file mode 100644 index 0000000..17511a3 --- /dev/null +++ b/e-voting-system/frontend/src/pages/ElectionDetailsPage.css @@ -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; + } +} diff --git a/e-voting-system/frontend/src/pages/ElectionDetailsPage.jsx b/e-voting-system/frontend/src/pages/ElectionDetailsPage.jsx new file mode 100644 index 0000000..bfcf485 --- /dev/null +++ b/e-voting-system/frontend/src/pages/ElectionDetailsPage.jsx @@ -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 ; + + if (error) { + return ( +
+
+ + +
+
+ ); + } + + if (!election) { + return ( +
+
+ + +
+
+ ); + } + + const status = getElectionStatus(); + const statusColor = status === 'Terminée' ? 'closed' : status === 'En cours' ? 'active' : 'future'; + + return ( +
+
+ + +
+
+

{election.name}

+ {status} +
+
+ +
+ {/* Section Informations */} +
+

📋 Informations

+

{election.description}

+ +
+
+ +
+ +

{formatDate(election.start_date)}

+
+
+
+ +
+ +

{formatDate(election.end_date)}

+
+
+
+
+ + {/* Section Candidats */} +
+

đŸ‘„ Candidats ({candidates.length})

+
+ {candidates.map((candidate, index) => ( +
+
{candidate.order || index + 1}
+
+

{candidate.name}

+ {candidate.description &&

{candidate.description}

} +
+
+ ))} +
+
+ + {/* Section Résultats */} + {results && election.results_published && ( +
+

📊 RĂ©sultats

+
+ {results.results && results.results.length > 0 ? ( + <> +

+ + Total: {results.total_votes} vote(s) +

+
+ {results.results.map((result, index) => ( +
+
+ + {userVote && userVote.candidate_name === result.candidate_name && ( + + )} + {result.candidate_name} + + {result.percentage.toFixed(1)}% +
+
+
+
+ {result.vote_count} vote(s) +
+ ))} +
+ + ) : ( +

Aucun résultat disponible

+ )} +
+
+ )} + + {!election.results_published && status === 'Terminée' && ( +
+

+ 📊 Les rĂ©sultats de cette Ă©lection n'ont pas encore Ă©tĂ© publiĂ©s. +

+
+ )} + + {status !== 'Terminée' && ( +
+

+ ⏳ Les rĂ©sultats seront disponibles une fois l'Ă©lection terminĂ©e. +

+
+ )} +
+
+
+ ); +} diff --git a/e-voting-system/frontend/src/pages/HistoriquePage.css b/e-voting-system/frontend/src/pages/HistoriquePage.css new file mode 100644 index 0000000..b9ef5aa --- /dev/null +++ b/e-voting-system/frontend/src/pages/HistoriquePage.css @@ -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; + } +} diff --git a/e-voting-system/frontend/src/pages/HistoriquePage.jsx b/e-voting-system/frontend/src/pages/HistoriquePage.jsx new file mode 100644 index 0000000..f3cb99c --- /dev/null +++ b/e-voting-system/frontend/src/pages/HistoriquePage.jsx @@ -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 ; + + return ( +
+
+
+ +

📋 Mon Historique de Votes

+

Toutes les élections passées auxquelles vous avez participé

+
+ + {filteredElections.length === 0 ? ( +
+
📭
+

Aucun vote enregistré

+

Vous n'avez pas encore participé à des élections terminées.

+
+ ) : ( + <> +
+
+ Total de votes + {filteredElections.length} +
+
+ Élections auxquelles vous avez participĂ© + {filteredElections.length} +
+
+ +
+ {filteredElections.map(vote => ( +
+
+

{vote.election_name}

+ + ✅ TerminĂ©e + +
+
+

Votre choix : {vote.candidate_name}

+

Date du vote : {new Date(vote.vote_date).toLocaleDateString('fr-FR')}

+ +
+
+ ))} +
+ + )} + + {/* Election Details Modal */} + setSelectedElectionId(null)} + voter={voter} + type="historique" + /> +
+
+ ); +} diff --git a/e-voting-system/frontend/src/pages/LoginPage.jsx b/e-voting-system/frontend/src/pages/LoginPage.jsx new file mode 100644 index 0000000..be7f8e8 --- /dev/null +++ b/e-voting-system/frontend/src/pages/LoginPage.jsx @@ -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 ( +
+
+
+
+

Se Connecter

+

Accédez à votre tableau de bord

+
+ + {error && ( + setError('')} /> + )} + +
+
+ +
+ + setEmail(e.target.value)} + required + disabled={loading} + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + required + disabled={loading} + /> +
+
+ + + + +
+ +
ou
+ +

+ Pas encore de compte ? S'inscrire +

+
+ +
+
+
đŸ—łïž
+

Bienvenue

+

Votez en toute confiance sur notre plateforme sécurisée

+
+
+
+
+ ); +} diff --git a/e-voting-system/frontend/src/pages/UpcomingVotesPage.css b/e-voting-system/frontend/src/pages/UpcomingVotesPage.css new file mode 100644 index 0000000..b1d82bc --- /dev/null +++ b/e-voting-system/frontend/src/pages/UpcomingVotesPage.css @@ -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; +} diff --git a/e-voting-system/frontend/src/pages/UpcomingVotesPage.jsx b/e-voting-system/frontend/src/pages/UpcomingVotesPage.jsx new file mode 100644 index 0000000..b200fde --- /dev/null +++ b/e-voting-system/frontend/src/pages/UpcomingVotesPage.jsx @@ -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 ; + + return ( +
+
+
+ +

⏳ Votes à Venir

+

Élections qui arriveront prochainement

+
+ + {upcomingElections.length === 0 ? ( +
+
📭
+

Aucun vote Ă  venir

+

Aucune élection prévue pour le moment.

+
+ ) : ( + <> +
+
+ Élections à venir + {upcomingElections.length} +
+
+ +
+ {upcomingElections.map(election => ( +
+
+

{election.name}

+ + ⏳ À venir + +
+
+

Description : {election.description || 'N/A'}

+

Début : {new Date(election.start_date).toLocaleDateString('fr-FR')}

+ +
+
+ ))} +
+ + )} + + setSelectedElectionId(null)} + voter={voter} + type="futur" + /> +
+
+ ); +} diff --git a/e-voting-system/frontend/src/pages/VotingPage.jsx b/e-voting-system/frontend/src/pages/VotingPage.jsx index 8d4d188..8acadac 100644 --- a/e-voting-system/frontend/src/pages/VotingPage.jsx +++ b/e-voting-system/frontend/src/pages/VotingPage.jsx @@ -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 electionData = await electionResponse.json(); + setElection(electionData); - const data = await response.json(); - setVote(data); + // 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 ; - if (!vote) { + if (!election) { return (
- +
- {/* Left Column - Vote Info */} -
-
-

{vote.titre}

-
- - Statut: {vote.status === 'actif' ? '🟱 OUVERT' : '🔮 TERMINÉ'} - - - Participants: {vote.total_votes || 0} votes - + {/* Vote Header */} +
+

{election.name}

+

{election.description}

+ + {/* Status Badge */} +
+ {isHistorique && 📊 TerminĂ© - RĂ©sultats disponibles} + {isActif && 🟱 Élection en cours} + {isFutur && ⏰ À venir} +
+ + {/* User's vote if already voted */} + {userVote && ( +
+ + Vous avez voté pour: {userVote.choix}
-
+ )} +
-
-

Description

-

{vote.description}

- - {vote.context && ( -
-

Contexte

-

{vote.context}

-
- )} - - {vote.resultats && vote.status === 'ferme' && ( -
-

Résultats Finaux

-
- {Object.entries(vote.resultats).map(([option, count]) => ( -
- {option} -
-
-
- - {Math.round((count / (vote.total_votes || 1)) * 100)}% - + {/* Candidates Section */} +
+ {candidates.length > 0 ? ( + <> +

{isHistorique ? '📊 RĂ©sultats' : 'Candidats'}

+
+ {candidates.map((candidate) => ( +
+
+

{candidate.name}

+ {candidate.description &&

{candidate.description}

}
- ))} -
-
- )} -
-
- - {/* Right Column - Voting Section */} -
-
-

Voter

- - {error && ( - setError('')} /> - )} - - {vote.status !== 'actif' && ( - - )} - - {vote.status === 'actif' && ( - <> -
-

- Sélectionnez votre option: -

- -
- {vote.options && vote.options.map((option) => ( - - ))} + {isActif && !userVote && ( + + )} + {isHistorique && results && ( +
+ + {results[candidate.name] || 0}% + +
+ )}
- - -
- -
- -

- Votre vote est final et ne peut pas ĂȘtre modifiĂ© aprĂšs la soumission. -

-
- - )} -
+ ))} +
+ + ) : ( + + )}
+ + {/* Future Election Message */} + {isFutur && ( +
+ +

Élection à venir

+

Les détails et la date d'ouverture seront bientÎt disponibles.

+
+ )}
diff --git a/e-voting-system/frontend/src/pages/index.js b/e-voting-system/frontend/src/pages/index.js index 5800e7c..e93d2cf 100644 --- a/e-voting-system/frontend/src/pages/index.js +++ b/e-voting-system/frontend/src/pages/index.js @@ -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'; diff --git a/e-voting-system/rebuild.sh b/e-voting-system/rebuild.sh new file mode 100755 index 0000000..abbe691 --- /dev/null +++ b/e-voting-system/rebuild.sh @@ -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" diff --git a/e-voting-system/restore_data.py b/e-voting-system/restore_data.py new file mode 100644 index 0000000..4d7976e --- /dev/null +++ b/e-voting-system/restore_data.py @@ -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)