"""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",
},
)