#!/usr/bin/env python3 """ Password and invite management for Cortex users. Usage: python manage_passwords.py set # prompt for password python manage_passwords.py set # set directly (avoid in shell history) python manage_passwords.py check # test a password interactively python manage_passwords.py list # show users, auth methods, and emails python manage_passwords.py invite [email] # generate + optionally email invite link python manage_passwords.py email # store/update an email address python manage_passwords.py google-add # register a user for Google sign-in """ import json import sys import getpass # Add cortex/ to path so we can import config and auth_utils sys.path.insert(0, str(__import__('pathlib').Path(__file__).parent)) from auth_utils import set_password, check_credentials, _auth_path, create_invite, link_google, _read_auth from persona import list_users from config import settings # --------------------------------------------------------------------------- # Profile helpers (home/{username}/profile.json) # --------------------------------------------------------------------------- def _profile_path(username: str): return settings.home_root() / username / "profile.json" def get_profile(username: str) -> dict: p = _profile_path(username) if not p.exists(): return {} try: return json.loads(p.read_text()) except Exception: return {} def save_profile(username: str, profile: dict) -> None: p = _profile_path(username) p.parent.mkdir(parents=True, exist_ok=True) p.write_text(json.dumps(profile, indent=2) + "\n") def get_email(username: str) -> str | None: return get_profile(username).get("email") def set_email(username: str, email: str) -> None: profile = get_profile(username) profile["email"] = email save_profile(username, profile) # --------------------------------------------------------------------------- # Commands # --------------------------------------------------------------------------- def cmd_set(args): if not args: print("Usage: manage_passwords.py set [password]") sys.exit(1) username = args[0] if len(args) >= 2: password = args[1] else: password = getpass.getpass(f"New password for {username}: ") confirm = getpass.getpass("Confirm password: ") if password != confirm: print("Passwords do not match.") sys.exit(1) set_password(username, password) print(f"Password set for: {username}") def cmd_check(args): if not args: print("Usage: manage_passwords.py check ") sys.exit(1) username = args[0] password = getpass.getpass(f"Password for {username}: ") if check_credentials(username, password): print("OK — credentials are valid.") else: print("FAIL — invalid username or password.") sys.exit(1) def cmd_list(_args): users = list_users() if not users: print(" No users found in home/") return print(f" {'USER':<18} {'PW':<6} {'GOOGLE':<8} {'EMAIL'}") print(f" {'-'*18} {'-'*6} {'-'*8} {'-'*30}") for user in users: auth = _read_auth(user) has_pw = "✓" if auth.get("password_hash") else "—" google = auth.get("google_email") or "—" email = get_email(user) or "—" print(f" {user:<18} {has_pw:<6} {google:<36} {email}") def cmd_email(args): if len(args) < 2: print("Usage: manage_passwords.py email ") sys.exit(1) username, email = args[0], args[1] set_email(username, email) print(f"Email saved for {username!r}: {email}") def cmd_invite(args): if not args: print("Usage: manage_passwords.py invite [email]") sys.exit(1) username = args[0] email_arg = args[1] if len(args) >= 2 else None # Ensure user directory exists (settings.home_root() / username).mkdir(parents=True, exist_ok=True) # Store email if provided if email_arg: set_email(username, email_arg) # Use stored email if no arg given to_email = email_arg or get_email(username) token = create_invite(username) url = f"{settings.cortex_base_url}/setup/{token}" print(f"\nInvite link for {username!r}:") print(f" {url}\n") print("Link expires in 72 hours. One-time use.") if to_email: from email_utils import send_invite_email print(f"Sending invite email to {to_email}...") ok = send_invite_email(to_email=to_email, username=username, token=token) if ok: print("Email sent.") else: print("Email failed — check SMTP settings. Link above is still valid.") else: print("No email address on file — send the link manually.") print("Tip: python manage_passwords.py invite to email it next time.\n") def cmd_google_add(args): if len(args) < 2: print("Usage: manage_passwords.py google-add ") sys.exit(1) username, email = args[0], args[1].lower().strip() # Ensure the user directory exists (settings.home_root() / username).mkdir(parents=True, exist_ok=True) # Store in auth.json (google_sub filled in on first sign-in) + profile.json (for invites) link_google(username, sub="", email=email) set_email(username, email) print(f"Google sign-in registered for {username!r}: {email}") print(f"They can now sign in at {settings.cortex_base_url}/login using that Google account.") def cmd_role(args): if len(args) < 2: print("Usage: manage_passwords.py role admin|user") sys.exit(1) username, role = args[0], args[1].lower().strip() if role not in ("admin", "user"): print("Role must be 'admin' or 'user'.") sys.exit(1) from auth_utils import _read_auth, _write_auth data = _read_auth(username) if not data: print(f"User '{username}' not found — no auth.json.") sys.exit(1) old_role = data.get("role", "user") data["role"] = role _write_auth(username, data) print(f"Role for '{username}': {old_role} → {role}") if __name__ == "__main__": if len(sys.argv) < 2: print(__doc__) sys.exit(0) command = sys.argv[1] rest = sys.argv[2:] if command == "set": cmd_set(rest) elif command == "check": cmd_check(rest) elif command == "list": cmd_list(rest) elif command == "email": cmd_email(rest) elif command == "invite": cmd_invite(rest) elif command == "google-add": cmd_google_add(rest) elif command == "role": cmd_role(rest) else: print(f"Unknown command: {command}") print(__doc__) sys.exit(1)