From 1ab50d456b63e799ef4a661d7331c302e242fedc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 14:34:51 +0000 Subject: [PATCH] =?UTF-8?q?security:=20corre=C3=A7=C3=B5es=20cr=C3=ADticas?= =?UTF-8?q?=20de=20seguran=C3=A7a=20e=20estabilidade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SEGURANÇA: - auth: XOS, LMS, Quality e /api/tenants agora exigem autenticação (102+ rotas) - CORS: 7 serviços Python trocam allow_origins=["*"] por APP_URL env var - credentials: removidas senhas hardcoded de metaset/routes.ts; SESSION_SECRET com warning se ausente - uvicorn: criados server/__init__.py e server/python/__init__.py para module-style correto - docker: embeddings_service usa uvicorn module-style como os demais ESTABILIDADE: - OpenAI: timeout=30s e maxRetries=3 no cliente Manus - Frappe: AbortSignal.timeout(30s) em todos os fetches - PipelineOrchestrator: guard processingMonitors evita execuções sobrepostas no setInterval DADOS: - WhatsApp auto-reply config agora persiste no banco (coluna auto_reply_config jsonb) - Migration 0001_whatsapp_auto_reply_config.sql adicionada https://claude.ai/code/session_01DinH3VcgbAv1d9MqnNxzdb --- docker/python-entrypoint.sh | 5 +++- .../0001_whatsapp_auto_reply_config.sql | 1 + migrations/meta/_journal.json | 7 +++++ server/__init__.py | 0 server/auth.ts | 6 ++++- server/blackboard/PipelineOrchestrator.ts | 5 ++++ server/crm/frappe-service.ts | 1 + server/lms/routes.ts | 9 +++++++ server/manus/service.ts | 2 ++ server/metaset/routes.ts | 7 +++-- server/python/__init__.py | 0 server/python/automation_engine.py | 2 +- server/python/bi_analysis_service.py | 2 +- server/python/bi_engine.py | 2 +- server/python/contabil_service.py | 2 +- server/python/embeddings_service.py | 3 ++- server/python/fisco_service.py | 2 +- server/python/people_service.py | 2 +- server/quality/routes.ts | 9 +++++++ server/routes.ts | 5 +++- server/whatsapp/routes.ts | 2 +- server/whatsapp/service.ts | 26 ++++++++++++++++--- server/xos/routes.ts | 9 +++++++ shared/schema.ts | 1 + 24 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 migrations/0001_whatsapp_auto_reply_config.sql create mode 100644 server/__init__.py create mode 100644 server/python/__init__.py diff --git a/docker/python-entrypoint.sh b/docker/python-entrypoint.sh index 43f2acc..a9bf876 100644 --- a/docker/python-entrypoint.sh +++ b/docker/python-entrypoint.sh @@ -41,7 +41,10 @@ case "$SERVICE_NAME" in --workers 2 ;; embeddings) - exec python server/python/embeddings_service.py + exec python -m uvicorn server.python.embeddings_service:app \ + --host 0.0.0.0 \ + --port "$SERVICE_PORT" \ + --workers 1 ;; *) echo "[entrypoint] ERRO: SERVICE_NAME desconhecido: $SERVICE_NAME" diff --git a/migrations/0001_whatsapp_auto_reply_config.sql b/migrations/0001_whatsapp_auto_reply_config.sql new file mode 100644 index 0000000..41723b8 --- /dev/null +++ b/migrations/0001_whatsapp_auto_reply_config.sql @@ -0,0 +1 @@ +ALTER TABLE "whatsapp_sessions" ADD COLUMN IF NOT EXISTS "auto_reply_config" jsonb; diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index be75370..42cce47 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1769121114659, "tag": "0000_low_tiger_shark", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1741824000000, + "tag": "0001_whatsapp_auto_reply_config", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/auth.ts b/server/auth.ts index 1e1d7f9..76f2d37 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -30,8 +30,12 @@ async function comparePasswords(supplied: string, stored: string) { return timingSafeEqual(hashedBuf, suppliedBuf); } +if (!process.env.SESSION_SECRET) { + console.warn("[auth] WARNING: SESSION_SECRET env var not set. Using insecure fallback. Set SESSION_SECRET in production."); +} + const sessionSettings: session.SessionOptions = { - secret: process.env.SESSION_SECRET || "arcadia-browser-secret-key-2024", + secret: process.env.SESSION_SECRET || `arcadia-dev-${Math.random().toString(36)}`, resave: false, saveUninitialized: false, store: storage.sessionStore, diff --git a/server/blackboard/PipelineOrchestrator.ts b/server/blackboard/PipelineOrchestrator.ts index 17ee16f..dc46f28 100644 --- a/server/blackboard/PipelineOrchestrator.ts +++ b/server/blackboard/PipelineOrchestrator.ts @@ -58,6 +58,7 @@ const ALL_PHASES: PipelinePhase[] = ["design", "codegen", "validation", "staging class PipelineOrchestrator extends EventEmitter { private activeMonitors: Map = new Map(); + private processingMonitors: Set = new Set(); async createPipeline(prompt: string, userId: string = "system", metadata?: any): Promise { const correlationId = randomUUID(); @@ -169,10 +170,14 @@ class PipelineOrchestrator extends EventEmitter { if (this.activeMonitors.has(pipelineId)) return; const interval = setInterval(async () => { + if (this.processingMonitors.has(pipelineId)) return; + this.processingMonitors.add(pipelineId); try { await this.checkPhaseProgress(pipelineId, mainTaskId); } catch (error) { console.error(`[PipelineOrchestrator] Erro no monitor #${pipelineId}:`, error); + } finally { + this.processingMonitors.delete(pipelineId); } }, 3000); diff --git a/server/crm/frappe-service.ts b/server/crm/frappe-service.ts index 11565c4..73e9ab2 100644 --- a/server/crm/frappe-service.ts +++ b/server/crm/frappe-service.ts @@ -36,6 +36,7 @@ export class FrappeService { "Content-Type": "application/json", Accept: "application/json", }, + signal: AbortSignal.timeout(30000), }; if (data && (method === "POST" || method === "PUT")) { diff --git a/server/lms/routes.ts b/server/lms/routes.ts index 96f2f64..c5344fe 100644 --- a/server/lms/routes.ts +++ b/server/lms/routes.ts @@ -4,6 +4,15 @@ import { sql } from "drizzle-orm"; const router = Router(); +function requireAuth(req: any, res: any, next: any) { + if (!req.isAuthenticated()) { + return res.status(401).json({ error: "Authentication required" }); + } + next(); +} + +router.use(requireAuth); + router.get("/courses", async (req: Request, res: Response) => { try { const { category, featured, published } = req.query; diff --git a/server/manus/service.ts b/server/manus/service.ts index 802b2e5..e29f173 100644 --- a/server/manus/service.ts +++ b/server/manus/service.ts @@ -12,6 +12,8 @@ import * as erpnextService from "../erpnext/service"; const openai = new OpenAI({ apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY, baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL, + timeout: 30000, + maxRetries: 3, }); const SYSTEM_PROMPT = `Você é o Agente Arcádia Manus, um assistente empresarial inteligente e proativo. diff --git a/server/metaset/routes.ts b/server/metaset/routes.ts index 128cfb0..2632d0c 100644 --- a/server/metaset/routes.ts +++ b/server/metaset/routes.ts @@ -4,14 +4,17 @@ import { metasetClient } from "./client"; const METASET_HOST = process.env.METABASE_HOST || "localhost"; const METASET_PORT = parseInt(process.env.METABASE_PORT || "8088", 10); const METASET_URL = `http://${METASET_HOST}:${METASET_PORT}`; -const ADMIN_EMAIL = process.env.METASET_ADMIN_EMAIL || "admin@arcadia.app"; -const ADMIN_PASSWORD = process.env.METASET_ADMIN_PASSWORD || "Arcadia2026!BI"; +const ADMIN_EMAIL = process.env.METASET_ADMIN_EMAIL; +const ADMIN_PASSWORD = process.env.METASET_ADMIN_PASSWORD; export function registerMetaSetRoutes(app: Express): void { app.get("/api/bi/metaset/autologin", async (req: Request, res: Response) => { try { if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" }); + if (!ADMIN_EMAIL || !ADMIN_PASSWORD) { + return res.status(503).json({ error: "MetaSet credentials not configured (METASET_ADMIN_EMAIL / METASET_ADMIN_PASSWORD)" }); + } const sessionResp = await fetch(`${METASET_URL}/api/session`, { method: "POST", diff --git a/server/python/__init__.py b/server/python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/python/automation_engine.py b/server/python/automation_engine.py index 6825b58..d39ed74 100644 --- a/server/python/automation_engine.py +++ b/server/python/automation_engine.py @@ -36,7 +36,7 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[os.getenv("APP_URL", "http://localhost:5000")], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/server/python/bi_analysis_service.py b/server/python/bi_analysis_service.py index 37b73a4..a1621bc 100644 --- a/server/python/bi_analysis_service.py +++ b/server/python/bi_analysis_service.py @@ -23,7 +23,7 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[os.getenv("APP_URL", "http://localhost:5000")], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/server/python/bi_engine.py b/server/python/bi_engine.py index dafb6c0..d9a25ba 100644 --- a/server/python/bi_engine.py +++ b/server/python/bi_engine.py @@ -38,7 +38,7 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[os.getenv("APP_URL", "http://localhost:5000")], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/server/python/contabil_service.py b/server/python/contabil_service.py index 1239cc5..65ef878 100644 --- a/server/python/contabil_service.py +++ b/server/python/contabil_service.py @@ -21,7 +21,7 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[os.getenv("APP_URL", "http://localhost:5000")], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/server/python/embeddings_service.py b/server/python/embeddings_service.py index e14943e..43673be 100644 --- a/server/python/embeddings_service.py +++ b/server/python/embeddings_service.py @@ -133,7 +133,8 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[os.getenv("APP_URL", "http://localhost:5000")], + allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) diff --git a/server/python/fisco_service.py b/server/python/fisco_service.py index 3e6e27a..b6816ff 100644 --- a/server/python/fisco_service.py +++ b/server/python/fisco_service.py @@ -56,7 +56,7 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[os.getenv("APP_URL", "http://localhost:5000")], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/server/python/people_service.py b/server/python/people_service.py index d895b65..e6bfc2b 100644 --- a/server/python/people_service.py +++ b/server/python/people_service.py @@ -20,7 +20,7 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=[os.getenv("APP_URL", "http://localhost:5000")], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/server/quality/routes.ts b/server/quality/routes.ts index db98300..d437c85 100644 --- a/server/quality/routes.ts +++ b/server/quality/routes.ts @@ -13,6 +13,15 @@ import { eq, desc, and, gte, lte, like, or, sql, isNull } from "drizzle-orm"; const router = Router(); +function requireAuth(req: any, res: any, next: any) { + if (!req.isAuthenticated()) { + return res.status(401).json({ error: "Authentication required" }); + } + next(); +} + +router.use(requireAuth); + // ========== AMOSTRAS (RF-QC01) ========== router.get("/samples", async (req: Request, res: Response) => { diff --git a/server/routes.ts b/server/routes.ts index fb18b06..1b0fe2a 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -136,7 +136,10 @@ export async function registerRoutes( // Arcádia Plus - SSO routes (proxy already registered at top) app.use("/api/plus/sso", plusSsoRoutes); - app.get("/api/tenants", async (_req, res) => { + app.get("/api/tenants", async (req: any, res) => { + if (!req.isAuthenticated()) { + return res.status(401).json({ error: "Authentication required" }); + } try { const tenants = await storage.getTenants(); res.json(tenants); diff --git a/server/whatsapp/routes.ts b/server/whatsapp/routes.ts index 71618ab..e739174 100644 --- a/server/whatsapp/routes.ts +++ b/server/whatsapp/routes.ts @@ -229,7 +229,7 @@ export function registerWhatsappRoutes(app: Express): void { const userId = req.user!.id; const config = req.body; - whatsappService.setAutoReplyConfig(userId, config); + await whatsappService.setAutoReplyConfig(userId, config); res.json({ success: true, config: whatsappService.getAutoReplyConfig(userId) }); } catch (error) { console.error("WhatsApp set auto-reply config error:", error); diff --git a/server/whatsapp/service.ts b/server/whatsapp/service.ts index a063206..159ac7f 100644 --- a/server/whatsapp/service.ts +++ b/server/whatsapp/service.ts @@ -9,7 +9,7 @@ import { EventEmitter } from "events"; import * as fs from "fs"; import * as path from "path"; import { db } from "../../db/index"; -import { whatsappContacts, whatsappMessages, whatsappTickets, graphNodes, graphEdges, chatThreads, chatParticipants, chatMessages, pcCrmLeads, tenants } from "@shared/schema"; +import { whatsappContacts, whatsappMessages, whatsappTickets, whatsappSessions, graphNodes, graphEdges, chatThreads, chatParticipants, chatMessages, pcCrmLeads, tenants } from "@shared/schema"; import { eq, and, desc, sql } from "drizzle-orm"; import { learningService } from "../learning/service"; import OpenAI from "openai"; @@ -57,8 +57,17 @@ class WhatsAppService extends EventEmitter { } } - setAutoReplyConfig(userId: string, config: Partial): void { - const existing = this.autoReplyConfigs.get(userId) || { + async setAutoReplyConfig(userId: string, config: Partial): Promise { + const existing = this.autoReplyConfigs.get(userId) || await this.loadAutoReplyConfig(userId); + const merged = { ...existing, ...config }; + this.autoReplyConfigs.set(userId, merged); + await db.update(whatsappSessions) + .set({ autoReplyConfig: merged }) + .where(eq(whatsappSessions.userId, userId)); + } + + async loadAutoReplyConfig(userId: string): Promise { + const defaults: AutoReplyConfig = { enabled: false, welcomeMessage: "Olá! Obrigado por entrar em contato. Em breve um atendente irá te responder.", businessHours: { start: 8, end: 18 }, @@ -66,7 +75,16 @@ class WhatsAppService extends EventEmitter { aiEnabled: true, maxAutoRepliesPerContact: 3, }; - this.autoReplyConfigs.set(userId, { ...existing, ...config }); + try { + const [session] = await db.select({ autoReplyConfig: whatsappSessions.autoReplyConfig }) + .from(whatsappSessions) + .where(eq(whatsappSessions.userId, userId)) + .limit(1); + if (session?.autoReplyConfig) { + return { ...defaults, ...(session.autoReplyConfig as Partial) }; + } + } catch {} + return defaults; } getAutoReplyConfig(userId: string): AutoReplyConfig { diff --git a/server/xos/routes.ts b/server/xos/routes.ts index a5d7c53..ab5f959 100644 --- a/server/xos/routes.ts +++ b/server/xos/routes.ts @@ -4,6 +4,15 @@ import { sql } from "drizzle-orm"; const router = Router(); +function requireAuth(req: any, res: any, next: any) { + if (!req.isAuthenticated()) { + return res.status(401).json({ error: "Authentication required" }); + } + next(); +} + +router.use(requireAuth); + // ========== CONTACTS ========== router.get("/contacts", async (req: Request, res: Response) => { diff --git a/shared/schema.ts b/shared/schema.ts index 67a90ef..93f6168 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -443,6 +443,7 @@ export const whatsappSessions = pgTable("whatsapp_sessions", { status: text("status").default("disconnected"), phoneNumber: text("phone_number"), lastSync: timestamp("last_sync"), + autoReplyConfig: jsonb("auto_reply_config").$type>(), createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(), });