refactor: fix architectural issues across frontend and backend
Address all architecture review findings: P0 fixes: - Add API key authentication for admin endpoints (analytics, replay, openapi) and WebSocket connections via ADMIN_API_KEY env var - Add PostgreSQL-backed PgSessionManager and PgInterruptManager for multi-worker production deployments (in-memory defaults preserved) P1 fixes: - Implement actual tool generation in OpenAPI approve_job endpoint using generate_tool_code() and generate_agent_yaml() - Add missing clarification, interrupt_expired, and tool_result message handlers in frontend ChatPage P2 fixes: - Replace monkey-patching on CompiledStateGraph with typed GraphContext - Replace 9-param dispatch_message with WebSocketContext dataclass - Extract duplicate _envelope() into shared app/api_utils.py - Replace mutable module-level counter with crypto.randomUUID() - Remove hardcoded mock data from ReviewPage, use api.ts wrappers - Remove `as any` type escape from ReplayPage All 516 tests passing, 0 TypeScript errors.
This commit is contained in:
72
backend/app/auth.py
Normal file
72
backend/app/auth.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""API key authentication for admin endpoints and WebSocket connections."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import secrets
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, HTTPException, Query, Request, WebSocket, status
|
||||
from fastapi.security import APIKeyHeader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||
|
||||
|
||||
def _get_admin_api_key(request: Request) -> str:
|
||||
"""Retrieve the configured admin API key from app settings.
|
||||
|
||||
Returns empty string if settings are not configured (test/dev mode).
|
||||
"""
|
||||
settings = getattr(request.app.state, "settings", None)
|
||||
if settings is None:
|
||||
return ""
|
||||
key = getattr(settings, "admin_api_key", "")
|
||||
return key if isinstance(key, str) else ""
|
||||
|
||||
|
||||
async def require_admin_api_key(
|
||||
request: Request,
|
||||
api_key: Annotated[str | None, Depends(_API_KEY_HEADER)] = None,
|
||||
) -> None:
|
||||
"""Dependency that enforces API key authentication on admin endpoints.
|
||||
|
||||
Skips validation when no admin_api_key is configured (dev mode).
|
||||
"""
|
||||
expected = _get_admin_api_key(request)
|
||||
if not expected:
|
||||
return
|
||||
|
||||
if api_key is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing X-API-Key header",
|
||||
)
|
||||
if not secrets.compare_digest(api_key, expected):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid API key",
|
||||
)
|
||||
|
||||
|
||||
async def verify_ws_token(
|
||||
ws: WebSocket,
|
||||
token: str | None = Query(default=None),
|
||||
) -> None:
|
||||
"""Verify WebSocket connection token from query parameter.
|
||||
|
||||
Skips validation when no admin_api_key is configured (dev mode).
|
||||
Usage: ws://host/ws?token=<api_key>
|
||||
"""
|
||||
settings = ws.app.state.settings
|
||||
expected = settings.admin_api_key
|
||||
if not expected:
|
||||
return
|
||||
|
||||
if token is None or not secrets.compare_digest(token, expected):
|
||||
await ws.close(code=4001, reason="Unauthorized")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid or missing WebSocket token",
|
||||
)
|
||||
Reference in New Issue
Block a user