96 lines
3.5 KiB
Python
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)
|