""" 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("/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