Files
Cortex-Inara/cortex/manage_passwords.py
Scott Idem 46b65d087c feat: persona onboarding — invite tokens, self-service setup, persona creation, switcher
New user flow:
  1. Admin: python manage_passwords.py invite <username>  → generates URL
  2. User visits /setup/<token> → sets own password → logged in
  3. User redirected to /setup/persona → fills name/emoji/description
  4. persona_template.py generates all starter files → lands at /{user}/{persona}

Multiple personas:
  - Header persona name is now a clickable dropdown listing all personas
  - "New persona" link at bottom → /setup/persona (available to logged-in users)
  - /api/personas endpoint returns persona list for current session user

New files:
  - persona_template.py: generates IDENTITY/SOUL/PROTOCOLS/USER/HELP.md + data files
  - routers/onboarding.py: /setup/{token}, /setup/persona GET+POST
  - static/setup.html: two-step form (password → persona), emoji picker, mobile-friendly

Updated:
  - auth_utils.py: create_invite(), validate_invite(), consume_invite()
  - manage_passwords.py: invite command with URL output
  - auth_middleware.py: /setup/* prefix is public (invite tokens need no auth)
  - routers/ui.py: /api/personas endpoint; post-login redirect if no personas
  - static/app.js: persona switcher dropdown with navigation + Add persona link
  - static/style.css: .persona-switcher, .persona-dropdown, mobile adjustments

Mobile: login/setup pages are card-centered with responsive padding;
dropdown avoids edge-clipping on narrow screens; logout button stays visible.

All 80 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 23:10:32 -04:00

100 lines
3.0 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 which users have a password set
python manage_passwords.py invite <username> # generate a one-time setup link
"""
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
from persona import list_users
from config import settings
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):
for user in list_users():
has = _auth_path(user).exists()
status = "✓ password set" if has else "✗ no password"
print(f" {user:<20} {status}")
def cmd_invite(args):
if not args:
print("Usage: manage_passwords.py invite <username>")
sys.exit(1)
username = args[0]
# Create the user directory if it doesn't exist yet
user_dir = settings.home_root() / username
user_dir.mkdir(parents=True, exist_ok=True)
token = create_invite(username)
# Try to read host from settings for a helpful URL
host = "cortex.dgrzone.com"
print(f"\nInvite link for {username!r}:")
print(f" https://{host}/setup/{token}\n")
print("Link expires in 72 hours. One-time use.")
print("Send this to the user — they'll set their own password and create a persona.\n")
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 == "invite":
cmd_invite(rest)
else:
print(f"Unknown command: {command}")
print(__doc__)
sys.exit(1)