Phase 1 — multi-user auth: - server/auth.py: bcrypt passwords, itsdangerous signed-cookie sessions, sha256 bearer tokens, FastAPI current_user/require_owner deps, login rate limit - users + api_tokens tables + jobs/assets.user_id (additive migrations) - HARD RULE enforced server-side: guests are local-only — /api/operators filters out requires_env operators, POST /api/jobs 403s cloud ops for non-owners (proven via direct POST in smoke.sh, not just UI). Settings owner-only. auth_secret hidden from the settings API. Per-user active-job cap (owner exempt). Own-asset/ own-job checks. WS auth via cookie or ?token=. Owner bootstrap prints pw once. - mb-ready: bearer MB_TOKEN; scripts/users.py for out-of-band management - Frontend: Login gate, header user chip + logout, guest note, username on jobs, Users panel in Settings (owner) Phase 2 — dashboard: - server/sysinfo.py: psutil CPU/RAM/disk + macmon Apple GPU (util/power/temp, no sudo), computed lane occupancy, 24h job summary, recent jobs w/ output thumbs; all cached (5s stats, 5min du). /api/system + /api/jobs/recent. - Dashboard.jsx: snapshot-on-refresh (no polling) — stat cards, per-core strip, lane strip, running/queued, recent grid. tests/smoke.sh rewritten for auth: 28 checks passing incl. all guest-security rules. Browser-verified owner + guest + dashboard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
85 lines
2.7 KiB
Python
85 lines
2.7 KiB
Python
"""Settings + secrets: a key/value store surfaced to the UI and injected as env
|
|
vars into operator subprocesses. Secret values are redacted from job logs."""
|
|
|
|
# setting key -> environment variable name handed to operator subprocesses
|
|
ENV_MAP = {
|
|
"fal_key": "FAL_KEY",
|
|
"tripo_key": "TRIPO_KEY",
|
|
"meshy_key": "MESHY_KEY",
|
|
"replicate_token": "REPLICATE_API_TOKEN",
|
|
"openrouter_key": "OPENROUTER_API_KEY",
|
|
"hf_token": "HF_TOKEN",
|
|
"models_dir": "MODELBEAST_MODELS_DIR",
|
|
"archive_host": "MODELBEAST_ARCHIVE_HOST",
|
|
"archive_path": "MODELBEAST_ARCHIVE_PATH",
|
|
}
|
|
|
|
# keys whose values are secret: rendered as password fields, redacted from logs
|
|
SECRET_KEYS = {"fal_key", "tripo_key", "meshy_key", "replicate_token", "openrouter_key", "hf_token"}
|
|
|
|
# internal keys never exposed to the settings API or env injection (auth signing key)
|
|
HIDDEN_KEYS = {"auth_secret"}
|
|
|
|
DEFAULTS = {
|
|
"archive_host": "m3ultra@100.69.21.128",
|
|
"archive_path": "~/modelbeast-archive",
|
|
}
|
|
|
|
|
|
def get_all(con) -> dict:
|
|
rows = con.execute("SELECT key, value FROM settings").fetchall()
|
|
out = dict(DEFAULTS)
|
|
for r in rows:
|
|
if r["key"] in HIDDEN_KEYS:
|
|
continue # auth_secret etc. never reach the UI or env injection
|
|
out[r["key"]] = r["value"]
|
|
return out
|
|
|
|
|
|
def set_many(con, updates: dict) -> None:
|
|
for key, value in updates.items():
|
|
if key in HIDDEN_KEYS:
|
|
continue # never settable through the settings API
|
|
con.execute(
|
|
"INSERT INTO settings (key, value) VALUES (?, ?) "
|
|
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
(key, "" if value is None else str(value)),
|
|
)
|
|
con.commit()
|
|
|
|
|
|
def public_view(settings: dict) -> dict:
|
|
"""Mask secret values so the UI can show 'set / unset' without leaking them."""
|
|
out = {}
|
|
for key, value in settings.items():
|
|
if key in SECRET_KEYS:
|
|
out[key] = "••••••••" if value else ""
|
|
else:
|
|
out[key] = value
|
|
return out
|
|
|
|
|
|
def env_from_settings(settings: dict) -> dict:
|
|
env = {}
|
|
for key, env_name in ENV_MAP.items():
|
|
value = settings.get(key)
|
|
if value:
|
|
env[env_name] = value
|
|
# HF libraries read several token names
|
|
if settings.get("hf_token"):
|
|
env["HUGGING_FACE_HUB_TOKEN"] = settings["hf_token"]
|
|
return env
|
|
|
|
|
|
def secret_values(settings: dict) -> list[str]:
|
|
return [settings[k] for k in SECRET_KEYS if settings.get(k)]
|
|
|
|
|
|
def redact(text: str, secrets: list[str]) -> str:
|
|
if not text:
|
|
return text
|
|
for secret in secrets:
|
|
if secret and len(secret) >= 6:
|
|
text = text.replace(secret, "••••REDACTED••••")
|
|
return text
|