modelbeast/scripts/users.py
MODELBEAST 810400fb60 Auth + dashboard (HANDOFF2 phases 1-2): guests local-only, system snapshot
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>
2026-07-13 12:06:52 +10:00

54 lines
1.8 KiB
Python

#!/usr/bin/env python
"""Manage MODELBEAST users directly against the DB — for bootstrapping or when
you're locked out of the UI. Run from the repo root: uv run python scripts/users.py ...
"""
import argparse
import getpass
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from server import auth, db # noqa: E402
def main():
con = db.connect()
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
a = sub.add_parser("add")
a.add_argument("username")
a.add_argument("--owner", action="store_true")
a.add_argument("--max-jobs", type=int, default=4)
sub.add_parser("list")
p = sub.add_parser("passwd")
p.add_argument("username")
d = sub.add_parser("delete")
d.add_argument("username")
args = ap.parse_args()
if args.cmd == "add":
pw = getpass.getpass("password: ")
u = auth.create_user(con, args.username, pw,
role="owner" if args.owner else "guest",
max_active_jobs=args.max_jobs)
print(f"created {u['username']} ({u['role']})")
elif args.cmd == "list":
for u in auth.list_users(con):
print(f"{u['username']:<20} {u['role']:<7} max_jobs={u['max_active_jobs']}")
elif args.cmd == "passwd":
u = auth.get_user_by_name(con, args.username)
if not u:
sys.exit("no such user")
auth.set_password(con, u["id"], getpass.getpass("new password: "))
print("updated")
elif args.cmd == "delete":
u = auth.get_user_by_name(con, args.username)
if not u:
sys.exit("no such user")
auth.delete_user(con, u["id"])
print("deleted")
if __name__ == "__main__":
main()