Files
terra-view/backend/operator_admin.py
T
2026-06-17 19:50:37 +00:00

138 lines
4.9 KiB
Python

#!/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()