"""Serving what an administrator customised. Both routes here are deliberately **unauthenticated**, and for the same reason the manifest and the offline page are: the sign-in page needs the logo before anybody has signed in, and a browser fetches a stylesheet and a launcher icon outside any page's session. What that exposes is a file an administrator uploaded on purpose to be shown to everybody, under a random filename, in a format that cannot execute in an `` — `services/uploads.py:ALLOWED_TYPES` is what makes the last part true, and it is why SVG is not in it. """ from __future__ import annotations from fastapi import APIRouter, HTTPException, Response, status from fastapi.responses import FileResponse from lembas.services import branding as branding_service from lembas.services import uploads router = APIRouter(tags=["branding"]) @router.get("/branding.css", include_in_schema=False) async def branding_css() -> Response: """The custom themes and the custom CSS. A route rather than an inline `` away from being a script on every page. Cached hard and busted by a query string. `base.html` links this with `?v={{ brand.revision }}`, a hash of everything below, so the URL changes exactly when the stylesheet does. Without that the browser's cache is what decides when a rebrand takes effect, which is a save that looks like it worked and did nothing. """ brand = branding_service.snapshot() return Response( branding_service.stylesheet(brand), media_type="text/css", headers={"Cache-Control": "public, max-age=604800"}, ) @router.get("/branding/{filename}", include_in_schema=False) async def branding_asset(filename: str) -> Response: """A logo, a favicon, or a launcher icon derived from one.""" path = uploads.branding_image_path(filename) if path is None: raise HTTPException(status.HTTP_404_NOT_FOUND, "No such file.") return FileResponse( path, media_type=uploads.media_type_for(filename), # Public, unlike a model avatar: this is served to somebody who is not # signed in, so there is nothing private to keep out of a shared cache. # Names are random, so a replacement is a new URL. headers={ "Cache-Control": "public, max-age=604800", "X-Content-Type-Options": "nosniff", }, )