"""Composition de l'application Kazeia-central (FastAPI). `create_app` câble le contexte (adb, store chiffré, bridge voix, autosync) et **compose les routers de features** (operator, fleet, voices). Ajouter un domaine = créer `features//router.py` exposant `make_router(ctx)` et l'enregistrer ci-dessous. Lancer : `python -m kazeia_central` (host/port via env) ou `uvicorn kazeia_central.app:app`. """ from __future__ import annotations import base64 import os import secrets from pathlib import Path from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from starlette.responses import Response from .core.adb import Adb from .core.context import VERSION, Ctx from .core.operator_router import make_router as operator_router from .core.store import Store from .features.fleet.router import make_router as fleet_router from .features.voices.autosync import VoiceAutoSync from .features.voices.bridge import VoiceBridge from .features.voices.router import make_router as voices_router _WEB_DIR = Path(__file__).resolve().parent / "web" _STORE_PATH = Path(os.environ.get("KAZEIA_STORE", Path.cwd() / "data" / "central.db")) _OPERATOR = os.environ.get("KAZEIA_USER") or "local" # Routers de features à composer (ordre libre ; les littéraux vs /{serial} sont gérés # DANS chaque router). Ajouter une feature = ajouter sa fabrique ici. _FEATURES = (operator_router, fleet_router, voices_router) def _install_basic_auth(app: FastAPI) -> None: """Auth HTTP Basic optionnelle, active SEULEMENT si KAZEIA_USER+KAZEIA_PASS sont définis. Couvre tout (UI statique incluse) via middleware — pour une exposition réseau (0.0.0.0). Sans elle, l'app reste ouverte.""" user, pwd = os.environ.get("KAZEIA_USER"), os.environ.get("KAZEIA_PASS") if not (user and pwd): return @app.middleware("http") async def _basic_auth(request, call_next): hdr = request.headers.get("authorization", "") ok = False if hdr.startswith("Basic "): try: u, _, p = base64.b64decode(hdr[6:]).decode("utf-8").partition(":") ok = secrets.compare_digest(u, user) and secrets.compare_digest(p, pwd) except Exception: ok = False if not ok: return Response("Authentification requise", status_code=401, headers={"WWW-Authenticate": 'Basic realm="Kazeia-central"'}) return await call_next(request) def create_app(adb: Adb | None = None, store: Store | None = None, bridge: VoiceBridge | None = None, *, start_autosync: bool = True) -> FastAPI: adb = adb or Adb() if store is None: _STORE_PATH.parent.mkdir(parents=True, exist_ok=True) store = Store(_STORE_PATH) bridge = bridge or VoiceBridge() autosync = VoiceAutoSync(adb, store, bridge) if start_autosync: autosync.start() # désarmé par défaut : ne fait que suivre les connexions app = FastAPI(title="Kazeia-central", version=VERSION) _install_basic_auth(app) ctx = Ctx(adb=adb, store=store, bridge=bridge, autosync=autosync, operator=_OPERATOR) for make_router in _FEATURES: app.include_router(make_router(ctx)) # UI web servie à la racine — APRÈS les routes /api (le mount "/" capte le reste). if _WEB_DIR.is_dir(): app.mount("/", StaticFiles(directory=str(_WEB_DIR), html=True), name="web") return app app = create_app()