89 lines
3.5 KiB
Python
89 lines
3.5 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.patients.router import make_router as patients_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)
|
|
|
|
|
|
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()
|