118 lines
3.5 KiB
Python
118 lines
3.5 KiB
Python
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",
|
|
)
|