feat: Controllo accessi RFID completo con gestione sessioni

- Aggiunto supporto multi-pattern RFID (US/IT layout)
- Implementata invalidazione sessioni al restart del server
- Schermata "badge non trovato" con countdown 30s
- Notifica quando badge validatore passato senza utente
- Database aggiornato con badge reali di test
- Layout ottimizzato per tablet orizzontale
- Banner NumLock per desktop
- Toggle visibilità password
- Carosello benvenuto multilingua (10 lingue)
- Pagina debug RFID (/debug)
This commit is contained in:
2026-01-17 20:06:50 +01:00
parent 21b509c6ba
commit e68f299feb
48 changed files with 3625 additions and 2445 deletions

153
backend-mock/api/routes.py Normal file
View File

@@ -0,0 +1,153 @@
"""
Focolari Voting System - API Routes
"""
import time
from fastapi import APIRouter, HTTPException
from schemas import (
LoginRequest,
EntryRequest,
UserResponse,
RoomInfoResponse,
LoginResponse,
EntryResponse,
)
router = APIRouter()
# Timestamp di avvio del server (per invalidare sessioni frontend)
SERVER_START_TIME = int(time.time() * 1000)
# Dati caricati dinamicamente dal main
_data = {
"validator_password": "",
"room": {"room_name": "", "meeting_id": ""},
"users": []
}
# Caratteri sentinel da pulire (tutti i layout supportati)
SENTINEL_CHARS = [";", "?", "ò", "_", "ç", "+"]
def init_data(data: dict):
"""Inizializza i dati caricati dal file JSON"""
global _data
_data = data
def clean_badge(badge: str) -> str:
"""Rimuove i caratteri sentinel dal badge"""
clean = badge.strip()
for char in SENTINEL_CHARS:
clean = clean.replace(char, "")
return clean
def find_user(badge_code: str) -> dict | None:
"""Cerca un utente per badge code"""
clean = clean_badge(badge_code)
for user in _data["users"]:
if user["badge_code"] == clean or user["badge_code"].lstrip("0") == clean.lstrip("0"):
return user
return None
@router.get("/info-room", response_model=RoomInfoResponse)
async def get_room_info():
"""Restituisce le informazioni sulla sala e la riunione corrente."""
return RoomInfoResponse(
room_name=_data["room"]["room_name"],
meeting_id=_data["room"]["meeting_id"],
server_start_time=SERVER_START_TIME
)
@router.post("/login-validate", response_model=LoginResponse)
async def login_validate(request: LoginRequest):
"""
Valida la password del validatore.
Il badge viene passato dal frontend ma attualmente non viene verificato
lato server - serve solo per essere memorizzato nella sessione frontend.
TODO: Concordare con committenti se il badge debba essere verificato
anche lato server (es. lista badge validatori autorizzati).
"""
if request.password != _data["validator_password"]:
raise HTTPException(
status_code=401,
detail="Password non corretta"
)
# Il badge viene restituito per conferma, ma non è validato lato server
clean = clean_badge(request.badge) if request.badge else "unknown"
return LoginResponse(
success=True,
message="Login validatore effettuato con successo",
token=f"focolare-token-{clean}"
)
@router.get("/anagrafica/{badge_code}", response_model=UserResponse)
async def get_user_anagrafica(badge_code: str):
"""
Cerca un utente tramite il suo badge code.
"""
user = find_user(badge_code)
if not user:
clean = clean_badge(badge_code)
raise HTTPException(
status_code=404,
detail=f"Utente con badge {clean} non trovato nel sistema"
)
response = UserResponse(
badge_code=user["badge_code"],
nome=user["nome"],
cognome=user["cognome"],
url_foto=user["url_foto"],
ruolo=user["ruolo"],
ammesso=user["ammesso"]
)
if not user["ammesso"]:
response.warning = "ATTENZIONE: Questo utente NON è autorizzato all'ingresso!"
return response
@router.post("/entry-request", response_model=EntryResponse)
async def process_entry_request(request: EntryRequest):
"""
Processa una richiesta di ingresso.
Risposta asettica senza messaggi multilingua.
"""
if request.validator_password != _data["validator_password"]:
raise HTTPException(
status_code=401,
detail="Password validatore non corretta"
)
user = find_user(request.user_badge)
if not user:
clean = clean_badge(request.user_badge)
raise HTTPException(
status_code=404,
detail=f"Utente con badge {clean} non trovato"
)
if not user["ammesso"]:
raise HTTPException(
status_code=403,
detail=f"L'utente {user['nome']} {user['cognome']} NON è autorizzato all'ingresso"
)
return EntryResponse(
success=True,
message=f"Ingresso registrato per {user['nome']} {user['cognome']}"
)