The invite command reads email from profile.json, not auth.json. google-add was only writing to auth.json so invite had no address to send to. Now calls set_email() as well. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
197 lines
6.2 KiB
Python
197 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Password and invite management for Cortex users.
|
|
|
|
Usage:
|
|
python manage_passwords.py set <username> # prompt for password
|
|
python manage_passwords.py set <username> <pass> # set directly (avoid in shell history)
|
|
python manage_passwords.py check <username> # test a password interactively
|
|
python manage_passwords.py list # show users, auth methods, and emails
|
|
python manage_passwords.py invite <username> [email] # generate + optionally email invite link
|
|
python manage_passwords.py email <username> <email> # store/update an email address
|
|
python manage_passwords.py google-add <username> <email> # 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 <username> [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 <username>")
|
|
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 <username> <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 <username> [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 <username> <email> to email it next time.\n")
|
|
|
|
|
|
def cmd_google_add(args):
|
|
if len(args) < 2:
|
|
print("Usage: manage_passwords.py google-add <username> <google_email>")
|
|
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.")
|
|
|
|
|
|
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)
|
|
else:
|
|
print(f"Unknown command: {command}")
|
|
print(__doc__)
|
|
sys.exit(1)
|