app.web

Serve the built SPA and its runtime configuration from the API process.

Mounted by app.main.create_app() only when ADRIANO_STATIC_DIR is set (the single container image sets it to the built dist/). Routing, with the API keeping precedence:

  • every API route (/api/..., /healthz, /openapi.json, /api/docs, /auth/dev/...) is matched first and behaves as without the SPA;

  • GET /config.js is rendered once from Settings (module-runtime-config-spa): window.__APP_CONFIG__ = {...}; with exactly the keys of frontend/src/config.ts’s RuntimeConfig. It never carries a dev-auth flag;

  • /assets/* (content-hashed bundle files) are served with a one-year immutable cache;

  • any other GET/HEAD that matches no route serves an existing top-level file of the SPA directory (e.g. favicon.svg) or else index.html with no-cache, so history-mode deep links and /auth/callback work. Unknown paths under the API prefixes (and missing assets) stay plain JSON 404s.

The fallback is a 404 exception handler rather than a catch-all route, so it never shadows a route and never turns an API 405 into something else.

SecurityHeadersMiddleware adds the headers nginx used to set to every response (see content_security_policy()).

Public API: mount_spa(), render_config_js(), content_security_policy(), SecurityHeadersMiddleware.

app.web.NO_FALLBACK_PREFIXES = ('/api', '/auth/dev', '/healthz', '/openapi.json', '/assets', '/config.js')

Path prefixes that never fall back to index.html: an unknown path below them is a JSON 404. A prefix matches itself and anything below <prefix>/.

app.web.DOCS_PATHS = ('/api/docs', '/redoc')

Interactive API documentation pages (FastAPI’s Swagger UI and ReDoc), which load their scripts and styles from a CDN and use inline scripts.

app.web.DOCS_CSP = "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https://fastapi.tiangolo.com https://cdn.redoc.ly; worker-src 'self' blob:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"

CSP for the documentation pages: the SPA policy would block the CDN assets and inline scripts of Swagger UI / ReDoc. Still no framing, no foreign connections, no base or form targets.

app.web.content_security_policy(idp_origin)

Build the SPA’s Content-Security-Policy.

connect-src: oidc-client-ts fetches the IdP discovery document, token and userinfo endpoints from the browser. frame-src: its silent renew loads the IdP authorize endpoint in a hidden iframe. Vuetify sets theme CSS variables through inline style attributes, which needs style-src 'unsafe-inline' (there is no nonce mechanism for style attributes).

Parameters:

idp_origin (str | None) – scheme://host[:port] of the OIDC authority, already validated by Settings; None keeps both directives at 'self'.

Return type:

str

Returns:

The header value.

app.web.render_config_js(settings)

Render /config.js from the settings.

The object is JSON-encoded (no template substitution, so no injection), and every < is escaped so no value can close a <script> element even if the file is ever inlined. Unset values are empty strings, which frontend/src/config.ts treats as unset. apiBaseUrl is always "": the API is same-origin.

Parameters:

settings (Settings) – Application settings.

Return type:

str

Returns:

The JavaScript source.

class app.web.SecurityHeadersMiddleware(app, csp)

Bases: object

Add the security headers to every HTTP response.

Applied to API responses as well: a CSP on JSON is harmless and protects against a response ever being rendered as a document. The documentation pages (DOCS_PATHS) get DOCS_CSP instead. A header the response already sets is left alone.

Parameters:
app.web.mount_spa(app, settings)

Serve the SPA, /config.js and the security headers.

Call after every API router is registered.

Parameters:
  • app (FastAPI) – The application under construction.

  • settings (Settings) – Application settings; static_dir must be set.

Raises:

ConfigurationError – If static_dir is unset or holds no index.html.

Return type:

None