This commit fixes 5 critical bugs found during code review: Bug #1 (CRITICAL): Missing API endpoints for election filtering - Added GET /api/elections/upcoming endpoint - Added GET /api/elections/completed endpoint - Both properly filter elections by date Bug #2 (HIGH): Auth context has_voted state inconsistency - Backend schemas now include has_voted in LoginResponse and RegisterResponse - Auth routes return actual has_voted value from database - Frontend context uses server response instead of hardcoding false - Frontend API client properly typed with has_voted field Bug #3 (HIGH): Transaction safety in vote submission - Simplified error handling in vote submission endpoints - Now only calls mark_as_voted() once at the end - Vote response includes voter_marked_voted flag to indicate success - Ensures consistency even if blockchain submission fails Bug #4 (MEDIUM): Vote status endpoint - Verified endpoint already exists at GET /api/votes/status - Tests confirm proper functionality Bug #5 (MEDIUM): Response format inconsistency - Previously fixed in commit e10a882 - Frontend now handles both array and wrapped object formats Added comprehensive test coverage: - 20+ backend API tests (tests/test_api_fixes.py) - 6+ auth context tests (frontend/__tests__/auth-context.test.tsx) - 8+ elections API tests (frontend/__tests__/elections-api.test.ts) - 10+ vote submission tests (frontend/__tests__/vote-submission.test.ts) All fixes ensure frontend and backend communicate consistently. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
261 lines
7.8 KiB
Python
261 lines
7.8 KiB
Python
"""
|
|
Routes pour les élections et les candidats.
|
|
|
|
Elections are stored immutably in blockchain with cryptographic security:
|
|
- SHA-256 hash chain prevents tampering
|
|
- RSA-PSS signatures authenticate election data
|
|
- Merkle tree verification for candidates
|
|
- Complete audit trail on blockchain
|
|
"""
|
|
|
|
from fastapi import APIRouter, HTTPException, status, Depends
|
|
from sqlalchemy.orm import Session
|
|
from datetime import datetime, timezone
|
|
from .. import schemas, services
|
|
from ..dependencies import get_db, get_current_voter
|
|
from ..models import Voter
|
|
from ..blockchain_elections import (
|
|
record_election_to_blockchain,
|
|
verify_election_in_blockchain,
|
|
get_elections_blockchain_data,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/elections", tags=["elections"])
|
|
|
|
|
|
@router.get("/debug/all")
|
|
def debug_all_elections(db: Session = Depends(get_db)):
|
|
"""DEBUG: Return all elections with dates for troubleshooting"""
|
|
from .. import models
|
|
|
|
now = datetime.now(timezone.utc)
|
|
all_elections = db.query(models.Election).all()
|
|
|
|
return {
|
|
"current_time": now.isoformat(),
|
|
"elections": [
|
|
{
|
|
"id": e.id,
|
|
"name": e.name,
|
|
"is_active": e.is_active,
|
|
"start_date": e.start_date.isoformat() if e.start_date else None,
|
|
"end_date": e.end_date.isoformat() if e.end_date else None,
|
|
"should_be_active": (
|
|
e.start_date <= now <= e.end_date and e.is_active
|
|
if e.start_date and e.end_date
|
|
else False
|
|
),
|
|
}
|
|
for e in all_elections
|
|
],
|
|
}
|
|
|
|
|
|
@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"""
|
|
from datetime import timedelta
|
|
from .. import models
|
|
|
|
now = datetime.now(timezone.utc)
|
|
# Allow 1 hour buffer for timezone issues
|
|
start_buffer = now - timedelta(hours=1)
|
|
end_buffer = now + timedelta(hours=1)
|
|
|
|
active = db.query(models.Election).filter(
|
|
(models.Election.start_date <= end_buffer) &
|
|
(models.Election.end_date >= start_buffer) &
|
|
(models.Election.is_active == True)
|
|
).order_by(models.Election.id.asc()).all()
|
|
|
|
return active
|
|
|
|
|
|
@router.get("/upcoming", response_model=list[schemas.ElectionResponse])
|
|
def get_upcoming_elections(db: Session = Depends(get_db)):
|
|
"""Récupérer toutes les élections à venir"""
|
|
from datetime import timedelta
|
|
from .. import models
|
|
|
|
now = datetime.now(timezone.utc)
|
|
# Allow 1 hour buffer for timezone issues
|
|
end_buffer = now + timedelta(hours=1)
|
|
|
|
upcoming = db.query(models.Election).filter(
|
|
(models.Election.start_date > end_buffer) &
|
|
(models.Election.is_active == True)
|
|
).order_by(models.Election.start_date.asc()).all()
|
|
|
|
return upcoming
|
|
|
|
|
|
@router.get("/completed", response_model=list[schemas.ElectionResponse])
|
|
def get_completed_elections(db: Session = Depends(get_db)):
|
|
"""Récupérer toutes les élections terminées"""
|
|
from datetime import timedelta
|
|
from .. import models
|
|
|
|
now = datetime.now(timezone.utc)
|
|
# Allow 1 hour buffer for timezone issues
|
|
start_buffer = now - timedelta(hours=1)
|
|
|
|
completed = db.query(models.Election).filter(
|
|
(models.Election.end_date < start_buffer) &
|
|
(models.Election.is_active == True)
|
|
).order_by(models.Election.end_date.desc()).all()
|
|
|
|
return completed
|
|
|
|
|
|
@router.get("/blockchain")
|
|
def get_elections_blockchain():
|
|
"""
|
|
Retrieve the complete elections blockchain.
|
|
|
|
Returns all election records stored immutably with cryptographic verification.
|
|
Useful for auditing election creation and verifying no tampering occurred.
|
|
"""
|
|
return get_elections_blockchain_data()
|
|
|
|
|
|
@router.get("/{election_id}/blockchain-verify")
|
|
def verify_election_blockchain(election_id: int, db: Session = Depends(get_db)):
|
|
"""
|
|
Verify an election's blockchain integrity.
|
|
|
|
Returns verification report:
|
|
- hash_valid: Block hash matches computed hash
|
|
- chain_valid: Entire chain integrity is valid
|
|
- signature_valid: Block is properly signed
|
|
- verified: All checks passed
|
|
"""
|
|
# First verify it exists in database
|
|
election = services.ElectionService.get_election(db, election_id)
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Election not found in database"
|
|
)
|
|
|
|
# Then verify it's in blockchain
|
|
verification = verify_election_in_blockchain(election_id)
|
|
|
|
if not verification.get("verified"):
|
|
# Still return data but mark as unverified
|
|
return {
|
|
**verification,
|
|
"warning": "Election blockchain verification failed - possible tampering"
|
|
}
|
|
|
|
return verification
|
|
|
|
|
|
# Routes with path parameters must come AFTER specific routes
|
|
@router.get("/active/results")
|
|
def get_active_election_results(db: Session = Depends(get_db)):
|
|
"""Récupérer les résultats de l'élection active"""
|
|
|
|
election = services.ElectionService.get_active_election(db)
|
|
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="No active election"
|
|
)
|
|
|
|
results = services.VoteService.get_election_results(db, election.id)
|
|
|
|
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}/results", response_model=schemas.ElectionResultResponse)
|
|
def get_election_results(
|
|
election_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Récupérer les résultats d'une élection.
|
|
Disponible après la fermeture du scrutin.
|
|
"""
|
|
|
|
election = services.ElectionService.get_election(db, election_id)
|
|
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Election not found"
|
|
)
|
|
|
|
if not election.results_published:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Results not yet published"
|
|
)
|
|
|
|
results = services.VoteService.get_election_results(db, election_id)
|
|
|
|
return schemas.ElectionResultResponse(
|
|
election_id=election.id,
|
|
election_name=election.name,
|
|
total_votes=sum(r.vote_count for r in results),
|
|
results=results
|
|
)
|
|
|
|
|
|
@router.post("/{election_id}/publish-results")
|
|
def publish_results(
|
|
election_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
Publier les résultats d'une élection (admin only).
|
|
À utiliser après la fermeture du scrutin.
|
|
"""
|
|
|
|
election = services.ElectionService.get_election(db, election_id)
|
|
|
|
if not election:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Election not found"
|
|
)
|
|
|
|
# Marquer les résultats comme publiés
|
|
election.results_published = True
|
|
db.commit()
|
|
|
|
return {
|
|
"message": "Results published successfully",
|
|
"election_id": election.id,
|
|
"election_name": election.name
|
|
}
|
|
|
|
|
|
@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"""
|
|
|
|
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 |