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:
parent
b80bd22c83
commit
1ab50d456b
|
|
@ -41,7 +41,10 @@ case "$SERVICE_NAME" in
|
||||||
--workers 2
|
--workers 2
|
||||||
;;
|
;;
|
||||||
embeddings)
|
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"
|
echo "[entrypoint] ERRO: SERVICE_NAME desconhecido: $SERVICE_NAME"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "whatsapp_sessions" ADD COLUMN IF NOT EXISTS "auto_reply_config" jsonb;
|
||||||
|
|
@ -8,6 +8,13 @@
|
||||||
"when": 1769121114659,
|
"when": 1769121114659,
|
||||||
"tag": "0000_low_tiger_shark",
|
"tag": "0000_low_tiger_shark",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1741824000000,
|
||||||
|
"tag": "0001_whatsapp_auto_reply_config",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -30,8 +30,12 @@ async function comparePasswords(supplied: string, stored: string) {
|
||||||
return timingSafeEqual(hashedBuf, suppliedBuf);
|
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 = {
|
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,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
store: storage.sessionStore,
|
store: storage.sessionStore,
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ const ALL_PHASES: PipelinePhase[] = ["design", "codegen", "validation", "staging
|
||||||
|
|
||||||
class PipelineOrchestrator extends EventEmitter {
|
class PipelineOrchestrator extends EventEmitter {
|
||||||
private activeMonitors: Map<number, NodeJS.Timeout> = new Map();
|
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> {
|
async createPipeline(prompt: string, userId: string = "system", metadata?: any): Promise<XosDevPipeline> {
|
||||||
const correlationId = randomUUID();
|
const correlationId = randomUUID();
|
||||||
|
|
@ -169,10 +170,14 @@ class PipelineOrchestrator extends EventEmitter {
|
||||||
if (this.activeMonitors.has(pipelineId)) return;
|
if (this.activeMonitors.has(pipelineId)) return;
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
|
if (this.processingMonitors.has(pipelineId)) return;
|
||||||
|
this.processingMonitors.add(pipelineId);
|
||||||
try {
|
try {
|
||||||
await this.checkPhaseProgress(pipelineId, mainTaskId);
|
await this.checkPhaseProgress(pipelineId, mainTaskId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[PipelineOrchestrator] Erro no monitor #${pipelineId}:`, error);
|
console.error(`[PipelineOrchestrator] Erro no monitor #${pipelineId}:`, error);
|
||||||
|
} finally {
|
||||||
|
this.processingMonitors.delete(pipelineId);
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export class FrappeService {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
|
signal: AbortSignal.timeout(30000),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data && (method === "POST" || method === "PUT")) {
|
if (data && (method === "POST" || method === "PUT")) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,15 @@ import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
const router = Router();
|
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) => {
|
router.get("/courses", async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { category, featured, published } = req.query;
|
const { category, featured, published } = req.query;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import * as erpnextService from "../erpnext/service";
|
||||||
const openai = new OpenAI({
|
const openai = new OpenAI({
|
||||||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
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.
|
const SYSTEM_PROMPT = `Você é o Agente Arcádia Manus, um assistente empresarial inteligente e proativo.
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,17 @@ import { metasetClient } from "./client";
|
||||||
const METASET_HOST = process.env.METABASE_HOST || "localhost";
|
const METASET_HOST = process.env.METABASE_HOST || "localhost";
|
||||||
const METASET_PORT = parseInt(process.env.METABASE_PORT || "8088", 10);
|
const METASET_PORT = parseInt(process.env.METABASE_PORT || "8088", 10);
|
||||||
const METASET_URL = `http://${METASET_HOST}:${METASET_PORT}`;
|
const METASET_URL = `http://${METASET_HOST}:${METASET_PORT}`;
|
||||||
const ADMIN_EMAIL = process.env.METASET_ADMIN_EMAIL || "admin@arcadia.app";
|
const ADMIN_EMAIL = process.env.METASET_ADMIN_EMAIL;
|
||||||
const ADMIN_PASSWORD = process.env.METASET_ADMIN_PASSWORD || "Arcadia2026!BI";
|
const ADMIN_PASSWORD = process.env.METASET_ADMIN_PASSWORD;
|
||||||
|
|
||||||
export function registerMetaSetRoutes(app: Express): void {
|
export function registerMetaSetRoutes(app: Express): void {
|
||||||
|
|
||||||
app.get("/api/bi/metaset/autologin", async (req: Request, res: Response) => {
|
app.get("/api/bi/metaset/autologin", async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
if (!req.isAuthenticated()) return res.status(401).json({ error: "Not authenticated" });
|
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`, {
|
const sessionResp = await fetch(`${METASET_URL}/api/session`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ app = FastAPI(
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ app = FastAPI(
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ app = FastAPI(
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ app = FastAPI(
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,8 @@ app = FastAPI(
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||||
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ app = FastAPI(
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ app = FastAPI(
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=[os.getenv("APP_URL", "http://localhost:5000")],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,15 @@ import { eq, desc, and, gte, lte, like, or, sql, isNull } from "drizzle-orm";
|
||||||
|
|
||||||
const router = Router();
|
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) ==========
|
// ========== AMOSTRAS (RF-QC01) ==========
|
||||||
|
|
||||||
router.get("/samples", async (req: Request, res: Response) => {
|
router.get("/samples", async (req: Request, res: Response) => {
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,10 @@ export async function registerRoutes(
|
||||||
// Arcádia Plus - SSO routes (proxy already registered at top)
|
// Arcádia Plus - SSO routes (proxy already registered at top)
|
||||||
app.use("/api/plus/sso", plusSsoRoutes);
|
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 {
|
try {
|
||||||
const tenants = await storage.getTenants();
|
const tenants = await storage.getTenants();
|
||||||
res.json(tenants);
|
res.json(tenants);
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ export function registerWhatsappRoutes(app: Express): void {
|
||||||
|
|
||||||
const userId = req.user!.id;
|
const userId = req.user!.id;
|
||||||
const config = req.body;
|
const config = req.body;
|
||||||
whatsappService.setAutoReplyConfig(userId, config);
|
await whatsappService.setAutoReplyConfig(userId, config);
|
||||||
res.json({ success: true, config: whatsappService.getAutoReplyConfig(userId) });
|
res.json({ success: true, config: whatsappService.getAutoReplyConfig(userId) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("WhatsApp set auto-reply config error:", error);
|
console.error("WhatsApp set auto-reply config error:", error);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { EventEmitter } from "events";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { db } from "../../db/index";
|
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 { eq, and, desc, sql } from "drizzle-orm";
|
||||||
import { learningService } from "../learning/service";
|
import { learningService } from "../learning/service";
|
||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
|
|
@ -57,8 +57,17 @@ class WhatsAppService extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setAutoReplyConfig(userId: string, config: Partial<AutoReplyConfig>): void {
|
async setAutoReplyConfig(userId: string, config: Partial<AutoReplyConfig>): Promise<void> {
|
||||||
const existing = this.autoReplyConfigs.get(userId) || {
|
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,
|
enabled: false,
|
||||||
welcomeMessage: "Olá! Obrigado por entrar em contato. Em breve um atendente irá te responder.",
|
welcomeMessage: "Olá! Obrigado por entrar em contato. Em breve um atendente irá te responder.",
|
||||||
businessHours: { start: 8, end: 18 },
|
businessHours: { start: 8, end: 18 },
|
||||||
|
|
@ -66,7 +75,16 @@ class WhatsAppService extends EventEmitter {
|
||||||
aiEnabled: true,
|
aiEnabled: true,
|
||||||
maxAutoRepliesPerContact: 3,
|
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 {
|
getAutoReplyConfig(userId: string): AutoReplyConfig {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,15 @@ import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
const router = Router();
|
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 ==========
|
// ========== CONTACTS ==========
|
||||||
|
|
||||||
router.get("/contacts", async (req: Request, res: Response) => {
|
router.get("/contacts", async (req: Request, res: Response) => {
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,7 @@ export const whatsappSessions = pgTable("whatsapp_sessions", {
|
||||||
status: text("status").default("disconnected"),
|
status: text("status").default("disconnected"),
|
||||||
phoneNumber: text("phone_number"),
|
phoneNumber: text("phone_number"),
|
||||||
lastSync: timestamp("last_sync"),
|
lastSync: timestamp("last_sync"),
|
||||||
|
autoReplyConfig: jsonb("auto_reply_config").$type<Record<string, any>>(),
|
||||||
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue