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"))