security: correções críticas de segurança e estabilidade

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
This commit is contained in:
Claude 2026-03-13 14:34:51 +00:00
parent b80bd22c83
commit 1ab50d456b
No known key found for this signature in database
24 changed files with 93 additions and 17 deletions

View File

@ -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"

View File

@ -0,0 +1 @@
ALTER TABLE "whatsapp_sessions" ADD COLUMN IF NOT EXISTS "auto_reply_config" jsonb;

View File

@ -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
}
]
}

0
server/__init__.py Normal file
View File

View File

@ -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,

View File

@ -58,6 +58,7 @@ const ALL_PHASES: PipelinePhase[] = ["design", "codegen", "validation", "staging
class PipelineOrchestrator extends EventEmitter {
private activeMonitors: Map<number, NodeJS.Timeout> = new Map();
private processingMonitors: Set<number> = new Set();
async createPipeline(prompt: string, userId: string = "system", metadata?: any): Promise<XosDevPipeline> {
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);

View File

@ -36,6 +36,7 @@ export class FrappeService {
"Content-Type": "application/json",
Accept: "application/json",
},
signal: AbortSignal.timeout(30000),
};
if (data && (method === "POST" || method === "PUT")) {

View File

@ -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;

View File

@ -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.

View File

@ -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",

View File

View File

@ -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=["*"],

View File

@ -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=["*"],

View File

@ -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=["*"],

View File

@ -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=["*"],

View File

@ -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=["*"],
)

View File

@ -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=["*"],

View File

@ -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=["*"],

View File

@ -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) => {

View File

@ -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);

View File

@ -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);

View File

@ -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<AutoReplyConfig>): void {
const existing = this.autoReplyConfigs.get(userId) || {
async setAutoReplyConfig(userId: string, config: Partial<AutoReplyConfig>): Promise<void> {
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<AutoReplyConfig> {
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<AutoReplyConfig>) };
}
} catch {}
return defaults;
}
getAutoReplyConfig(userId: string): AutoReplyConfig {

View File

@ -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) => {

View File

@ -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<Record<string, any>>(),
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});