Struttura progetto: - Backend mock Python (FastAPI) con API per gestione varchi - Frontend React + TypeScript + Vite + Tailwind CSS - Documentazione e piani di sviluppo Backend (backend-mock/): - API REST: /info-room, /login-validate, /anagrafica, /entry-request - Dati mock: 7 utenti, validatore (999999/focolari) - CORS abilitato, docs OpenAPI automatiche - Configurazione pipenv per ambiente virtuale Frontend (frontend/): - State machine completa per flusso accesso varco - Hook useRFIDScanner per lettura badge (pattern singolo) - Componenti UI: Logo, Button, Input, Modal, UserCard, Timer - Schermate: Loading, Login, ActiveGate, Success/Error Modal - Design system con colori Focolari - Ottimizzato per tablet touch Documentazione (ai-prompts/): - Welcome guide per futuri agenti - Piano sviluppo backend e frontend con checklist DA COMPLETARE: - Hook RFID multi-pattern (US/IT/altri layout tastiera) - Pagina /debug per diagnostica in loco - Logging console strutturato
278 lines
7.7 KiB
Python
278 lines
7.7 KiB
Python
"""
|
|
Focolari Voting System - Backend Mock
|
|
Sistema di controllo accessi per votazioni del Movimento dei Focolari
|
|
"""
|
|
|
|
import random
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
|
|
app = FastAPI(
|
|
title="Focolari Voting System API",
|
|
description="Backend mock per il sistema di controllo accessi",
|
|
version="1.0.0"
|
|
)
|
|
|
|
# CORS abilitato per tutti
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# ============================================
|
|
# MODELLI PYDANTIC
|
|
# ============================================
|
|
|
|
class LoginRequest(BaseModel):
|
|
badge: str
|
|
password: str
|
|
|
|
class EntryRequest(BaseModel):
|
|
user_badge: str
|
|
validator_password: str
|
|
|
|
class UserResponse(BaseModel):
|
|
badge_code: str
|
|
nome: str
|
|
cognome: str
|
|
url_foto: str
|
|
ruolo: str
|
|
ammesso: bool
|
|
warning: Optional[str] = None
|
|
|
|
class RoomInfoResponse(BaseModel):
|
|
room_name: str
|
|
meeting_id: str
|
|
|
|
class LoginResponse(BaseModel):
|
|
success: bool
|
|
message: str
|
|
token: Optional[str] = None
|
|
|
|
class EntryResponse(BaseModel):
|
|
success: bool
|
|
message: str
|
|
welcome_message: Optional[str] = None
|
|
|
|
# ============================================
|
|
# DATI MOCK
|
|
# ============================================
|
|
|
|
# Credenziali Validatore
|
|
VALIDATOR_BADGE = "999999"
|
|
VALIDATOR_PASSWORD = "focolari"
|
|
MOCK_TOKEN = "focolare-validator-token-2024"
|
|
|
|
# Lista utenti mock
|
|
USERS_DB = [
|
|
{
|
|
"badge_code": "000001",
|
|
"nome": "Maria",
|
|
"cognome": "Rossi",
|
|
"url_foto": "https://randomuser.me/api/portraits/women/1.jpg",
|
|
"ruolo": "Votante",
|
|
"ammesso": True
|
|
},
|
|
{
|
|
"badge_code": "000002",
|
|
"nome": "Giuseppe",
|
|
"cognome": "Bianchi",
|
|
"url_foto": "https://randomuser.me/api/portraits/men/2.jpg",
|
|
"ruolo": "Votante",
|
|
"ammesso": True
|
|
},
|
|
{
|
|
"badge_code": "000003",
|
|
"nome": "Anna",
|
|
"cognome": "Verdi",
|
|
"url_foto": "https://randomuser.me/api/portraits/women/3.jpg",
|
|
"ruolo": "Tecnico",
|
|
"ammesso": True
|
|
},
|
|
{
|
|
"badge_code": "000004",
|
|
"nome": "Francesco",
|
|
"cognome": "Neri",
|
|
"url_foto": "https://randomuser.me/api/portraits/men/4.jpg",
|
|
"ruolo": "Ospite",
|
|
"ammesso": False # Non ammesso!
|
|
},
|
|
{
|
|
"badge_code": "000005",
|
|
"nome": "Lucia",
|
|
"cognome": "Gialli",
|
|
"url_foto": "https://randomuser.me/api/portraits/women/5.jpg",
|
|
"ruolo": "Votante",
|
|
"ammesso": True
|
|
},
|
|
{
|
|
"badge_code": "000006",
|
|
"nome": "Paolo",
|
|
"cognome": "Blu",
|
|
"url_foto": "https://randomuser.me/api/portraits/men/6.jpg",
|
|
"ruolo": "Votante",
|
|
"ammesso": False # Non ammesso!
|
|
},
|
|
{
|
|
"badge_code": "123456",
|
|
"nome": "Teresa",
|
|
"cognome": "Martini",
|
|
"url_foto": "https://randomuser.me/api/portraits/women/7.jpg",
|
|
"ruolo": "Votante",
|
|
"ammesso": True
|
|
},
|
|
]
|
|
|
|
# Messaggi di benvenuto multilingua
|
|
WELCOME_MESSAGES = [
|
|
"Benvenuto! / Welcome!",
|
|
"Bienvenue! / Willkommen!",
|
|
"Bienvenido! / Bem-vindo!",
|
|
"欢迎! / 歓迎!",
|
|
"Добро пожаловать! / مرحبا!"
|
|
]
|
|
|
|
# ============================================
|
|
# ENDPOINTS
|
|
# ============================================
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
"""Endpoint di test per verificare che il server sia attivo"""
|
|
return {"status": "ok", "message": "Focolari Voting System API is running"}
|
|
|
|
|
|
@app.get("/info-room", response_model=RoomInfoResponse)
|
|
async def get_room_info():
|
|
"""
|
|
Restituisce le informazioni sulla sala e la riunione corrente.
|
|
"""
|
|
return RoomInfoResponse(
|
|
room_name="Sala Assemblea",
|
|
meeting_id="VOT-2024"
|
|
)
|
|
|
|
|
|
@app.post("/login-validate", response_model=LoginResponse)
|
|
async def login_validate(request: LoginRequest):
|
|
"""
|
|
Valida le credenziali del validatore.
|
|
Il badge deve essere quello del validatore (999999) e la password corretta.
|
|
"""
|
|
# Pulisci il badge da eventuali caratteri sentinel
|
|
clean_badge = request.badge.strip().replace(";", "").replace("?", "")
|
|
|
|
if clean_badge != VALIDATOR_BADGE:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="Badge validatore non riconosciuto"
|
|
)
|
|
|
|
if request.password != VALIDATOR_PASSWORD:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="Password non corretta"
|
|
)
|
|
|
|
return LoginResponse(
|
|
success=True,
|
|
message="Login validatore effettuato con successo",
|
|
token=MOCK_TOKEN
|
|
)
|
|
|
|
|
|
@app.get("/anagrafica/{badge_code}", response_model=UserResponse)
|
|
async def get_user_anagrafica(badge_code: str):
|
|
"""
|
|
Cerca un utente tramite il suo badge code.
|
|
Restituisce i dati anagrafici e un warning se non è ammesso.
|
|
"""
|
|
# Pulisci il badge da eventuali caratteri sentinel
|
|
clean_badge = badge_code.strip().replace(";", "").replace("?", "")
|
|
|
|
# Normalizza: rimuovi zeri iniziali per il confronto, ma cerca anche con zeri
|
|
for user in USERS_DB:
|
|
if user["badge_code"] == clean_badge or user["badge_code"].lstrip("0") == clean_badge.lstrip("0"):
|
|
response = UserResponse(
|
|
badge_code=user["badge_code"],
|
|
nome=user["nome"],
|
|
cognome=user["cognome"],
|
|
url_foto=user["url_foto"],
|
|
ruolo=user["ruolo"],
|
|
ammesso=user["ammesso"]
|
|
)
|
|
|
|
# Aggiungi warning se non ammesso
|
|
if not user["ammesso"]:
|
|
response.warning = "ATTENZIONE: Questo utente NON è autorizzato all'ingresso!"
|
|
|
|
return response
|
|
|
|
# Utente non trovato
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Utente con badge {clean_badge} non trovato nel sistema"
|
|
)
|
|
|
|
|
|
@app.post("/entry-request", response_model=EntryResponse)
|
|
async def process_entry_request(request: EntryRequest):
|
|
"""
|
|
Processa una richiesta di ingresso.
|
|
Richiede il badge dell'utente e la password del validatore.
|
|
"""
|
|
# Pulisci i dati
|
|
clean_user_badge = request.user_badge.strip().replace(";", "").replace("?", "")
|
|
|
|
# Verifica password validatore
|
|
if request.validator_password != VALIDATOR_PASSWORD:
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail="Password validatore non corretta"
|
|
)
|
|
|
|
# Cerca l'utente
|
|
user_found = None
|
|
for user in USERS_DB:
|
|
if user["badge_code"] == clean_user_badge or user["badge_code"].lstrip("0") == clean_user_badge.lstrip("0"):
|
|
user_found = user
|
|
break
|
|
|
|
if not user_found:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Utente con badge {clean_user_badge} non trovato"
|
|
)
|
|
|
|
if not user_found["ammesso"]:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail=f"L'utente {user_found['nome']} {user_found['cognome']} NON è autorizzato all'ingresso"
|
|
)
|
|
|
|
# Successo! Genera messaggio di benvenuto casuale
|
|
welcome = random.choice(WELCOME_MESSAGES)
|
|
|
|
return EntryResponse(
|
|
success=True,
|
|
message=f"Ingresso registrato per {user_found['nome']} {user_found['cognome']}",
|
|
welcome_message=welcome
|
|
)
|
|
|
|
|
|
# ============================================
|
|
# AVVIO SERVER
|
|
# ============================================
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
print("🚀 Avvio Focolari Voting System Backend...")
|
|
print("📍 Server in ascolto su http://localhost:8000")
|
|
print("📚 Documentazione API su http://localhost:8000/docs")
|
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|