import ipaddress from contextlib import asynccontextmanager, suppress from fastapi import FastAPI, Request from fastapi.exceptions import HTTPException from fastapi.responses import JSONResponse from app.auth.rate_limiter import LoginRateLimiter from app.config import get_settings from app.database import Base, get_engine @asynccontextmanager async def lifespan(application: FastAPI): settings = get_settings() application.state.login_rate_limiter = LoginRateLimiter( max_failures=settings.login_max_failures, window_seconds=settings.login_window_seconds, cooldown_seconds=settings.login_cooldown_seconds, ) trusted_networks = [] for part in settings.login_trusted_proxy_ips.split(","): part = part.strip() if part: with suppress(ValueError): trusted_networks.append(ipaddress.ip_network(part, strict=False)) application.state.login_trusted_networks = trusted_networks engine = get_engine() async with engine.begin() as conn: # In production, Alembic handles migrations; this is a dev convenience await conn.run_sync(Base.metadata.create_all) yield await engine.dispose() app = FastAPI(title="Reactbin API", version="1.0.0", lifespan=lifespan) # Defaults so app.state is populated even when lifespan doesn't run (e.g. tests) app.state.login_rate_limiter = LoginRateLimiter() app.state.login_trusted_networks = [] @app.exception_handler(HTTPException) async def http_exception_handler(request: Request, exc: HTTPException): if isinstance(exc.detail, dict): return JSONResponse(status_code=exc.status_code, content=exc.detail) return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}) @app.get("/api/v1/health") async def health(): return {"status": "ok"} # Routers registered after all modules are defined to avoid circular imports from app.routers import auth, images, tags # noqa: E402 app.include_router(auth.router, prefix="/api/v1") app.include_router(images.router, prefix="/api/v1") app.include_router(tags.router, prefix="/api/v1")