Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- Learning engine: expand default weights from 3 to all 9 strategies - Learning engine: resolve placeholder strategy_id with DB lookup - Learning engine: pass strategy_sources from trade execution - Trade executor: respect trading:paused Redis flag in RiskManager - Portfolio sync: compute actual daily P&L from day-start snapshot - Portfolio API: cumulative P&L from first snapshot, read pause flag - Portfolio metrics: compute max drawdown and avg hold duration - Add strategy_sources field to TradeExecution schema - Add dev_mode config (TRADING_DEV_MODE) to bypass auth for local dev - Dashboard: VITE_DEV_MODE bypasses ProtectedRoute and 401 redirects - Vite proxy target configurable via VITE_API_TARGET - Add top-level README.md and remaining-work-plan.md - Update CLAUDE.md with correct counts and remove stale TODOs - 404 tests passing Made-with: Cursor
80 lines
2.4 KiB
Python
80 lines
2.4 KiB
Python
"""Auth middleware — FastAPI dependency for JWT-based authentication."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import Depends, HTTPException, status
|
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
|
|
import jwt as pyjwt
|
|
|
|
from services.api_gateway.auth.jwt import decode_token
|
|
from services.api_gateway.config import ApiGatewayConfig
|
|
|
|
# Shared config instance — injected via FastAPI dependency override in tests.
|
|
_config: ApiGatewayConfig | None = None
|
|
|
|
security = HTTPBearer(auto_error=False)
|
|
|
|
|
|
def get_config() -> ApiGatewayConfig:
|
|
"""Return the singleton config. Overridden in tests."""
|
|
global _config
|
|
if _config is None:
|
|
_config = ApiGatewayConfig()
|
|
return _config
|
|
|
|
|
|
_DEV_USER = {
|
|
"sub": "00000000-0000-0000-0000-000000000000",
|
|
"username": "dev",
|
|
"type": "access",
|
|
}
|
|
|
|
|
|
async def get_current_user(
|
|
credentials: HTTPAuthorizationCredentials | None = Depends(security),
|
|
config: ApiGatewayConfig = Depends(get_config),
|
|
) -> dict:
|
|
"""FastAPI dependency that extracts and validates a Bearer JWT.
|
|
|
|
Returns the decoded token payload (contains ``sub``, ``username``, etc.)
|
|
on success. Raises a 401 ``HTTPException`` for missing, expired, or
|
|
invalid tokens.
|
|
|
|
When ``config.dev_mode`` is ``True``, authentication is bypassed and a
|
|
synthetic dev user is returned.
|
|
"""
|
|
if config.dev_mode:
|
|
return _DEV_USER
|
|
|
|
if credentials is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Missing authorization header",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
token = credentials.credentials
|
|
try:
|
|
payload = decode_token(token, config)
|
|
except pyjwt.ExpiredSignatureError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Token has expired",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
except pyjwt.InvalidTokenError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid token",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
if payload.get("type") != "access":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid token type",
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
return payload
|