56 lines
1.7 KiB
Python
56 lines
1.7 KiB
Python
"""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()
|