Dashboard: accounts totals only; log client IPs per request (#140)
Dashboard ACCOUNTS section now shows only totals (users, active/total subscriptions, notifications, push subs) — the per-user and per-subscription lists are gone, and the dashboard no longer queries that PII at all. Backend: add a per-request access log (logs/access/access-<date>.jsonl) recording the client IP (real IP via the left-most X-Forwarded-For hop when proxied behind Caddy, else the peer address), method, path, status, and category. Static assets and the dashboard's own metrics polling are skipped. Best-effort, never raises into the request path — retained for later traffic/IP analysis.
This commit is contained in:
parent
19f4965258
commit
5222ae3067
2 changed files with 24 additions and 1 deletions
17
app.py
17
app.py
|
|
@ -141,6 +141,15 @@ app = FastAPI(title="Thermograph", version="0.2.0", lifespan=_lifespan)
|
|||
app.add_middleware(GZipMiddleware, minimum_size=1024)
|
||||
|
||||
|
||||
def _client_ip(request) -> "str | None":
|
||||
"""The real client IP: the left-most X-Forwarded-For hop when a proxy fronts us
|
||||
(Caddy on prod sets it), otherwise the direct peer address (LAN dev)."""
|
||||
xff = request.headers.get("x-forwarded-for")
|
||||
if xff:
|
||||
return xff.split(",")[0].strip()
|
||||
return request.client.host if request.client else None
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def revalidate_static(request, call_next):
|
||||
"""Serve the frontend with no-cache (NOT no-store): browsers may keep a copy
|
||||
|
|
@ -152,7 +161,13 @@ async def revalidate_static(request, call_next):
|
|||
response = await call_next(request)
|
||||
path = request.url.path
|
||||
try:
|
||||
metrics.record_inbound(metrics.classify_inbound(path, BASE), response.status_code)
|
||||
cat = metrics.classify_inbound(path, BASE)
|
||||
metrics.record_inbound(cat, response.status_code)
|
||||
# Retain per-request client IPs for later analysis; skip static assets and the
|
||||
# dashboard's own metrics polling to keep the log to real, meaningful traffic.
|
||||
if cat not in ("static", "metrics"):
|
||||
audit.log_access({"ip": _client_ip(request), "method": request.method,
|
||||
"path": path, "status": response.status_code, "cat": cat})
|
||||
except Exception: # noqa: BLE001 - never let instrumentation break a response
|
||||
pass
|
||||
pages = (BASE, f"{BASE}/", f"{BASE}/calendar", f"{BASE}/day", f"{BASE}/compare", f"{BASE}/legend", f"{BASE}/alerts")
|
||||
|
|
|
|||
8
audit.py
8
audit.py
|
|
@ -26,6 +26,7 @@ import uuid
|
|||
LOG_ROOT = os.path.join(os.path.dirname(__file__), "..", "logs")
|
||||
AUDIT_DIR = os.path.join(LOG_ROOT, "audit")
|
||||
ERROR_DIR = os.path.join(LOG_ROOT, "errors")
|
||||
ACCESS_DIR = os.path.join(LOG_ROOT, "access")
|
||||
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
|
@ -65,6 +66,13 @@ def log_event(tag: str, record: dict) -> dict:
|
|||
return rec
|
||||
|
||||
|
||||
def log_access(record: dict) -> None:
|
||||
"""Append one request line (client IP, method, path, status) to the daily access
|
||||
log, so request/IP data is retained for later analysis. Best-effort; never raises
|
||||
into the request path."""
|
||||
_append(os.path.join(ACCESS_DIR, f"access-{_today()}.jsonl"), {"ts": _now_iso(), **record})
|
||||
|
||||
|
||||
class RunAudit:
|
||||
"""Times a graded run and writes one audit line when the ``with`` block exits.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue