Files
Cortex-Inara/cortex/auth_middleware.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

52 lines
1.8 KiB
Python

"""
Session auth middleware.
Validates the JWT cookie on every request. Unprotected paths are explicitly
listed in _PUBLIC. Webhook endpoints have their own auth (HMAC/JWT) so they
are also excluded.
Sets request.state.session_user to the authenticated username so downstream
routers can enforce ownership without re-reading the cookie.
"""
import jwt
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import RedirectResponse, JSONResponse
from auth_utils import COOKIE_NAME, decode_token
# Paths that don't require a session cookie
_PUBLIC = {"/login", "/logout", "/health"}
# Path prefixes that are always public (setup flow + webhooks)
_PUBLIC_PREFIXES = ("/setup/", "/channels/", "/webhook/")
class SessionAuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
path = request.url.path
# Always allow public paths and setup/webhook prefixes
if path in _PUBLIC or any(path.startswith(p) for p in _PUBLIC_PREFIXES):
return await call_next(request)
# Allow static assets without a cookie
if path.startswith("/static/"):
return await call_next(request)
# Validate session cookie
token = request.cookies.get(COOKIE_NAME)
if token:
try:
request.state.session_user = decode_token(token)
return await call_next(request)
except jwt.InvalidTokenError:
pass
# No valid session — redirect browser requests, 401 for API/JSON
accept = request.headers.get("accept", "")
if "text/html" in accept:
return RedirectResponse("/login", status_code=302)
return JSONResponse({"detail": "Not authenticated"}, status_code=401)