Admins (Microsoft 365 sign-in names in BACKUP_DASH_ADMINS) get an Admin link to /admin.html, where they can create, list and revoke the API keys collectors use, instead of running manage.py on the server. Backend - /api/v1/admin/keys (list, create, revoke), admins only (403 otherwise); the user comes from the headers nginx sets after sign-in - Changes need an X-Requested-By header, which another site's page can't add, so a signed-in admin's browser can't be used to forge them - A new key is returned once; only its hash is stored. Names are unique for good, so revoked keys' history stays unambiguous - Keys record who created/revoked them and the company they last reported for (schema v2; databases migrate automatically on start) - /api/v1/me reports is_admin; manage.py shares the key code and records itself as creator/revoker Frontend - admin.html / admin.js: name validation shown after interaction (:user-invalid, aria-invalid kept in sync), the one-time key with a copy button (falls back to selecting it if clipboard access is refused), a key table flagging keys quiet for over 24 hours, and revoking confirmed in a <dialog>; results announced in a status region - Shared el() and signed-in helpers moved to dom.js / signed-in.js - Mock mode (?mock=1) includes sample keys Tested: 15 new backend tests (access, forged requests, names, duplicates, revoking, last-used company, v1 -> v2 upgrade); in the browser, every admin flow in mock mode and at phone width; end to end through the nginx config with the real API (a key created on the API works for ingest; revoking it makes ingest 401; non-admins see an explanation and no Admin link). Co-Authored-By: Claude Opus 5.5 <[email protected]>
52 lines
1.9 KiB
Python
52 lines
1.9 KiB
Python
import os
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
# The modal shows this many days of run history; retention can never be shorter.
|
|
HISTORY_DAYS = 30
|
|
|
|
# Shared by the systemd service (EnvironmentFile=) and manage.py, so both always use the same DB.
|
|
DEFAULT_ENV_FILE = "/etc/backup-dashboard/backup-dashboard.env"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Settings:
|
|
db_path: Path
|
|
retention_days: int = 90
|
|
# Dev only: serve ../public from the API so you can run without nginx.
|
|
serve_static: bool = False
|
|
# Microsoft 365 sign-in names allowed into the admin pages (lowercase)
|
|
admins: frozenset[str] = frozenset()
|
|
|
|
|
|
def read_env_file(path: Path) -> dict[str, str]:
|
|
"""Parse simple KEY=VALUE lines (the subset systemd's EnvironmentFile accepts)."""
|
|
values = {}
|
|
try:
|
|
lines = path.read_text().splitlines()
|
|
except FileNotFoundError:
|
|
return values
|
|
for line in lines:
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, _, value = line.partition("=")
|
|
values[key.strip()] = value.strip().strip("\"'")
|
|
return values
|
|
|
|
|
|
def load_settings() -> Settings:
|
|
# Real environment variables win over the env file; the env file wins over dev defaults.
|
|
env = read_env_file(Path(os.environ.get("BACKUP_DASH_ENV_FILE", DEFAULT_ENV_FILE))) | os.environ
|
|
return Settings(
|
|
db_path=Path(env.get("BACKUP_DASH_DB", "backup-dashboard.db")),
|
|
retention_days=max(HISTORY_DAYS, int(env.get("BACKUP_DASH_RETENTION_DAYS", "90"))),
|
|
serve_static=env.get("BACKUP_DASH_SERVE_STATIC") == "1",
|
|
admins=parse_admins(env.get("BACKUP_DASH_ADMINS", "")),
|
|
)
|
|
|
|
|
|
def parse_admins(value: str) -> frozenset[str]:
|
|
"""'[email protected], [email protected]' -> {'[email protected]', '[email protected]'}"""
|
|
return frozenset(part.strip().lower() for part in value.replace(";", ",").split(",") if part.strip())
|