from fastapi import FastAPI, Request from fastapi.responses import PlainTextResponse from app.analyzer import analyze from app.config import settings from app.schemas import AnalyzeRequest, AnalyzeResponse from app.webhooks_chatwoot import router as chatwoot_router from app.db import init_db from app.admin_ui.router import router as admin_dashboard_router from app.api.internal import router as internal_api_router def _looks_like_invalid_uuid_error(exc: Exception) -> bool: text_value = str(exc) lowered = text_value.lower() return ( "invalid input syntax for type uuid" in lowered or "badly formed hexadecimal uuid" in lowered or "valueerror: badly formed hexadecimal uuid" in lowered or "could not parse uuid" in lowered ) def _is_browser_request(request: Request) -> bool: accept = str(request.headers.get("accept") or "") return "text/html" in accept and "application/json" not in accept app = FastAPI( title="ClientFlow MVP", description="Motor de contexto comercial com Qwen3 30B + regras ClientFlow.", version="0.1.0", ) @app.middleware("http") async def security_headers_middleware(request: 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", "strict-origin-when-cross-origin") response.headers.setdefault("Permissions-Policy", "camera=(), microphone=(), geolocation=()") # A UI atual carrega Bootstrap, Bootstrap Icons e HTMX por CDN. # A CSP deve refletir explicitamente essas dependĂȘncias para evitar erros reais # no browser/Playwright, mantendo frame-ancestors e defaults restritos. response.headers.setdefault( "Content-Security-Policy", "default-src 'self'; " "img-src 'self' data:; " "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " "style-src-elem 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com; " "script-src-elem 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com; " "font-src 'self' data: https://cdn.jsdelivr.net; " "connect-src 'self' ws: wss:; " "frame-ancestors 'none'", ) path = str(request.url.path or "") if path in {"/", "/tasks", "/customers", "/opportunities", "/finance", "/system", "/settings"} or path.startswith(("/tasks", "/customers", "/opportunities", "/outbox", "/finance", "/financeiro")): response.headers.setdefault("Cache-Control", "private, no-store") return response @app.exception_handler(Exception) async def controlled_exception_handler(request: Request, exc: Exception): if _looks_like_invalid_uuid_error(exc): return PlainTextResponse("Identificador invĂĄlido.", status_code=422) # Keep unexpected errors visible during development/test while avoiding raw tracebacks in HTML. raise exc app.include_router(chatwoot_router) app.include_router(internal_api_router) @app.on_event("startup") async def startup_event() -> None: init_db() @app.get("/health") async def health() -> dict: return { "status": "ok", "app": settings.app_name, "env": settings.env, "model": settings.openrouter_model, } @app.post("/analyze", response_model=AnalyzeResponse) async def analyze_endpoint(request: AnalyzeRequest) -> AnalyzeResponse: return await analyze(request) app.include_router(admin_dashboard_router)