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>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Password management for Cortex users.
|
||||
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
|
||||
@@ -15,8 +16,9 @@ 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
|
||||
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):
|
||||
@@ -56,6 +58,25 @@ def cmd_list(_args):
|
||||
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__)
|
||||
@@ -70,6 +91,8 @@ if __name__ == "__main__":
|
||||
cmd_check(rest)
|
||||
elif command == "list":
|
||||
cmd_list(rest)
|
||||
elif command == "invite":
|
||||
cmd_invite(rest)
|
||||
else:
|
||||
print(f"Unknown command: {command}")
|
||||
print(__doc__)
|
||||
|
||||
Reference in New Issue
Block a user