""" Art-DAG L1 Server Application Factory. Creates and configures the FastAPI application with all routers and middleware. """ import time from pathlib import Path from urllib.parse import quote from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from artdag_common import create_jinja_env from artdag_common.middleware.auth import get_user_from_cookie from .config import settings # Paths that should never trigger a silent auth check _SKIP_PREFIXES = ("/auth/", "/static/", "/api/", "/ipfs/", "/download/") _SILENT_CHECK_COOLDOWN = 300 # 5 minutes def create_app() -> FastAPI: """ Create and configure the L1 FastAPI application. Returns: Configured FastAPI instance """ app = FastAPI( title="Art-DAG L1 Server", description="Content-addressed media processing with distributed execution", version="1.0.0", ) # Database lifecycle events from database import init_db, close_db @app.on_event("startup") async def startup(): await init_db() @app.on_event("shutdown") async def shutdown(): await close_db() # Silent auth check — auto-login via prompt=none OAuth @app.middleware("http") async def silent_auth_check(request: Request, call_next): path = request.url.path if ( request.method != "GET" or any(path.startswith(p) for p in _SKIP_PREFIXES) or request.headers.get("hx-request") # skip HTMX ): return await call_next(request) # Already logged in if get_user_from_cookie(request): return await call_next(request) # Check cooldown — don't re-check within 5 minutes pnone_at = request.cookies.get("pnone_at") if pnone_at: try: if (time.time() - float(pnone_at)) < _SILENT_CHECK_COOLDOWN: return await call_next(request) except (ValueError, TypeError): pass # Redirect to silent OAuth check current_url = str(request.url) return RedirectResponse( url=f"/auth/login?prompt=none&next={quote(current_url, safe='')}", status_code=302, ) # Initialize Jinja2 templates template_dir = Path(__file__).parent / "templates" app.state.templates = create_jinja_env(template_dir) # Custom 404 handler @app.exception_handler(404) async def not_found_handler(request: Request, exc): from artdag_common.middleware import wants_html if wants_html(request): from artdag_common import render return render(app.state.templates, "404.html", request, user=None, status_code=404, ) return JSONResponse({"detail": "Not found"}, status_code=404) # Include routers from .routers import auth, storage, api, recipes, cache, runs, home, effects # Home and auth routers (root level) app.include_router(home.router, tags=["home"]) app.include_router(auth.router, prefix="/auth", tags=["auth"]) # Feature routers app.include_router(storage.router, prefix="/storage", tags=["storage"]) app.include_router(api.router, prefix="/api", tags=["api"]) # Runs and recipes routers app.include_router(runs.router, prefix="/runs", tags=["runs"]) app.include_router(recipes.router, prefix="/recipes", tags=["recipes"]) # Cache router - handles /cache and /media app.include_router(cache.router, prefix="/cache", tags=["cache"]) # Also mount cache router at /media for convenience app.include_router(cache.router, prefix="/media", tags=["media"]) # Effects router app.include_router(effects.router, prefix="/effects", tags=["effects"]) return app # Create the default app instance app = create_app()