"""FastAPI application assembly. Endpoints live in `dsh_sync.routers.*`, one module per resource. This file only wires configuration, middleware, and routers together. """ from __future__ import annotations from fastapi import FastAPI, Request from .auth import ensure_bootstrap_admin from .config import Config from .db import init_db from .ratelimit import SlidingWindow from .routers import audit, backup, connect, pages, plugins, settings, tokens, users from .util import now_iso # Security headers for every response. The app is served over plain HTTP on a # LAN, so HSTS is deliberately absent; `camera=(self)` is required because the # PWA's in-app scanner calls getUserMedia on the same origin. SECURITY_HEADERS = { "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "Referrer-Policy": "no-referrer", "Permissions-Policy": "geolocation=(), microphone=(), camera=(self)", } def create_app(cfg: Config | None = None) -> FastAPI: cfg = cfg or Config.from_env() cfg.ensure_dirs() init_db(cfg) ensure_bootstrap_admin(cfg) app = FastAPI(title="dsh-sync", version="0.1.0") app.state.cfg = cfg app.state.invite_limiter = SlidingWindow(limit=10, window_seconds=60) @app.middleware("http") async def add_security_headers(request: Request, call_next): response = await call_next(request) for k, v in SECURITY_HEADERS.items(): response.headers.setdefault(k, v) return response @app.get("/healthz") def healthz(): return {"status": "ok", "time": now_iso()} for module in (pages, connect, tokens, users, settings, plugins, audit, backup): app.include_router(module.router) return app app = create_app()