Changes:
- Add next-themes dependency for theme management
- Create ThemeProvider wrapper for app root layout
- Set dark mode as default theme
- Create ThemeToggle component with Sun/Moon icons
- Add theme toggle to home page navigation
- Add theme toggle to dashboard header
- App now starts in dark mode with ability to switch to light mode
Styling uses existing Tailwind dark mode variables configured in
tailwind.config.ts and globals.css. All existing components automatically
support dark theme.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
225 lines
6.6 KiB
Python
225 lines
6.6 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("/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 |