Kazeia-central/kazeia_central/app.py

93 lines
3.8 KiB
Python

"""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/<x>/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.medical.conversations.router import make_router as conversations_router
from .features.medical.patients.router import make_router as patients_router
from .features.medical.questionnaires.router import make_router as questionnaires_router
from .features.medical.rag.router import make_router as rag_router
from .features.technical.fleet.router import make_router as fleet_router
from .features.technical.voices.autosync import VoiceAutoSync
from .features.technical.voices.bridge import VoiceBridge
from .features.technical.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, patients_router,
conversations_router, rag_router, questionnaires_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()