feat: session auth + per-user/persona UI at /{user}/{persona}
Replaces nginx basic auth with a proper per-user session system:
- auth_utils.py: bcrypt password hashing, JWT cookie creation/decode
- auth_middleware.py: validates JWT cookie on all routes except /login,
/health, /static/, and webhook endpoints (/channels/, /webhook/)
- routers/ui.py: GET /login, POST /login, POST /logout,
GET /{username}/{persona} — serves index.html with CORTEX_CONFIG injected
- static/login.html: minimal login form (dark theme, matches UI)
- main.py: registers SessionAuthMiddleware + ui.router
- config.py: jwt_secret, jwt_expire_days settings
- manage_passwords.py: CLI tool to set/check/list user passwords
- app.js: reads window.CORTEX_CONFIG (user + persona), sends both on
every /chat and /orchestrate request; persona name shown in header;
logout button (⏏) added to header
- requirements.txt: bcrypt, PyJWT, python-multipart
- .env.default: JWT_SECRET, JWT_EXPIRE_DAYS documented
- tests: client fixture injects JWT cookie; security test assertions
updated for URL-normalized path traversal paths (still secure, codes differ)
All 80 tests pass.
Setup for a new user:
python manage_passwords.py set scott
python manage_passwords.py set holly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,14 @@ import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
import uvicorn
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s")
|
||||
|
||||
from config import settings
|
||||
from auth_middleware import SessionAuthMiddleware
|
||||
from routers import chat, google_chat, nextcloud_talk, files, distill, auth, orchestrator
|
||||
from routers import ui
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -23,6 +24,9 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
app = FastAPI(title="Cortex Dispatcher", lifespan=lifespan)
|
||||
|
||||
app.add_middleware(SessionAuthMiddleware)
|
||||
|
||||
# API routers
|
||||
app.include_router(chat.router)
|
||||
app.include_router(google_chat.router)
|
||||
app.include_router(nextcloud_talk.router)
|
||||
@@ -30,14 +34,13 @@ app.include_router(files.router)
|
||||
app.include_router(distill.router)
|
||||
app.include_router(auth.router)
|
||||
app.include_router(orchestrator.router)
|
||||
|
||||
# UI router (login + /{user}/{persona} — must be last to avoid swallowing API paths)
|
||||
app.include_router(ui.router)
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def index() -> FileResponse:
|
||||
return FileResponse("static/index.html")
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict:
|
||||
return {"status": "ok"}
|
||||
|
||||
Reference in New Issue
Block a user