Alexis Bruneteau 1a42b4d83b feat: Implement blockchain-based election storage with cryptographic security
Elections are now immutably recorded to blockchain with:
- SHA-256 hash chain for integrity (prevents tampering)
- RSA-PSS signatures for authentication
- Candidate verification via SHA-256 hash
- Tamper detection on every verification
- Complete audit trail

Changes:
- backend/blockchain_elections.py: Core blockchain implementation (ElectionBlock, ElectionsBlockchain)
- backend/init_blockchain.py: Startup initialization to sync existing elections
- backend/services.py: ElectionService.create_election() with automatic blockchain recording
- backend/main.py: Added blockchain initialization on startup
- backend/routes/elections.py: Already had /api/elections/blockchain and /{id}/blockchain-verify endpoints
- test_blockchain_election.py: Comprehensive test suite for blockchain integration
- BLOCKCHAIN_ELECTION_INTEGRATION.md: Full technical documentation
- BLOCKCHAIN_QUICK_START.md: Quick reference guide
- BLOCKCHAIN_IMPLEMENTATION_SUMMARY.md: Implementation summary

API Endpoints:
- GET /api/elections/blockchain - Returns complete blockchain
- GET /api/elections/{id}/blockchain-verify - Verifies election integrity

Test:
  python3 test_blockchain_election.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 03:01:11 +01:00

276 lines
8.2 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 .. 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 datetime import datetime
from .. import models
now = datetime.utcnow()
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 datetime, timedelta
from .. import models
now = datetime.utcnow()
# 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("/completed")
def get_completed_elections(db: Session = Depends(get_db)):
"""Récupérer tous les votes passés/terminés"""
from datetime import datetime
elections = db.query(services.models.Election).filter(
services.models.Election.end_date < datetime.utcnow(),
services.models.Election.results_published == True
).all()
return elections
@router.get("/upcoming")
def get_upcoming_elections(db: Session = Depends(get_db)):
"""Récupérer tous les votes à venir"""
from datetime import datetime
elections = db.query(services.models.Election).filter(
services.models.Election.start_date > datetime.utcnow()
).all()
return elections
@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}", 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
@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("/completed", response_model=list[schemas.ElectionResponse])
def get_completed_elections(db: Session = Depends(get_db)):
"""Récupérer toutes les élections terminées (archives)"""
from datetime import datetime
from .. import models
completed = db.query(models.Election).filter(
models.Election.end_date < datetime.utcnow(),
models.Election.results_published == True
).all()
return completed
@router.get("/upcoming", response_model=list[schemas.ElectionResponse])
def get_upcoming_elections(db: Session = Depends(get_db)):
"""Récupérer toutes les élections futures"""
from datetime import datetime
from .. import models
upcoming = db.query(models.Election).filter(
models.Election.start_date > datetime.utcnow()
).all()
return upcoming
@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