Access-log extras: remote-user on /_api, visitor info on /_ws

This commit is contained in:
2026-09-03 19:38:15 +00:00
parent 38af57218a
commit d6d07db2c5
2 changed files with 32 additions and 1 deletions
+25
View File
@@ -37,6 +37,7 @@ from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import Response
from fastapi_vue import Frontend
from starlette.types import ASGIApp, Receive, Scope, Send
from pagerite import api, files, pages, tracking
from pagerite.__main__ import DEVMODE
@@ -53,6 +54,28 @@ frontend = Frontend(
)
class _AccessLogExtraMiddleware:
"""Fill the ``log_extra`` slot of fastapi_vue's access log.
Everything under ``/_api`` is gated by the SSO forward-auth, which names
the authenticated user in the ``remote-user`` header; put that user on
the access-log line, for plain requests and WebSocket open/close alike.
The scope dict is shared with the outer AccessLogMiddleware, which reads
the slot back at response/accept/close time.
"""
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] in ("http", "websocket") and scope["path"].startswith("/_api"):
headers = dict(scope["headers"])
user = headers.get(b"remote-user", b"").decode("latin-1")
if user:
scope.setdefault("state", {})["log_extra"] = user
await self.app(scope, receive, send)
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator:
"""Open the database (migrations run inside kanta.open), load assets, load GeoIP."""
@@ -83,6 +106,8 @@ app = FastAPI(
openapi_url=None,
)
app.add_middleware(_AccessLogExtraMiddleware)
@app.middleware("http")
async def _headers(request: Request, call_next) -> Response:
+7 -1
View File
@@ -435,10 +435,16 @@ async def activity_ws(ws: WebSocket) -> None:
DB-IP geoip lookups happen in background tasks so message handling is
never delayed by slow DNS or the first MMDB decompress.
"""
await ws.accept()
ip = _client_ip(ws)
ua = ws.headers.get("user-agent", "")
accept_language = ws.headers.get("accept-language", "")
# Identify the visitor on the access-log open/close lines (the IP is
# already printed there): compact UA plus the browser's language tag.
lang, _country = analytics._parse_accept_language(accept_language)
ws.scope.setdefault("state", {})["log_extra"] = " ".join(
part for part in (analytics._compact_user_agent(ua), lang) if part
)
await ws.accept()
try:
while True:
text = await ws.receive_text()