kirche-einfach/backend/app/main.py

84 lines
2.8 KiB
Python

import asyncio
import logging
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from starlette.middleware.body_limit import RequestBodyLimitMiddleware
from .auth.api import access_router, auth_router
from .flows.api import approvals_router, flows_router
from .flows.sync import format_report, run_sync_blocking, watch_forever
from .middleware import RunThrottleMiddleware, SecurityHeadersMiddleware
from .settings import settings
logger = logging.getLogger(__name__)
def _blocking_startup_sync() -> None:
"""One-shot reconciliation at startup (fresh session, never fatal)."""
try:
report = run_sync_blocking(Path(settings.flows_dir))
logger.info("startup flow sync: %s", format_report(report))
for filename, reason in report.errors.items():
logger.warning("startup flow sync: %s: %s", filename, reason)
except FileNotFoundError as exc:
logger.error("startup flow sync skipped: %s", exc)
except Exception:
logger.exception("startup flow sync failed") # never fatal at boot
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
task: asyncio.Task | None = None
stop: asyncio.Event | None = None
if settings.flows_watch_enabled:
await asyncio.to_thread(_blocking_startup_sync)
stop = asyncio.Event()
task = asyncio.create_task(
watch_forever(stop, Path(settings.flows_dir)), name="flows-watcher"
)
yield
if task is not None and stop is not None:
stop.set()
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
app = FastAPI(
title="Kirche Einfach API",
description="KI-gestützte Verwaltungsplattform für Kirche",
version="0.1.0",
# Interactive docs only in debug: /api/docs maps the whole surface.
docs_url="/api/docs" if settings.debug else None,
redoc_url="/api/redoc" if settings.debug else None,
openapi_url="/api/openapi.json" if settings.debug else None,
lifespan=lifespan,
)
# TLS terminates at the reverse proxy: uvicorn --proxy-headers (see
# entrypoint.sh) trusts its forwarding headers so Secure cookies, client
# IPs (throttle) and absolute URLs are correct. The API is not directly
# internet-reachable (proxy-only), so trusting them is safe.
app.add_middleware(RequestBodyLimitMiddleware, max_body_size=settings.max_body_bytes)
app.add_middleware(RunThrottleMiddleware)
app.add_middleware(SecurityHeadersMiddleware)
app.include_router(flows_router)
app.include_router(approvals_router)
app.include_router(access_router)
app.include_router(auth_router)
@app.get("/")
async def root():
return {"message": "Kirche Einfach API", "version": settings.version}
@app.get("/health")
async def health():
return {"status": "ok"}