"""FastAPI application - LeoCRM backend.""" from __future__ import annotations import time import traceback from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.base import BaseHTTPMiddleware import os from app.config import get_settings from app.core.db import close_engine, get_engine from app.core.middleware import CSRFMiddleware from app.core.monitoring import record_error, record_request from app.core.service_container import get_container from app.plugins.registry import get_registry from app.routes import ( ai_copilot, auth, companies, contacts, health, import_export, metrics, notifications, plugins, roles, tenants, users, workflows, ) class RequestLoggingMiddleware(BaseHTTPMiddleware): """Structured logging + Prometheus metrics for every HTTP request.""" async def dispatch(self, request: Request, call_next): start_time = time.perf_counter() method = request.method path = request.url.path # Extract tenant_id from session cookie if available (best-effort) tenant_id = None try: response = await call_next(request) except Exception as exc: duration_ms = (time.perf_counter() - start_time) * 1000 tb_str = traceback.format_exc() record_error( event="unhandled_exception", method=method, path=path, status_code=500, error=str(exc), traceback_str=tb_str, tenant_id=tenant_id, ) raise duration_ms = (time.perf_counter() - start_time) * 1000 status_code = response.status_code # Try to get tenant_id from response headers or request state # (set by auth middleware/dependency — best-effort, never log credentials) record_request( method=method, path=path, status_code=status_code, duration_ms=duration_ms, tenant_id=tenant_id, ) return response @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan: startup and shutdown.""" # Initialize service container container = get_container() await container.initialize() # Initialize plugin registry and discover built-in plugins registry = get_registry() registry.initialize(get_engine(), app) registry.discover_builtins() yield await close_engine() def create_app() -> FastAPI: """Create and configure the FastAPI application.""" settings = get_settings() app = FastAPI(title="LeoCRM", version="1.0.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origin_list, allow_credentials=True, allow_methods=["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"], allow_headers=["Authorization", "Content-Type", "X-CSRF-Token", "X-Tenant-ID"], max_age=3600, ) app.add_middleware(CSRFMiddleware) app.add_middleware(RequestLoggingMiddleware) app.include_router(health.router) app.include_router(metrics.router) app.include_router(auth.router) app.include_router(users.router) app.include_router(roles.router) app.include_router(tenants.router) app.include_router(notifications.router) app.include_router(companies.router) app.include_router(contacts.router) app.include_router(import_export.router) app.include_router(plugins.router) app.include_router(ai_copilot.router) app.include_router(workflows.router) # ─── Serve frontend static files (SPA) ─────────────────────────────── # Mount built frontend assets (JS, CSS, images) frontend_dist = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist") if os.path.isdir(frontend_dist): # Serve static assets at /assets assets_path = os.path.join(frontend_dist, "assets") if os.path.isdir(assets_path): app.mount("/assets", StaticFiles(directory=assets_path), name="assets") # SPA catch-all: serve index.html for all non-API routes @app.get("/{full_path:path}") async def spa_spa(full_path: str): """Serve index.html for all non-API routes (SPA fallback).""" # Don't intercept API routes if full_path.startswith(("api/", "docs", "openapi", "redoc")): raise HTTPException(status_code=404, detail="Not Found") index_path = os.path.join(frontend_dist, "index.html") if os.path.isfile(index_path): return FileResponse(index_path) raise HTTPException(status_code=404, detail="Frontend not built") return app app = create_app()