feat: setup iniziale sistema controllo accessi Focolari

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
This commit is contained in:
2026-01-17 18:20:55 +01:00
commit 21b509c6ba
40 changed files with 7453 additions and 0 deletions

277
backend-mock/main.py Normal file
View File

@@ -0,0 +1,277 @@
"""
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)