from contextlib import asynccontextmanager from urllib.parse import urlparse, parse_qs from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.exception_handlers import http_exception_handler from app.core.install_secrets import ensure_install_secrets from app.core.config import settings from app.core.jwt_denylist import ensure_jwt_revocation_backend from app.core.app_settings import bootstrap_settings from app.core.storage import ensure_bucket from app.core import database as db_module from app.db.token_cleanup import cleanup_expired_tokens from app.db.base import Base from app.db import models as _models # noqa: F401 from app.db.seed import run_seed from app.modules.auth.router import router as auth_router from app.modules.users.router import router as users_router from app.modules.content.router import router as content_router from app.modules.admin.router import router as admin_router from app.modules.media.router import router as media_router from app.modules.test.router import router as test_router from app.modules.sync.router import router as sync_router, enterprise_router as sync_enterprise_router from app.modules.zootech.router import router as zootech_router from app.modules.zootech.wesp_compat import router as wesp_compat_router from app.modules.zootech.wesp_compat_auth import router as wesp_compat_auth_router from app.modules.zootech.wesp_compat_sync import router as wesp_compat_sync_router from app.modules.zootech.wesp_compat_admin import router as wesp_compat_admin_router from app.modules.zootech.wesp_compat_misc import router as wesp_compat_misc_router from app.modules.zootech.wesp_compat_reports import router as wesp_compat_reports_router from app.modules.zootech.wesp_compat_analytics import router as wesp_compat_analytics_router from app.modules.enterprise.router import router as enterprise_router def _assert_production_guards() -> None: if settings.app_env.lower() != "production": return if settings.enable_test_routes: raise RuntimeError("ENABLE_TEST_ROUTES must be false in production") if settings.enable_docs: raise RuntimeError("ENABLE_DOCS must be false in production") if not settings.enable_rate_limit: raise RuntimeError("ENABLE_RATE_LIMIT must be true in production") if not settings.cookie_secure: raise RuntimeError("COOKIE_SECURE must be true in production") if settings.jwt_access_secret.startswith("change-me-"): raise RuntimeError("JWT_ACCESS_SECRET placeholder is not allowed in production") if settings.jwt_refresh_pepper.startswith("change-me-"): raise RuntimeError("JWT_REFRESH_PEPPER placeholder is not allowed in production") parsed = urlparse(settings.database_url) if parsed.username == "user" and parsed.password == "pass": raise RuntimeError("Default database credentials are not allowed in production") if parsed.scheme.startswith("postgresql"): sslmode = parse_qs(parsed.query).get("sslmode", [""])[0] if sslmode != "require": raise RuntimeError("DATABASE_URL must contain sslmode=require in production") def create_app() -> FastAPI: @asynccontextmanager async def lifespan(_: FastAPI): ensure_install_secrets() bootstrap_settings() ensure_jwt_revocation_backend() _assert_production_guards() if settings.enable_test_routes: Base.metadata.create_all(db_module.engine) run_seed() cleanup_expired_tokens() ensure_bucket() yield app = FastAPI( title="Compton API", version="1.0.0", docs_url="/api/v1/docs" if settings.enable_docs else None, openapi_url="/api/v1/openapi.json" if settings.enable_docs else None, lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, allow_methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS"], allow_headers=["Authorization", "Content-Type", "X-Request-ID"], ) @app.exception_handler(HTTPException) async def wesp_compat_http_exception_handler(request: Request, exc: HTTPException): if isinstance(exc.detail, dict) and exc.detail.get("status") == "error": return JSONResponse(status_code=exc.status_code, content=exc.detail) return await http_exception_handler(request, exc) @app.get("/api/v1/health") async def health() -> dict[str, str]: return {"status": "ok"} app.include_router(auth_router, prefix="/api/v1/auth", tags=["auth"]) app.include_router(users_router, prefix="/api/v1/users", tags=["users"]) app.include_router(content_router, prefix="/api/v1/content", tags=["content"]) app.include_router(admin_router, prefix="/api/v1/admin", tags=["admin"]) app.include_router(media_router, prefix="/api/v1/media", tags=["media"]) app.include_router(sync_router, prefix="/api/v1/sync", tags=["sync"]) app.include_router(sync_enterprise_router, prefix="/api/v1/enterprise", tags=["enterprise"]) app.include_router(enterprise_router, prefix="/api/v1/enterprise", tags=["enterprise"]) app.include_router(zootech_router, prefix="/api/v1/zootech", tags=["zootech"]) app.include_router(wesp_compat_router, prefix="/api", tags=["zootech-wesp-compat"]) app.include_router(wesp_compat_auth_router, prefix="/api", tags=["zootech-wesp-compat"]) app.include_router(wesp_compat_sync_router, prefix="/api", tags=["zootech-wesp-compat"]) app.include_router(wesp_compat_admin_router, prefix="/api", tags=["zootech-wesp-compat"]) app.include_router(wesp_compat_misc_router, prefix="/api", tags=["zootech-wesp-compat"]) app.include_router(wesp_compat_reports_router, prefix="/api", tags=["zootech-wesp-compat"]) app.include_router(wesp_compat_analytics_router, prefix="/api", tags=["zootech-wesp-compat"]) if settings.enable_test_routes: app.include_router(test_router, prefix="/api/v1/test", tags=["test"]) return app app = create_app()