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:
@@ -18,7 +18,7 @@ from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||
|
||||
from auth_utils import COOKIE_NAME, check_credentials, create_token, decode_token
|
||||
from persona import list_user_personas, validate as validate_persona
|
||||
from persona import list_users, list_user_personas, validate as validate_persona
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -126,6 +126,16 @@ async def logout():
|
||||
# Main UI — /{username}/{persona}
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/api/personas", tags=["ui"])
|
||||
async def api_personas(request: Request) -> dict:
|
||||
"""Return the list of personas for the current session user."""
|
||||
user = _get_session_user(request)
|
||||
if not user:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
return {"user": user, "personas": list_user_personas(user)}
|
||||
|
||||
|
||||
@router.get("/{username}/{persona}", include_in_schema=False)
|
||||
@router.get("/{username}/{persona}/", include_in_schema=False)
|
||||
async def serve_ui(username: str, persona: str, request: Request):
|
||||
|
||||
Reference in New Issue
Block a user