import sentry_sdk from contextlib import asynccontextmanager from fastapi import FastAPI, Request, Response from fastapi.middleware.cors import CORSMiddleware from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from app.config import settings from app.limiter import limiter, get_real_client_ip from app.routers import auth, audit, design, github, billing, settings as settings_router, legal, projects from loguru import logger @asynccontextmanager async def lifespan(app: FastAPI): logger.info(f"{settings.APP_NAME} API startup complete. Environment: {'DEBUG' if settings.DEBUG else 'PRODUCTION'}") yield logger.info(f"{settings.APP_NAME} shutting down.") app = FastAPI( title=settings.APP_NAME, description="Archvise SaaS Backend Engine", version=settings.API_VERSION, debug=settings.DEBUG, lifespan=lifespan ) # Attach Limiter to App State app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # Configure CORS Middleware # Build list of allowed origins from config and defaults allowed_origins = [ "http://localhost:3000", "https://archvise.vercel.app", ] if isinstance(settings.CORS_ORIGINS, list): allowed_origins.extend(settings.CORS_ORIGINS) elif isinstance(settings.CORS_ORIGINS, str) and settings.CORS_ORIGINS != "*": allowed_origins.append(settings.CORS_ORIGINS) app.add_middleware( CORSMiddleware, allow_origins=list(set(allowed_origins)), allow_origin_regex=r"https://archvise-.*\.vercel\.app", allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Register Routers app.include_router(auth.router, prefix="/api") app.include_router(audit.router, prefix="/api") app.include_router(design.router, prefix="/api") app.include_router(github.router, prefix="/api") app.include_router(billing.router, prefix="/api") app.include_router(settings_router.router, prefix="/api") app.include_router(legal.router, prefix="/api") app.include_router(projects.router, prefix="/api") @app.get("/") def read_root(): return {"status": "online", "app": settings.APP_NAME, "version": settings.API_VERSION} @app.get("/health") async def health_check(): """Liveness + readiness probe — checks DB and Redis connectivity.""" import redis as redis_lib from app.database import engine from sqlalchemy import text checks = {"database": "ok", "redis": "ok"} status_code = 200 # Check DB try: async with engine.connect() as conn: await conn.execute(text("SELECT 1")) except Exception as e: checks["database"] = f"error: {str(e)[:80]}" status_code = 503 # Check Redis try: r = redis_lib.Redis.from_url(settings.REDIS_URL, socket_connect_timeout=2) r.ping() r.close() except Exception as e: checks["redis"] = f"error: {str(e)[:80]}" status_code = 503 from fastapi.responses import JSONResponse return JSONResponse(content={"status": "healthy" if status_code == 200 else "degraded", **checks}, status_code=status_code)