from contextlib import asynccontextmanager from pathlib import Path from urllib.parse import quote from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse from fastapi.staticfiles import StaticFiles from app.api.router import api_router from app.core.config import settings from app.core.database import init_db from app.deps import NotAuthenticated from app.web.admin import router as admin_router from app.web.pages import router as pages_router from app.web.scopes import router as scopes_router STATIC_DIR = Path(__file__).resolve().parent / "static" @asynccontextmanager async def lifespan(app: FastAPI): # Dev/SQLite convenience. Use Alembic migrations in production. if settings.auto_create_tables: await init_db() yield def create_app() -> FastAPI: app = FastAPI(title=settings.app_name, debug=settings.debug, lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") app.include_router(api_router, prefix="/api") app.include_router(pages_router) app.include_router(scopes_router) app.include_router(admin_router) @app.exception_handler(NotAuthenticated) async def _redirect_to_login(request: Request, exc: NotAuthenticated): # Preserve the originally requested URL so we can return there after # login. Only meaningful for GET navigations (the ones users deep-link). login_url = "/login" if request.method == "GET" and request.url.path not in ("/", "/login"): target = request.url.path if request.url.query: target += "?" + request.url.query login_url = f"/login?next={quote(target, safe='')}" return RedirectResponse(url=login_url, status_code=302) @app.get("/healthz", tags=["health"]) async def healthz(): return {"status": "ok"} return app app = create_app()