"""FastAPI application entrypoint: routers, static files, security headers, startup.""" import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, FileResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.base import BaseHTTPMiddleware from app.api.routers import (accounts, attachments, auth, campaigns, dashboard, recipients, settings_router, subjects, suppression, templates, phones, test_send, logs, unsubscribe as unsub_router) from app.core.config import settings logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s") @asynccontextmanager async def lifespan(app: FastAPI): yield from app.db.session import close_engines await close_engines() app = FastAPI(title=settings.APP_NAME, lifespan=lifespan, docs_url="/api/docs", openapi_url="/api/openapi.json") class SecurityHeadersMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): response = await call_next(request) response.headers.setdefault("X-Content-Type-Options", "nosniff") response.headers.setdefault("X-Frame-Options", "DENY") response.headers.setdefault("Referrer-Policy", "no-referrer") response.headers.setdefault("Content-Security-Policy", "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';" " script-src 'self'; frame-ancestors 'none'") if settings.ENVIRONMENT == "production": response.headers.setdefault("Strict-Transport-Security", "max-age=63072000; includeSubDomains") return response app.add_middleware(SecurityHeadersMiddleware) app.include_router(auth.router) app.include_router(accounts.router) app.include_router(recipients.router) app.include_router(subjects.router) app.include_router(templates.router) app.include_router(attachments.router) app.include_router(phones.router) app.include_router(campaigns.router) app.include_router(test_send.router) app.include_router(dashboard.router) app.include_router(suppression.router) app.include_router(settings_router.router) app.include_router(logs.router) app.include_router(unsub_router.router) app.mount("/static", StaticFiles(directory="static"), name="static") @app.get("/api/health") async def health(): from app.db.session import check_db_health import redis as redis_lib db_ok = check_db_health() try: r = redis_lib.from_url(settings.REDIS_URL, socket_timeout=1) r.ping() redis_ok = True except Exception: redis_ok = False return {"status": "ok" if db_ok and redis_ok else "degraded", "database": "up" if db_ok else "down", "redis": "up" if redis_ok else "down"} # Frontend SPA-ish pages @app.get("/", include_in_schema=False) async def index(): return FileResponse("static/index.html") @app.get("/{page}", include_in_schema=False) async def page(page: str): import os safe = page.replace("/", "") path = f"static/{safe}.html" if os.path.exists(path): return FileResponse(path) return JSONResponse({"error": "not found"}, status_code=404)