from __future__ import annotations from fastapi import Request from fastapi.responses import JSONResponse from server.auth.context import bind_identity, reset_identity from server.auth.licenses import get_license_provider, is_desktop_request from server.auth.providers import AuthError, get_auth_provider PUBLIC_API_PATHS = { "/api/health", "/api/auth/login", "/api/auth/refresh", "/api/auth/logout", "/api/auth/license/activate", } SELF_AUTHORIZED_WRITE_PREFIXES = ( "/api/workspace", "/api/projects", "/api/sessions", "/api/project-files", ) class AuthenticationMiddleware: """ASGI middleware keeps identity bound for the full streaming response lifetime.""" def __init__(self, app) -> None: self.app = app async def __call__(self, scope, receive, send) -> None: if scope.get("type") != "http": await self.app(scope, receive, send) return path = scope.get("path") or "" if scope.get("method") == "OPTIONS" or not path.startswith("/api/") or path in PUBLIC_API_PATHS: await self.app(scope, receive, send) return request = Request(scope, receive=receive) try: provider = get_license_provider() if is_desktop_request(request) else get_auth_provider() identity = await provider.authenticate(request) except AuthError as exc: response = JSONResponse( {"error": {"code": exc.code, "message": exc.message}}, status_code=exc.status_code, ) await response(scope, receive, send) return token = bind_identity(identity) try: method = scope.get("method") or "GET" if method not in {"GET", "HEAD", "OPTIONS"} and not path.startswith(SELF_AUTHORIZED_WRITE_PREFIXES): from server.state.projects import get_project_store try: get_project_store().require_active_write() except PermissionError as exc: response = JSONResponse( {"error": {"code": "PROJECT_READ_ONLY", "message": str(exc)}}, status_code=403, ) await response(scope, receive, send) return await self.app(scope, receive, send) finally: reset_identity(token)