#!/usr/bin/env python3 """Operator-account admin CLI — the bootstrap and the break-glass. Run inside the terra-view container against the live DB. Temp/raw passwords are printed ONCE; only hashes persist. # first superadmin (before any UI is reachable) — prompts for a password, or --generate python3 backend/operator_admin.py create-superadmin --email you@x.com --name "Brian" # a parent's account — generates a temp password, must-change on first login python3 backend/operator_admin.py create-user --email dad@x.com --name "Dad" --role admin python3 backend/operator_admin.py reset-password --email dad@x.com python3 backend/operator_admin.py list python3 backend/operator_admin.py disable --email dad@x.com python3 backend/operator_admin.py enable --email dad@x.com """ import os import sys import getpass import argparse from datetime import datetime sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from backend.database import SessionLocal from backend.models import OperatorUser from backend.operator_auth import ( create_operator, reset_operator_password, set_operator_active, _norm_email, ) def _get(db, email): u = db.query(OperatorUser).filter_by(email=_norm_email(email)).first() if not u: sys.exit(f"No operator with email '{email}'.") return u def cmd_create_superadmin(email, name, password=None, generate=False): db = SessionLocal() try: if password is None and not generate: password = getpass.getpass("Password for new superadmin: ") if not password or len(password) < 8: sys.exit("Password must be at least 8 characters.") user, raw = create_operator(db, email, name, "superadmin", password=None if generate else password) if generate: print(f"✓ Superadmin {user.email} created. Temp password (shown once): {raw}") else: print(f"✓ Superadmin {user.email} created.") except ValueError as e: sys.exit(str(e)) finally: db.close() def cmd_create_user(email, name, role="admin"): db = SessionLocal() try: user, raw = create_operator(db, email, name, role) print(f"✓ {role} {user.email} created. Temp password (shown once): {raw}") print(" They'll be required to change it on first login.") except ValueError as e: sys.exit(str(e)) finally: db.close() def cmd_reset_password(email): db = SessionLocal() try: user = _get(db, email) raw = reset_operator_password(db, user) print(f"✓ Reset {user.email}. Temp password (shown once): {raw}") finally: db.close() def cmd_set_active(email, active): db = SessionLocal() try: user = _get(db, email) set_operator_active(db, user, active) print(f"✓ {user.email} {'enabled' if active else 'disabled'}.") finally: db.close() def cmd_list(): db = SessionLocal() try: users = db.query(OperatorUser).order_by(OperatorUser.display_name).all() if not users: print("No operators yet. Run create-superadmin first.") return for u in users: locked = " [LOCKED]" if (u.locked_until and u.locked_until > datetime.utcnow()) else "" state = "active" if u.active else "DISABLED" last = u.last_login_at.strftime("%Y-%m-%d %H:%M") if u.last_login_at else "never" print(f" {u.display_name:<12} {u.email:<28} {u.role:<11} {state}{locked} last: {last}") finally: db.close() def main(): ap = argparse.ArgumentParser(description="Operator-account admin") sub = ap.add_subparsers(dest="cmd", required=True) p = sub.add_parser("create-superadmin") p.add_argument("--email", required=True); p.add_argument("--name", required=True) p.add_argument("--generate", action="store_true", help="generate a temp password instead of prompting") p.set_defaults(fn=lambda a: cmd_create_superadmin(a.email, a.name, generate=a.generate)) p = sub.add_parser("create-user") p.add_argument("--email", required=True); p.add_argument("--name", required=True) p.add_argument("--role", default="admin", choices=["admin", "superadmin"]) p.set_defaults(fn=lambda a: cmd_create_user(a.email, a.name, a.role)) p = sub.add_parser("reset-password") p.add_argument("--email", required=True) p.set_defaults(fn=lambda a: cmd_reset_password(a.email)) p = sub.add_parser("disable"); p.add_argument("--email", required=True) p.set_defaults(fn=lambda a: cmd_set_active(a.email, False)) p = sub.add_parser("enable"); p.add_argument("--email", required=True) p.set_defaults(fn=lambda a: cmd_set_active(a.email, True)) p = sub.add_parser("list"); p.set_defaults(fn=lambda a: cmd_list()) args = ap.parse_args() args.fn(args) if __name__ == "__main__": main()