from __future__ import annotations from collections.abc import AsyncIterator from contextlib import asynccontextmanager import yaml from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, Response from fastapi.staticfiles import StaticFiles from app.config import ROOT_DIR, get_settings, read_app_version from app.database import SessionLocal from app.routers import admin, auth_routes, favourites, quizzes, users from app.seed import seed_starter_data_if_empty BRAND_FILES = { "2dlogo_large.png": ROOT_DIR / "2dlogo_large.png", "2dlogo_medium_256x256.png": ROOT_DIR / "2dlogo_medium_256x256.png", "2dlogo_small_64x64.png": ROOT_DIR / "2dlogo_small_64x64.png", "3dlogo_large.png": ROOT_DIR / "3dlogo_large.png", } FRONTEND_DIST = ROOT_DIR / "frontend" / "dist" @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncIterator[None]: settings = get_settings() if settings.seed_starter_quizzes: with SessionLocal() as db: seed_starter_data_if_empty(db) yield app = FastAPI( title="TryMe Gaming App API", description=( "Student quiz creation, sharing, voting, favourites, leaderboards, " "Auth0 federated login, API keys, and moderation." ), version=read_app_version(), docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173", "http://localhost:8000"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.include_router(auth_routes.router) app.include_router(users.router) app.include_router(quizzes.router) app.include_router(favourites.router) app.include_router(admin.router) @app.get("/health", tags=["System"]) def healthcheck() -> dict[str, str]: return { "status": "ok", "service": "trymeapp", "version": read_app_version(), } @app.get("/openapi.yaml", include_in_schema=False) def get_openapi_yaml() -> Response: spec_yaml = yaml.safe_dump(app.openapi(), sort_keys=False, allow_unicode=True) return Response(content=spec_yaml, media_type="application/yaml") @app.get("/brand/{filename}", include_in_schema=False) def serve_brand_asset(filename: str) -> FileResponse: target = BRAND_FILES.get(filename) if target is None or not target.exists(): raise HTTPException(status_code=404, detail="Brand asset not found.") return FileResponse(target, media_type="image/png") if (FRONTEND_DIST / "assets").exists(): app.mount( "/assets", StaticFiles(directory=str(FRONTEND_DIST / "assets")), name="spa-assets", ) @app.get("/{full_path:path}", include_in_schema=False) def serve_spa_fallback(full_path: str) -> Response: if full_path.startswith("api/"): raise HTTPException(status_code=404, detail="API endpoint not found.") if FRONTEND_DIST.exists(): candidate = (FRONTEND_DIST / full_path).resolve() if ( full_path and candidate.is_file() and str(candidate).startswith(str(FRONTEND_DIST.resolve())) ): return FileResponse(candidate) index_html = FRONTEND_DIST / "index.html" if index_html.is_file(): return FileResponse(index_html) return Response( content=( "TryMe API is running. Build the frontend (`cd frontend && npm run build`) " "or visit /docs for interactive API documentation." ), media_type="text/plain", )