rayhpeng 0f82f8a3a2 feat(app): add plugin system with auth plugin and static assets
Add new application structure:
- app/main.py - application entry point
- app/plugins/ - plugin system with auth plugin:
  - api/ - REST API endpoints and schemas
  - authorization/ - auth policies, providers, hooks
  - domain/ - business logic (service, models, jwt, password)
  - injection/ - route injection and guards
  - ops/ - operational utilities
  - runtime/ - runtime configuration
  - security/ - middleware, CSRF, dependencies
  - storage/ - user repositories and models
- app/static/ - static assets (scalar.js for API docs)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-22 11:31:42 +08:00

79 lines
2.8 KiB
Python

"""Global authentication middleware for the auth plugin."""
from __future__ import annotations
from collections.abc import Callable
from fastapi import HTTPException, Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from starlette.types import ASGIApp
from app.plugins.auth.authorization import _ALL_PERMISSIONS, AuthContext
from app.plugins.auth.domain.errors import AuthErrorCode, AuthErrorResponse
from app.plugins.auth.injection.registry_loader import RoutePolicyRegistry
from app.plugins.auth.security.dependencies import get_current_user_from_request
from deerflow.runtime.actor_context import ActorContext, bind_actor_context, reset_actor_context
_PUBLIC_PATH_PREFIXES: tuple[str, ...] = ("/health", "/docs", "/redoc", "/openapi.json")
_PUBLIC_EXACT_PATHS: frozenset[str] = frozenset(
{
"/api/v1/auth/login/local",
"/api/v1/auth/register",
"/api/v1/auth/logout",
"/api/v1/auth/setup-status",
"/api/v1/auth/initialize",
}
)
def _is_public(path: str) -> bool:
stripped = path.rstrip("/")
if stripped in _PUBLIC_EXACT_PATHS:
return True
return any(path.startswith(prefix) for prefix in _PUBLIC_PATH_PREFIXES)
class AuthMiddleware(BaseHTTPMiddleware):
def __init__(self, app: ASGIApp) -> None:
super().__init__(app)
async def dispatch(self, request: Request, call_next: Callable) -> Response:
registry = getattr(request.app.state, "auth_route_policy_registry", None)
is_public = False
if isinstance(registry, RoutePolicyRegistry):
is_public = registry.is_public_request(request.method, request.url.path)
if is_public or _is_public(request.url.path):
return await call_next(request)
if not request.cookies.get("access_token"):
return JSONResponse(
status_code=401,
content={
"detail": AuthErrorResponse(
code=AuthErrorCode.NOT_AUTHENTICATED,
message="Authentication required",
).model_dump()
},
)
try:
user = await get_current_user_from_request(request)
except HTTPException as exc:
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
auth_context = AuthContext(user=user, permissions=_ALL_PERMISSIONS)
request.scope["user"] = user
request.scope["auth"] = auth_context
request.state.user = user
request.state.auth = auth_context
token = bind_actor_context(ActorContext(user_id=str(user.id)))
try:
return await call_next(request)
finally:
reset_actor_context(token)
__all__ = ["AuthMiddleware", "_is_public"]