trymeapp/app/main.py

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