Files
clientflow_backend/app/main.py

96 lines
3.5 KiB
Python

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)