import logging from typing import Annotated from fastapi import Depends, HTTPException, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.ext.asyncio import AsyncSession from gps_denied.config import RuntimeConfig, get_settings from gps_denied.core.processor import FlightProcessor from gps_denied.core.sse import SSEEventStreamer from gps_denied.db.engine import get_session from gps_denied.db.repository import FlightRepository logger = logging.getLogger(__name__) # Singleton instance of SSE Event Streamer _sse_streamer = SSEEventStreamer() # Singleton FlightProcessor (one per process, reused across requests) _processor: FlightProcessor | None = None # JWT Bearer scheme (auto_error=False — ми самі обробляємо помилки) _bearer = HTTPBearer(auto_error=False) async def verify_token( credentials: HTTPAuthorizationCredentials | None = Depends(_bearer), ) -> None: """JWT перевірка. При AUTH_ENABLED=false — пропускає все.""" settings = get_settings() if not settings.auth.enabled: return # dev/SITL: автентифікація вимкнена if credentials is None: raise HTTPException(status_code=401, detail="Authorization header required") try: import jwt jwt.decode( credentials.credentials, settings.auth.secret_key, algorithms=[settings.auth.algorithm], ) except ImportError: logger.warning("PyJWT not installed — JWT validation skipped") except Exception as exc: raise HTTPException(status_code=401, detail=f"Invalid token: {exc}") from exc def get_sse_streamer() -> SSEEventStreamer: return _sse_streamer async def get_repository( session: AsyncSession = Depends(get_session), ) -> FlightRepository: return FlightRepository(session) async def get_flight_processor( request: Request, repo: FlightRepository = Depends(get_repository), sse: SSEEventStreamer = Depends(get_sse_streamer), ) -> FlightProcessor: global _processor if _processor is None: # Prefer the processor already built by lifespan (via build_pipeline) lifespan_processor = getattr(request.app.state, "processor", None) if lifespan_processor is not None: _processor = lifespan_processor else: # Fallback: build pipeline directly (e.g. in tests without lifespan) from gps_denied.pipeline import build_pipeline _settings = RuntimeConfig() _processor = build_pipeline( env=_settings.env, config=_settings, repository=repo, streamer=sse, ) # Оновлюємо repo та streamer (нова сесія на кожен запит) _processor.repository = repo _processor.streamer = sse return _processor # Аліаси для зручності в роутерах SessionDep = Annotated[AsyncSession, Depends(get_session)] RepoDep = Annotated[FlightRepository, Depends(get_repository)] ProcessorDep = Annotated[FlightProcessor, Depends(get_flight_processor)] AuthDep = Annotated[None, Depends(verify_token)]