"""Command-line user management for the Work Package Suite. Use this to create the FIRST admin account (the /api/auth/users endpoint needs an existing admin, so you have to bootstrap one here), and for occasional account maintenance from a shell on the server. Run from the PROJECT ROOT (same place you run uvicorn), so the package imports and .env resolve the same way the API does: python -m server.manage_users create-admin alice --name "Alice Smith" python -m server.manage_users create bob --role user --name "Bob Jones" python -m server.manage_users list python -m server.manage_users reset-password alice python -m server.manage_users disable bob python -m server.manage_users enable bob If --password is omitted you'll be prompted (input is hidden). Passwords must be at least 8 characters. """ import argparse import getpass import sys import uuid from .db import SessionLocal, Base, engine from . import models, auth def _gen_id() -> str: return f"user_{uuid.uuid4().hex[:12]}" def _prompt_password(provided: str | None, username: str = "") -> str: pw = provided if not pw: pw = getpass.getpass("New password: ") confirm = getpass.getpass("Confirm password: ") if pw != confirm: sys.exit("Passwords do not match.") problem = auth.password_problem(pw, username) if problem: sys.exit(problem) return pw def cmd_create(args, role: str | None = None) -> None: role = role or args.role if role not in ("admin", "user"): sys.exit("role must be 'admin' or 'user'") pw = _prompt_password(getattr(args, "password", None), args.username) with SessionLocal() as db: if auth.find_user(db, args.username): sys.exit(f"A user named '{args.username}' already exists.") u = models.User( id=_gen_id(), username=args.username.strip(), full_name=(args.name or "").strip(), email=(args.email or "").strip(), password_hash=auth.hash_password(pw), role=role, ) db.add(u) db.commit() print(f"Created {role}: {u.username} (id={u.id})") def cmd_list(args) -> None: with SessionLocal() as db: rows = db.query(models.User).order_by(models.User.username).all() if not rows: print("No users yet. Create one with: create-admin ") return print(f"{'USERNAME':<24}{'ROLE':<8}{'ACTIVE':<8}{'NAME'}") for u in rows: print(f"{u.username:<24}{u.role:<8}{('yes' if u.is_active else 'no'):<8}{u.full_name}") def cmd_reset_password(args) -> None: pw = _prompt_password(getattr(args, "password", None), args.username) with SessionLocal() as db: u = auth.find_user(db, args.username) if not u: sys.exit(f"No user named '{args.username}'.") u.password_hash = auth.hash_password(pw) db.commit() print(f"Password reset for {u.username}.") def _set_active(username: str, active: bool) -> None: with SessionLocal() as db: u = auth.find_user(db, username) if not u: sys.exit(f"No user named '{username}'.") u.is_active = active db.commit() print(f"{u.username} is now {'enabled' if active else 'disabled'}.") def main() -> None: # Ensure the users table exists even on a fresh database. Base.metadata.create_all(bind=engine) p = argparse.ArgumentParser(prog="manage_users", description="Work Package Suite user management") sub = p.add_subparsers(dest="cmd", required=True) def add_create(name, help_): sp = sub.add_parser(name, help=help_) sp.add_argument("username") sp.add_argument("--password", help="set non-interactively (otherwise prompted)") sp.add_argument("--name", default="", help="full name") sp.add_argument("--email", default="") return sp add_create("create-admin", "create an admin account") c = add_create("create", "create an account") c.add_argument("--role", choices=["admin", "user"], default="user") sub.add_parser("list", help="list all accounts") rp = sub.add_parser("reset-password", help="reset a user's password") rp.add_argument("username") rp.add_argument("--password", help="set non-interactively (otherwise prompted)") dp = sub.add_parser("disable", help="disable an account (blocks login)") dp.add_argument("username") ep = sub.add_parser("enable", help="re-enable an account") ep.add_argument("username") args = p.parse_args() if args.cmd == "create-admin": cmd_create(args, role="admin") elif args.cmd == "create": cmd_create(args) elif args.cmd == "list": cmd_list(args) elif args.cmd == "reset-password": cmd_reset_password(args) elif args.cmd == "disable": _set_active(args.username, False) elif args.cmd == "enable": _set_active(args.username, True) if __name__ == "__main__": main()