Spaces:
Running
Running
File size: 3,258 Bytes
2e50ccd 6bb015e 2e50ccd 6bb015e 2e50ccd 8ae2973 bc18056 2e50ccd 6bb015e 8ae2973 6bb015e 2e50ccd 6bb015e 2e50ccd d6f7eeb 8ae2973 2e50ccd bc18056 2e50ccd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | """FastAPI application — flight search backend."""
import os
import time
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from .api import airports, auth, booking, calendar, currency, destinations, geolocation, search
from .benchmark import BENCHMARK_DATE, BENCHMARK_MODE
from .data_loader import get_route_graph
from .hub_detector import compute_hub_scores
app = FastAPI(title="Flight Search API", version="1.0.0")
# CORS for development
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Passkey middleware — protects /api/* routes when REQUIRE_PASSKEY is enabled
class PasskeyMiddleware(BaseHTTPMiddleware):
EXEMPT_PREFIXES = ("/api/auth/", "/api/health", "/api/geolocation", "/api/currency/", "/api/destinations/")
async def dispatch(self, request: Request, call_next):
if not auth._require_passkey():
return await call_next(request)
path = request.url.path
if path.startswith("/api/") and not any(
path.startswith(p) for p in self.EXEMPT_PREFIXES
):
token = request.cookies.get("flight_auth")
if token != auth.AUTH_TOKEN:
return JSONResponse(
status_code=401, content={"detail": "Not authenticated"}
)
return await call_next(request)
app.add_middleware(PasskeyMiddleware)
# Register API routers
app.include_router(auth.router)
app.include_router(airports.router)
app.include_router(search.router)
app.include_router(calendar.router)
app.include_router(booking.router)
app.include_router(currency.router)
app.include_router(geolocation.router)
app.include_router(destinations.router)
@app.on_event("startup")
async def startup():
"""Load data and compute hub scores on startup."""
t0 = time.time()
graph = get_route_graph()
hubs = compute_hub_scores(graph)
elapsed = time.time() - t0
print(f"Loaded {len(graph.airports)} airports, {len(hubs)} hubs in {elapsed:.1f}s")
@app.get("/api/health")
async def health():
graph = get_route_graph()
return {"status": "ok", "airports": len(graph.airports)}
@app.get("/api/config")
async def config():
"""Return client-visible configuration (e.g. benchmark mode)."""
return {
"benchmark_mode": BENCHMARK_MODE,
"benchmark_date": BENCHMARK_DATE.isoformat() if BENCHMARK_MODE else None,
}
# Serve frontend static files (production)
STATIC_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "dist")
if os.path.isdir(STATIC_DIR):
app.mount("/assets", StaticFiles(directory=os.path.join(STATIC_DIR, "assets")), name="assets")
@app.get("/{full_path:path}")
async def serve_frontend(full_path: str):
"""Serve the React SPA for all non-API routes."""
file_path = os.path.join(STATIC_DIR, full_path)
if os.path.isfile(file_path):
return FileResponse(file_path)
return FileResponse(os.path.join(STATIC_DIR, "index.html"))
|