"""The HTML pages, plus the two health endpoints. There is no template engine. The only thing a template would inject is the `next` parameter and a status message, both of which the page reads from location.search -- so these are static files and the JS does the rest. """ from __future__ import annotations import logging from pathlib import Path from fastapi import APIRouter, Depends, Request, status from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, Response from app import db as db_module from app.deps import current_user from app.models import User log = logging.getLogger(__name__) router = APIRouter(tags=["pages"]) WEB = Path(__file__).resolve().parent.parent.parent / "web" NO_STORE = {"Cache-Control": "no-store"} def page(name: str) -> FileResponse: return FileResponse(WEB / name, media_type="text/html; charset=utf-8", headers=NO_STORE) @router.get("/", include_in_schema=False) def index(user: User | None = Depends(current_user)) -> Response: # Redirect on the server. Gating in JS would flash the whole app shell # before bouncing, and would show nothing at all with scripting disabled. if user is None: return RedirectResponse("/login", status_code=status.HTTP_303_SEE_OTHER) return page("index.html") @router.get("/login", include_in_schema=False) def login_page(request: Request, user: User | None = Depends(current_user)) -> Response: if user is not None: nxt = request.query_params.get("next") or "/" if not nxt.startswith("/") or nxt.startswith("//"): nxt = "/" # only ever redirect within this site return RedirectResponse(nxt, status_code=status.HTTP_303_SEE_OTHER) return page("login.html") @router.get("/signup", include_in_schema=False) def signup_page(user: User | None = Depends(current_user)) -> Response: if user is not None: return RedirectResponse("/", status_code=status.HTTP_303_SEE_OTHER) return page("signup.html") @router.get("/forgot", include_in_schema=False) def forgot_page() -> Response: return page("forgot.html") # Both of these are deliberately inert: they render, read the token out of the # query string, and POST it. A mail scanner that GETs every link in a message # therefore cannot spend the token before the recipient opens it. @router.get("/reset", include_in_schema=False) def reset_page() -> Response: return page("reset.html") @router.get("/verify", include_in_schema=False) def verify_page() -> Response: return page("verify.html") @router.get("/admin", include_in_schema=False) def admin_page(user: User | None = Depends(current_user)) -> Response: if user is None: return RedirectResponse("/login?next=/admin", status_code=status.HTTP_303_SEE_OTHER) if not user.is_admin: return RedirectResponse("/", status_code=status.HTTP_303_SEE_OTHER) return page("admin.html") @router.get("/healthz", include_in_schema=False) def healthz() -> JSONResponse: """Liveness only. Deliberately does not touch MySQL: if it did, a thirty second RDS failover would restart the container instead of just failing the handful of routes that actually need the database.""" return JSONResponse({"status": "ok"}, headers=NO_STORE) @router.get("/readyz", include_in_schema=False) def readyz() -> JSONResponse: try: db_module.ping() except Exception as exc: # noqa: BLE001 - report, don't raise log.warning("readiness check failed: %s", exc) return JSONResponse({"db": "unreachable"}, status_code=status.HTTP_503_SERVICE_UNAVAILABLE, headers=NO_STORE) return JSONResponse({"db": "ok"}, headers=NO_STORE)