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:
2
backend-mock/.gitignore
vendored
2
backend-mock/.gitignore
vendored
@@ -28,3 +28,5 @@ Thumbs.db
|
||||
# Local environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
fastapi = ">=0.109.0"
|
||||
uvicorn = {extras = ["standard"], version = ">=0.27.0"}
|
||||
pydantic = ">=2.5.0"
|
||||
fastapi = "*"
|
||||
uvicorn = "*"
|
||||
pydantic = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.10"
|
||||
python_version = "3.14"
|
||||
|
||||
[scripts]
|
||||
start = "python main.py"
|
||||
|
||||
7
backend-mock/api/__init__.py
Normal file
7
backend-mock/api/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
Focolari Voting System - API Package
|
||||
"""
|
||||
|
||||
from .routes import router, init_data
|
||||
|
||||
__all__ = ["router", "init_data"]
|
||||
153
backend-mock/api/routes.py
Normal file
153
backend-mock/api/routes.py
Normal 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']}"
|
||||
)
|
||||
33
backend-mock/data/users_default.json
Normal file
33
backend-mock/data/users_default.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"validator_password": "focolari",
|
||||
"room": {
|
||||
"room_name": "Sala Assemblea",
|
||||
"meeting_id": "VOT-2024"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"badge_code": "0008988288",
|
||||
"nome": "Marco",
|
||||
"cognome": "Bianchi",
|
||||
"url_foto": "https://randomuser.me/api/portraits/men/1.jpg",
|
||||
"ruolo": "Votante",
|
||||
"ammesso": true
|
||||
},
|
||||
{
|
||||
"badge_code": "0007399575",
|
||||
"nome": "Laura",
|
||||
"cognome": "Rossi",
|
||||
"url_foto": "https://randomuser.me/api/portraits/women/2.jpg",
|
||||
"ruolo": "Votante",
|
||||
"ammesso": true
|
||||
},
|
||||
{
|
||||
"badge_code": "0000514162",
|
||||
"nome": "Giuseppe",
|
||||
"cognome": "Verdi",
|
||||
"url_foto": "https://randomuser.me/api/portraits/men/3.jpg",
|
||||
"ruolo": "Tecnico",
|
||||
"ammesso": false
|
||||
}
|
||||
]
|
||||
}
|
||||
33
backend-mock/data/users_test.json
Normal file
33
backend-mock/data/users_test.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"validator_password": "test123",
|
||||
"room": {
|
||||
"room_name": "Sala Test",
|
||||
"meeting_id": "TEST-001"
|
||||
},
|
||||
"users": [
|
||||
{
|
||||
"badge_code": "111111",
|
||||
"nome": "Test",
|
||||
"cognome": "Ammesso",
|
||||
"url_foto": "https://randomuser.me/api/portraits/lego/1.jpg",
|
||||
"ruolo": "Votante",
|
||||
"ammesso": true
|
||||
},
|
||||
{
|
||||
"badge_code": "222222",
|
||||
"nome": "Test",
|
||||
"cognome": "NonAmmesso",
|
||||
"url_foto": "https://randomuser.me/api/portraits/lego/2.jpg",
|
||||
"ruolo": "Ospite",
|
||||
"ammesso": false
|
||||
},
|
||||
{
|
||||
"badge_code": "333333",
|
||||
"nome": "Test",
|
||||
"cognome": "Tecnico",
|
||||
"url_foto": "https://randomuser.me/api/portraits/lego/3.jpg",
|
||||
"ruolo": "Tecnico",
|
||||
"ammesso": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,277 +1,191 @@
|
||||
"""
|
||||
Focolari Voting System - Backend Mock
|
||||
Sistema di controllo accessi per votazioni del Movimento dei Focolari
|
||||
|
||||
Utilizzo:
|
||||
python main.py # Default: porta 8000, dati default
|
||||
python main.py -p 9000 # Porta custom
|
||||
python main.py -d data/users_test.json # Dataset custom
|
||||
python main.py -p 9000 -d data/users_test.json
|
||||
"""
|
||||
|
||||
import random
|
||||
from fastapi import FastAPI, HTTPException
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
app = FastAPI(
|
||||
title="Focolari Voting System API",
|
||||
description="Backend mock per il sistema di controllo accessi",
|
||||
version="1.0.0"
|
||||
)
|
||||
from api.routes import router, init_data
|
||||
|
||||
# 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"}
|
||||
# Configurazione default
|
||||
DEFAULT_PORT = 8000
|
||||
DEFAULT_HOST = "0.0.0.0"
|
||||
DEFAULT_DATA = "data/users_default.json"
|
||||
STATIC_DIR = Path(__file__).parent.parent / "frontend" / "dist"
|
||||
|
||||
|
||||
@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"
|
||||
def load_data(data_path: str) -> dict:
|
||||
"""Carica i dati dal file JSON"""
|
||||
path = Path(data_path)
|
||||
|
||||
if not path.is_absolute():
|
||||
# Path relativo alla directory del main.py
|
||||
base_dir = Path(__file__).parent
|
||||
path = base_dir / path
|
||||
|
||||
if not path.exists():
|
||||
print(f"❌ Errore: File dati non trovato: {path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
print(f"📂 Dati caricati da: {path}")
|
||||
print(f" - Password validatore: {'*' * len(data['validator_password'])}")
|
||||
print(f" - Sala: {data['room']['room_name']}")
|
||||
print(f" - Utenti: {len(data['users'])}")
|
||||
return data
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Errore parsing JSON: {e}")
|
||||
sys.exit(1)
|
||||
except KeyError as e:
|
||||
print(f"❌ Errore struttura JSON: chiave mancante {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def create_app(data: dict, serve_frontend: bool = True) -> FastAPI:
|
||||
"""Crea e configura l'applicazione FastAPI"""
|
||||
app = FastAPI(
|
||||
title="Focolari Voting System API",
|
||||
description="Backend mock per il sistema di controllo accessi",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
# CORS abilitato per tutti (utile in sviluppo)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Inizializza i dati nelle routes
|
||||
init_data(data)
|
||||
|
||||
@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("?", "")
|
||||
# Registra le routes API
|
||||
app.include_router(router)
|
||||
|
||||
# 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"]
|
||||
)
|
||||
# Serve frontend statico se la cartella esiste
|
||||
if serve_frontend and STATIC_DIR.exists():
|
||||
print(f"🌐 Frontend statico servito da: {STATIC_DIR}")
|
||||
|
||||
# Aggiungi warning se non ammesso
|
||||
if not user["ammesso"]:
|
||||
response.warning = "ATTENZIONE: Questo utente NON è autorizzato all'ingresso!"
|
||||
# Serve index.html per la root e tutte le route SPA
|
||||
@app.get("/")
|
||||
async def serve_index():
|
||||
return FileResponse(STATIC_DIR / "index.html")
|
||||
|
||||
return response
|
||||
@app.get("/debug")
|
||||
async def serve_debug():
|
||||
return FileResponse(STATIC_DIR / "index.html")
|
||||
|
||||
# Utente non trovato
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Utente con badge {clean_badge} non trovato nel sistema"
|
||||
# Monta i file statici (JS, CSS, assets) - DEVE essere dopo le route
|
||||
app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets")
|
||||
|
||||
# Fallback per altri file statici nella root (favicon, ecc.)
|
||||
@app.get("/{filename:path}")
|
||||
async def serve_static(filename: str):
|
||||
file_path = STATIC_DIR / filename
|
||||
if file_path.exists() and file_path.is_file():
|
||||
return FileResponse(file_path)
|
||||
# Per route SPA sconosciute, serve index.html
|
||||
return FileResponse(STATIC_DIR / "index.html")
|
||||
else:
|
||||
# API-only mode
|
||||
@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",
|
||||
"room": data["room"]["room_name"],
|
||||
"frontend": "not built - run 'npm run build' in frontend/"
|
||||
}
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse degli argomenti da linea di comando"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Focolari Voting System - Backend Mock Server",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Esempi:
|
||||
python main.py # Avvio standard
|
||||
python main.py -p 9000 # Porta 9000
|
||||
python main.py -d data/users_test.json # Dataset test
|
||||
python main.py --host 127.0.0.1 -p 8080 # Solo localhost
|
||||
python main.py --api-only # Solo API, no frontend
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
parser.add_argument(
|
||||
"-p", "--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Porta del server (default: {DEFAULT_PORT})"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-d", "--data",
|
||||
type=str,
|
||||
default=DEFAULT_DATA,
|
||||
help=f"Path al file JSON con i dati (default: {DEFAULT_DATA})"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
type=str,
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host di binding (default: {DEFAULT_HOST})"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--api-only",
|
||||
action="store_true",
|
||||
help="Avvia solo le API senza servire il frontend"
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point principale"""
|
||||
args = parse_args()
|
||||
|
||||
print("🚀 Avvio Focolari Voting System Backend...")
|
||||
print("=" * 50)
|
||||
|
||||
# Carica i dati
|
||||
data = load_data(args.data)
|
||||
|
||||
print("=" * 50)
|
||||
print(f"📍 Server in ascolto su http://{args.host}:{args.port}")
|
||||
print(f"📚 Documentazione API su http://{args.host}:{args.port}/docs")
|
||||
if not args.api_only and STATIC_DIR.exists():
|
||||
print(f"🌐 Frontend disponibile su http://{args.host}:{args.port}/")
|
||||
print("=" * 50)
|
||||
|
||||
# Crea e avvia l'app
|
||||
app = create_app(data, serve_frontend=not args.api_only)
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
# ============================================
|
||||
# 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)
|
||||
main()
|
||||
|
||||
21
backend-mock/schemas/__init__.py
Normal file
21
backend-mock/schemas/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Focolari Voting System - Schemas Package
|
||||
"""
|
||||
|
||||
from .models import (
|
||||
LoginRequest,
|
||||
EntryRequest,
|
||||
UserResponse,
|
||||
RoomInfoResponse,
|
||||
LoginResponse,
|
||||
EntryResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LoginRequest",
|
||||
"EntryRequest",
|
||||
"UserResponse",
|
||||
"RoomInfoResponse",
|
||||
"LoginResponse",
|
||||
"EntryResponse",
|
||||
]
|
||||
50
backend-mock/schemas/models.py
Normal file
50
backend-mock/schemas/models.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Focolari Voting System - Modelli Pydantic
|
||||
"""
|
||||
|
||||
from typing import Optional, Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
"""Richiesta login validatore"""
|
||||
badge: str
|
||||
password: str
|
||||
|
||||
|
||||
class EntryRequest(BaseModel):
|
||||
"""Richiesta ingresso partecipante"""
|
||||
user_badge: str
|
||||
validator_password: str
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""Risposta dati utente"""
|
||||
badge_code: str
|
||||
nome: str
|
||||
cognome: str
|
||||
url_foto: str
|
||||
ruolo: Literal["Tecnico", "Votante", "Ospite"]
|
||||
ammesso: bool
|
||||
warning: Optional[str] = None
|
||||
|
||||
|
||||
class RoomInfoResponse(BaseModel):
|
||||
"""Risposta info sala"""
|
||||
room_name: str
|
||||
meeting_id: str
|
||||
server_start_time: int # Timestamp avvio server per invalidare sessioni
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
"""Risposta login"""
|
||||
success: bool
|
||||
message: str
|
||||
token: Optional[str] = None
|
||||
|
||||
|
||||
class EntryResponse(BaseModel):
|
||||
"""Risposta richiesta ingresso (asettica, senza welcome_message)"""
|
||||
success: bool
|
||||
message: str
|
||||
Reference in New Issue
Block a user