Files
Cortex-Inara/cortex/auth_middleware.py
Scott Idem ddf44a2aee feat: web push notifications (VAPID)
- push_utils.py: subscription storage + send helper (auto-prunes 410 endpoints)
- routers/push.py: GET /api/push/vapid-key (public), POST/DELETE /api/push/subscribe
- sw.js: push event listener shows notification; notificationclick focuses/opens tab
- app.js: subscribe/unsubscribe flow + "Enable notifications" toggle in settings dropdown
- tools/notify.py: web_push orchestrator tool (user-level, no admin required)
- VAPID keys in .env; pywebpush added to requirements.txt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:38:58 -04:00

53 lines
1.9 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", "/manifest.json", "/sw.js", "/favicon.ico",
"/api/push/vapid-key"}
# Path prefixes that are always public (setup flow + webhooks + Google OAuth)
_PUBLIC_PREFIXES = ("/setup/", "/channels/", "/webhook/", "/auth/google")
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)