from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from sqlalchemy import select from app.api.v1.router import api_router from app.auth.api_key import hash_api_key from app.config import get_settings from app.database import SessionLocal from app.models import ApiKey ROOT = Path(__file__).resolve().parents[2] def bootstrap_api_key() -> None: settings = get_settings() if not settings.sac_bootstrap_api_key: return db = SessionLocal() try: key_hash = hash_api_key(settings.sac_bootstrap_api_key) exists = db.scalar(select(ApiKey).where(ApiKey.key_hash == key_hash)) if exists: return prefix = settings.sac_bootstrap_api_key[:12] db.add( ApiKey( name="bootstrap", key_prefix=prefix, key_hash=key_hash, is_active=True, ) ) db.commit() finally: db.close() @asynccontextmanager async def lifespan(_app: FastAPI): bootstrap_api_key() yield def create_app() -> FastAPI: settings = get_settings() app = FastAPI( title="Security Alert Center", version="0.1.0", lifespan=lifespan, ) origins = [o.strip() for o in settings.cors_origins.split(",") if o.strip()] app.add_middleware( CORSMiddleware, allow_origins=origins if origins != ["*"] else ["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) from app.api.v1.health import router as health_router app.include_router(api_router, prefix="/api/v1") app.include_router(health_router) frontend_dist = ROOT / "frontend" / "dist" if frontend_dist.is_dir(): assets_dir = frontend_dist / "assets" if assets_dir.is_dir(): app.mount( "/assets", StaticFiles(directory=str(assets_dir)), name="ui-assets", ) @app.get("/{spa_path:path}") def spa_fallback(spa_path: str) -> FileResponse: if spa_path.startswith("api/"): raise HTTPException(status_code=404, detail="Not Found") candidate = frontend_dist / spa_path if spa_path and candidate.is_file(): return FileResponse(candidate) index = frontend_dist / "index.html" if not index.is_file(): raise HTTPException(status_code=404, detail="UI not built") return FileResponse(index) return app app = create_app()