Ajustes 2306

This commit is contained in:
2026-06-23 12:42:14 +00:00
parent e7aa5f5333
commit cf720c37a1
18 changed files with 471 additions and 850 deletions
+3 -15
View File
@@ -2,23 +2,11 @@
# Configuracao do Chatc2 # Configuracao do Chatc2
# ============================================================ # ============================================================
# Driver padrao para novas conexoes (postgres | firebird) # Driver padrao (firebird)
DB_DRIVER=postgres DB_DRIVER=firebird
# ------------------------------------------------------------ # ------------------------------------------------------------
# Banco principal: PostgreSQL (alias "novo_local") # Banco Firebird (alias "novo_local")
# ------------------------------------------------------------
PG_HOST=127.0.0.1
PG_PORT=15433
PG_USER=postgres
PG_PASSWORD=postgres
PG_DATABASE=novo_local
# Schema a ser lido (ex.: lajedo, novo, dev). Use "public" se as tabelas
# estiverem no schema padrao.
PG_SCHEMA=public
# ------------------------------------------------------------
# Banco Firebird (alias "firebird_local" / conexoes legadas)
# ------------------------------------------------------------ # ------------------------------------------------------------
DB_HOST=localhost DB_HOST=localhost
DB_PORT=3050 DB_PORT=3050
+21 -41
View File
@@ -20,14 +20,13 @@ SERVER_DOMAIN="${SERVER_DOMAIN:-}" # Opcional: seu-dominio.com.br
JWT_SECRET="" JWT_SECRET=""
NGROK_URL="" NGROK_URL=""
# PostgreSQL EXTERNO (banco principal "novo_local"). # Firebird LOCAL (banco principal).
# Informe via variaveis de ambiente antes de rodar, ou edite o .env depois. # Informe via variaveis de ambiente antes de rodar, ou edite o .env depois.
PG_HOST="${PG_HOST:-}" DB_HOST="${DB_HOST:-localhost}"
PG_PORT="${PG_PORT:-5432}" DB_PORT="${DB_PORT:-3050}"
PG_USER="${PG_USER:-postgres}" DB_USER="${DB_USER:-SYSDBA}"
PG_PASSWORD="${PG_PASSWORD:-}" DB_PASSWORD="${DB_PASSWORD:-masterkey}"
PG_DATABASE="${PG_DATABASE:-postgres}" DB_DATABASE="${DB_DATABASE:-}"
PG_SCHEMA="${PG_SCHEMA:-public}"
# Cores # Cores
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
@@ -247,28 +246,18 @@ cat > "$CHATC2_DIR/.env" << EOF
# ============================================================ # ============================================================
# Driver padrao do banco principal # Driver padrao do banco principal
DB_DRIVER=postgres DB_DRIVER=firebird
# ------------------------------------------------------------ # ------------------------------------------------------------
# PostgreSQL (EXTERNO) — banco principal "novo_local" # Firebird (alias "novo_local")
# ------------------------------------------------------------ # ------------------------------------------------------------
PG_HOST=${PG_HOST} DB_HOST=${DB_HOST}
PG_PORT=${PG_PORT} DB_PORT=${DB_PORT}
PG_USER=${PG_USER} DB_USER=${DB_USER}
PG_PASSWORD=${PG_PASSWORD} DB_PASSWORD=${DB_PASSWORD}
PG_DATABASE=${PG_DATABASE}
PG_SCHEMA=${PG_SCHEMA}
# ------------------------------------------------------------
# Firebird (legado / alias "firebird_local")
# ------------------------------------------------------------
DB_HOST=localhost
DB_PORT=3050
DB_USER=SYSDBA
DB_PASSWORD=masterkey
DB_ENCODING=UTF-8 DB_ENCODING=UTF-8
# Caminho do .FDB (vazio = usa ../NOVO.FDB na raiz do projeto) # Caminho do .FDB (vazio = usa ../NOVO.FDB na raiz do projeto)
DB_DATABASE= DB_DATABASE=${DB_DATABASE}
# Servidor # Servidor
PORT=3000 PORT=3000
@@ -289,12 +278,9 @@ EXTERNAL_URL=${EXTERNAL_URL}
EOF EOF
chown "$CHATC2_USER:$CHATC2_USER" "$CHATC2_DIR/.env" chown "$CHATC2_USER:$CHATC2_USER" "$CHATC2_DIR/.env"
log "Arquivo .env criado/atualizado com JWT_SECRET seguro" log "Arquivo .env criado/atualizado com JWT_SECRET seguro"
log " PG_HOST/DB = ${PG_HOST:-(vazio)} / ${PG_DATABASE} (schema: ${PG_SCHEMA})" log " Firebird = ${DB_HOST}:${DB_PORT} (database: ${DB_DATABASE:-(vazio)})"
log " LOCAL_URL = ${LOCAL_URL}" log " LOCAL_URL = ${LOCAL_URL}"
log " EXTERNAL_URL = ${EXTERNAL_URL}" log " EXTERNAL_URL = ${EXTERNAL_URL}"
if [ -z "$PG_HOST" ] || [ -z "$PG_PASSWORD" ]; then
warn "PostgreSQL externo nao configurado: edite $CHATC2_DIR/.env (PG_HOST, PG_PASSWORD, PG_DATABASE, PG_SCHEMA) antes de iniciar."
fi
# ████████████████████████████████████████████████████ # ████████████████████████████████████████████████████
# PASSO 9: MIGRACOES DO BANCO # PASSO 9: MIGRACOES DO BANCO
@@ -314,23 +300,22 @@ else
warn "Apos copiar, execute: chown $CHATC2_USER:firebird $CHATC2_DIR/db/*.FDB && chmod 660 $CHATC2_DIR/db/*.FDB" warn "Apos copiar, execute: chown $CHATC2_USER:firebird $CHATC2_DIR/db/*.FDB && chmod 660 $CHATC2_DIR/db/*.FDB"
fi fi
info "Passo 9/12: Migracoes (Firebird legado)..." info "Passo 9/12: Migracoes (Firebird)..."
# O schema do PostgreSQL e gerenciado no banco EXTERNO — nao migramos aqui. # Migracoes Firebird rodam se houver um .FDB local (alias novo_local).
# Migracoes Firebird so rodam se houver um .FDB local (alias firebird_local).
if ls "$CHATC2_DIR/db/"*.FDB "$CHATC2_DIR/"*.FDB >/dev/null 2>&1; then if ls "$CHATC2_DIR/db/"*.FDB "$CHATC2_DIR/"*.FDB >/dev/null 2>&1; then
if [ -f "$CHATC2_DIR/scripts/migracoes.js" ]; then if [ -f "$CHATC2_DIR/scripts/migracoes.js" ]; then
su - "$CHATC2_USER" -c "cd $CHATC2_DIR && node scripts/migracoes.js firebird_local 2>&1 | tail -10" || warn "Migracoes Firebird podem ter falhado" su - "$CHATC2_USER" -c "cd $CHATC2_DIR && node scripts/migracoes.js novo_local 2>&1 | tail -10" || warn "Migracoes Firebird podem ter falhado"
log "Migracoes Firebird executadas" log "Migracoes Firebird executadas"
fi fi
else else
info "Sem .FDB local — pulando migracoes Firebird (schema do Postgres e externo)." info "Sem .FDB local — pulando migracoes Firebird."
fi fi
# ████████████████████████████████████████████████████ # ████████████████████████████████████████████████████
# PASSO 9b: HABILITAR USUARIOS ADMIN PARA WEB # PASSO 9b: HABILITAR USUARIOS ADMIN PARA WEB
# ████████████████████████████████████████████████████ # ████████████████████████████████████████████████████
info "Passo 9b/12: Habilitando usuarios admin para acesso web..." info "Passo 9b/12: Habilitando usuarios admin para acesso web..."
# Habilita acesso web para admins (Postgres principal e Firebird se presente) # Habilita acesso web para admins (Firebird)
cat > /tmp/chatc2-habilitar-web.js << 'SCRIPTJS' cat > /tmp/chatc2-habilitar-web.js << 'SCRIPTJS'
const db = require('DATABASE_PATH'); const db = require('DATABASE_PATH');
@@ -352,16 +337,11 @@ const db = require('DATABASE_PATH');
SCRIPTJS SCRIPTJS
sed -i "s|DATABASE_PATH|$CHATC2_DIR/src/database|" /tmp/chatc2-habilitar-web.js sed -i "s|DATABASE_PATH|$CHATC2_DIR/src/database|" /tmp/chatc2-habilitar-web.js
# Postgres externo (alias principal) # Firebird (alias principal)
su - "$CHATC2_USER" -c "cd $CHATC2_DIR && node /tmp/chatc2-habilitar-web.js novo_local" 2>&1 || true su - "$CHATC2_USER" -c "cd $CHATC2_DIR && node /tmp/chatc2-habilitar-web.js novo_local" 2>&1 || true
# Firebird local (apenas se houver .FDB)
if ls "$CHATC2_DIR/db/"*.FDB "$CHATC2_DIR/"*.FDB >/dev/null 2>&1; then
su - "$CHATC2_USER" -c "cd $CHATC2_DIR && node /tmp/chatc2-habilitar-web.js firebird_local" 2>&1 || true
fi
rm -f /tmp/chatc2-habilitar-web.js rm -f /tmp/chatc2-habilitar-web.js
log "Usuarios admin verificados (Postgres + Firebird se presente)" log "Usuarios admin verificados (Firebird)"
# ████████████████████████████████████████████████████ # ████████████████████████████████████████████████████
# PASSO 10: NGINX # PASSO 10: NGINX
+5 -151
View File
@@ -7,6 +7,7 @@
"": { "": {
"name": "chatc2", "name": "chatc2",
"version": "2.0.0", "version": "2.0.0",
"hasInstallScript": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"cors": "^2.8.6", "cors": "^2.8.6",
@@ -14,8 +15,7 @@
"express": "^5.2.1", "express": "^5.2.1",
"ffmpeg-static": "^5.3.0", "ffmpeg-static": "^5.3.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"node-firebird": "^2.3.1", "node-firebird": "^2.3.1"
"pg": "^8.21.0"
} }
}, },
"node_modules/@derhuerst/http-basic": { "node_modules/@derhuerst/http-basic": {
@@ -832,134 +832,6 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/pg": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz",
"integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.13.0",
"pg-pool": "^3.14.0",
"pg-protocol": "^1.14.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.4.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz",
"integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz",
"integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/progress": { "node_modules/progress": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -983,9 +855,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.15.1", "version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.1.0" "side-channel": "^1.1.0"
@@ -1212,15 +1084,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -1314,15 +1177,6 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
} }
} }
} }
+4 -5
View File
@@ -1,14 +1,14 @@
{ {
"name": "chatc2", "name": "chatc2",
"version": "2.0.0", "version": "2.0.0",
"description": "API REST multi-banco (PostgreSQL + Firebird) para a plataforma de atendimento Chatc2", "description": "API REST Firebird para a plataforma de atendimento Chatc2",
"main": "src/server.js", "main": "src/server.js",
"scripts": { "scripts": {
"start": "node src/server.js", "start": "node src/server.js",
"dev": "node --watch src/server.js" "dev": "node --watch src/server.js",
"postinstall": "node scripts/postinstall.js"
}, },
"keywords": [ "keywords": [
"postgres",
"firebird", "firebird",
"api", "api",
"express" "express"
@@ -22,7 +22,6 @@
"express": "^5.2.1", "express": "^5.2.1",
"ffmpeg-static": "^5.3.0", "ffmpeg-static": "^5.3.0",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"node-firebird": "^2.3.1", "node-firebird": "^2.3.1"
"pg": "^8.21.0"
} }
} }
+74
View File
@@ -0,0 +1,74 @@
/**
* Script para corrigir registros existentes com SITUACAO = NULL
*
* Uso: node scripts/corrigir-situacao.js <alias> [schema]
* Ex: node scripts/corrigir-situacao.js novo
* Ex: node scripts/corrigir-situacao.js novo at
*/
require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') });
const db = require('../src/database');
async function main() {
const alias = process.argv[2] || 'novo';
const schema = process.argv[3] ? process.argv[3].trim() : '';
const dot = schema ? '.' : '';
console.log(`📌 Corrigindo registros no alias "${alias}"${schema ? ', schema "' + schema + '"' : ''}...\n`);
// 1. Corrigir CHATC2_INSTANCIAS sem INS_SITUACAO
const sql1 = `UPDATE ${schema}${dot}"CHATC2_INSTANCIAS" SET "INS_SITUACAO" = 'A' WHERE "INS_SITUACAO" IS NULL OR "INS_SITUACAO" = ''`;
console.log(`📌 ${sql1}`);
try {
const r1 = await db.execute(alias, sql1);
console.log(`✅ CHATC2_INSTANCIAS: ${r1.affectedRows} registro(s) atualizado(s)`);
} catch (e) {
console.log(`⚠️ CHATC2_INSTANCIAS: ${e.message.substring(0, 100)}`);
}
// 2. Corrigir CHATC2_CONVERSAS sem CON_SITUACAO
const sql2 = `UPDATE ${schema}${dot}"CHATC2_CONVERSAS" SET "CON_SITUACAO" = 'A' WHERE "CON_SITUACAO" IS NULL OR "CON_SITUACAO" = ''`;
console.log(`📌 ${sql2}`);
try {
const r2 = await db.execute(alias, sql2);
console.log(`✅ CHATC2_CONVERSAS: ${r2.affectedRows} registro(s) atualizado(s)`);
} catch (e) {
console.log(`⚠️ CHATC2_CONVERSAS: ${e.message.substring(0, 100)}`);
}
// 3. Corrigir CHATC2_CONVERSAS_MENSAGENS sem CME_SITUACAO
const sql3 = `UPDATE ${schema}${dot}"CHATC2_CONVERSAS_MENSAGENS" SET "CME_SITUACAO" = 'A' WHERE "CME_SITUACAO" IS NULL OR "CME_SITUACAO" = ''`;
console.log(`📌 ${sql3}`);
try {
const r3 = await db.execute(alias, sql3);
console.log(`✅ CHATC2_CONVERSAS_MENSAGENS: ${r3.affectedRows} registro(s) atualizado(s)`);
} catch (e) {
console.log(`⚠️ CHATC2_CONVERSAS_MENSAGENS: ${e.message.substring(0, 100)}`);
}
// 4. Verifica conversa 61111 especificamente
const sql4 = `SELECT "CON_CODIGO_ID", "CON_SITUACAO", "CON_STATUS" FROM ${schema}${dot}"CHATC2_CONVERSAS" WHERE "CON_CODIGO_ID" = '61111'`;
console.log(`📌 ${sql4}`);
try {
const conv = await db.query(alias, sql4);
if (conv.length > 0) {
console.log(`\n📋 Conversa 61111: SITUACAO=${conv[0].CON_SITUACAO || 'NULL'}, STATUS=${conv[0].CON_STATUS}`);
// Conta mensagens
const sql5 = `SELECT COUNT(*) AS CT FROM ${schema}${dot}"CHATC2_CONVERSAS_MENSAGENS" WHERE "CME_CONVERSA_ID" = '61111' AND "CME_SITUACAO" = 'A'`;
console.log(`📌 ${sql5}`);
const msgs = await db.query(alias, sql5);
console.log(`📋 Mensagens ativas na conversa 61111: ${msgs[0]?.CT || 0}`);
} else {
console.log(`\n❌ Conversa 61111 não encontrada`);
}
} catch (e) {
console.log(`⚠️ Verificação 61111: ${e.message.substring(0, 100)}`);
}
console.log(`\n✅ Correção concluída!`);
process.exit(0);
}
main().catch(err => {
console.error(`❌ Erro: ${err.message}`);
process.exit(1);
});
+3 -5
View File
@@ -22,9 +22,8 @@
const db = require('../src/database'); const db = require('../src/database');
// Este script aplica DDL no dialeto Firebird (BLOB SUB_TYPE, etc.). // Este script aplica DDL no dialeto Firebird (BLOB SUB_TYPE, etc.).
// O schema do PostgreSQL é gerenciado externamente (banco externo) — use este // Alias padrão: novo_local.
// script apenas para bancos Firebird. Alias padrão: firebird_local. const alias = process.argv[2] || 'novo_local';
const alias = process.argv[2] || 'firebird_local';
// ============================================================ // ============================================================
// CONTROLE DE MIGRAÇÕES // CONTROLE DE MIGRAÇÕES
@@ -428,8 +427,7 @@ async function main() {
catch (e) { console.error('❌', e.message); process.exit(1); } catch (e) { console.error('❌', e.message); process.exit(1); }
if (driver !== 'firebird') { if (driver !== 'firebird') {
console.log(`⚠️ O alias "${alias}" usa o driver "${driver}". Este script aplica DDL Firebird.`); console.log(`⚠️ O alias "${alias}" usa o driver "${driver}". Este script aplica DDL Firebird.`);
console.log(' O schema do PostgreSQL é gerenciado no banco externo — nada a fazer aqui.'); console.log(' Para migrar um banco Firebird: node scripts/migracoes.js novo_local\n');
console.log(' Para migrar um banco Firebird: node scripts/migracoes.js firebird_local\n');
process.exit(0); process.exit(0);
} }
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env node
/**
* postinstall.js - Aplica patches de compatibilidade no node-firebird
* após npm install, garantindo compatibilidade com Node.js 22+.
*/
const fs = require('fs');
const path = require('path');
const connPath = path.resolve(__dirname, '../node_modules/node-firebird/lib/wire/connection.js');
if (!fs.existsSync(connPath)) {
console.log('[patch] connection.js not found, skipping');
process.exit(0);
}
let content = fs.readFileSync(connPath, 'utf8');
let patched = false;
// ============================================================
// Patch 1: SRP empty buffer (Firebird 3.0 Legacy_Auth)
// ============================================================
const srpOld = (
' // TODO : Fallback Srp256 to Srp ?\n' +
' /*if (!d.buffer) {\n' +
' cnx.sendOpContAuth(\n' +
' cnx.clientKeys.public.toString(16),\n' +
' DEFAULT_ENCODING,\n' +
' accept.pluginName\n' +
' );\n' +
'\n' +
' return cb(new Error(\'login\'));\n' +
' }*/\n' +
'\n' +
' // Check buffer contains salt\n' +
' var saltLen = d.buffer.readUInt16LE(0);'
);
const srpNew = (
' // No auth data from server - server accepted the connection\n' +
' // without requiring SRP. This happens with Firebird 3.0 when\n' +
' // the server already validated the client.\n' +
' if (!d || !d.buffer) {\n' +
" accept.authData = '';\n" +
" accept.sessionKey = '';\n" +
' } else {\n' +
' // Check buffer contains salt\n' +
' var saltLen = d.buffer.readUInt16LE(0);'
);
if (content.includes(srpOld)) {
content = content.replace(srpOld, srpNew);
patched = true;
console.log('[patch] Patch 1 (SRP) applied');
}
// ============================================================
// Patch 2: Missing closing brace (Node.js 22+ syntax)
// ============================================================
const syntaxOld = (
' accept.authData = proof.authData.toString(16);\n' +
' accept.sessionKey = proof.clientSessionKey;\n' +
' } else if (accept.pluginName === Const.AUTH_PLUGIN_LEGACY) {'
);
const syntaxNew = (
' accept.authData = proof.authData.toString(16);\n' +
' accept.sessionKey = proof.clientSessionKey;\n' +
' } // fecha o else do if (!d || !d.buffer)\n' +
' } else if (accept.pluginName === Const.AUTH_PLUGIN_LEGACY) {'
);
if (content.includes(syntaxOld)) {
content = content.replace(syntaxOld, syntaxNew);
patched = true;
console.log('[patch] Patch 2 (syntax Node22) applied');
}
if (patched) {
fs.writeFileSync(connPath, content, 'utf8');
console.log('[patch] node-firebird patched successfully');
} else {
console.log('[patch] node-firebird already patched or not needed');
}
+2 -2
View File
@@ -62,9 +62,9 @@ app.use(routes);
// Rota raiz // Rota raiz
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.json({ res.json({
name: 'API Firebird - Chatc2', name: 'API Chatc2 - Firebird',
version: '2.0.0', version: '2.0.0',
description: 'API multi-banco com sistema de aliases', description: 'API Firebird com sistema de aliases',
auth: { auth: {
loginPage: 'GET /app/:alias/login', loginPage: 'GET /app/:alias/login',
login: 'POST /app/:alias/login', login: 'POST /app/:alias/login',
+1 -1
View File
@@ -75,7 +75,7 @@ class AuthController {
FROM USUARIOS FROM USUARIOS
WHERE USU_LOGIN = ? WHERE USU_LOGIN = ?
AND (USU_SENHA = ? OR USU_SENHA_WEB = ?) AND (USU_SENHA = ? OR USU_SENHA_WEB = ?)
AND COALESCE(USU_ACESSO_WEB, 0) = 1 AND COALESCE(USU_ACESSO_WEB, '0') IN ('1', 'S')
AND USU_STATUS = 'A' AND USU_STATUS = 'A'
`; `;
+19 -19
View File
@@ -148,7 +148,7 @@ class ChatController {
const statusFilter = req.query.status || 'A,E'; const statusFilter = req.query.status || 'A,E';
const filter = req.query.filter || ''; // 'mine', 'unassigned', 'all' const filter = req.query.filter || ''; // 'mine', 'unassigned', 'all'
if (!req.user?.empresas?.includes(empresaId)) if (!empresaId || !req.user?.empresas?.some(function(e) { return Number(e) === Number(empresaId); }))
return res.status(403).json({ success: false, error: 'Sem permissão.' }); return res.status(403).json({ success: false, error: 'Sem permissão.' });
// Status via parâmetros (evita SQL injection). Aceita apenas A/E/F. // Status via parâmetros (evita SQL injection). Aceita apenas A/E/F.
@@ -260,7 +260,7 @@ class ChatController {
var minhasEquipes = await db.query(alias, var minhasEquipes = await db.query(alias,
'SELECT EQU_EQUIPE_ID FROM CHATC2_USU_EQUIPES WHERE EQU_USUARIO_ID = ?', [userId]); 'SELECT EQU_EQUIPE_ID FROM CHATC2_USU_EQUIPES WHERE EQU_USUARIO_ID = ?', [userId]);
if (minhasEquipes.length > 0) { if (minhasEquipes.length > 0) {
var eqIds = minhasEquipes.map(function(e) { return e.EQU_EQUIPE_ID; }).join(','); var eqIds = minhasEquipes.map(function(e) { return "'" + e.EQU_EQUIPE_ID + "'"; }).join(',');
var countEq = await db.query(alias, var countEq = await db.query(alias,
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS WHERE CON_EMPRESA_ID = ? AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' AND CON_EQUIPE_ID IN (${eqIds})`, `SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS WHERE CON_EMPRESA_ID = ? AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' AND CON_EQUIPE_ID IN (${eqIds})`,
[empresaId]); [empresaId]);
@@ -296,7 +296,7 @@ class ChatController {
const row = result[0]; const row = result[0];
const empresaId = row.CON_EMPRESA_ID; const empresaId = row.CON_EMPRESA_ID;
if (!req.user?.empresas?.includes(empresaId)) if (empresaId && !req.user?.empresas?.some(function(e) { return String(e) === String(empresaId); }))
return res.status(403).json({ success: false, error: 'Sem permissão.' }); return res.status(403).json({ success: false, error: 'Sem permissão.' });
// Verifica se o número pertence a um dependente // Verifica se o número pertence a um dependente
@@ -471,7 +471,7 @@ class ChatController {
if (r.length === 0) return { status: 404 }; if (r.length === 0) return { status: 404 };
const emp = r[0].CON_EMPRESA_ID; const emp = r[0].CON_EMPRESA_ID;
const minhas = (req.user && req.user.empresas) || []; const minhas = (req.user && req.user.empresas) || [];
if (emp && minhas.indexOf(emp) === -1) return { status: 403 }; if (emp && !minhas.some(function(e) { return String(e) === String(emp); })) return { status: 403 };
return { ok: true, empresaId: emp }; return { ok: true, empresaId: emp };
} }
@@ -542,7 +542,7 @@ class ChatController {
const conv = await db.query(alias, 'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]); const conv = await db.query(alias, 'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]);
if (conv.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' }); if (conv.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
if (conv[0].CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).includes(conv[0].CON_EMPRESA_ID)) if (conv[0].CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).some(function(e) { return String(e) === String(conv[0].CON_EMPRESA_ID); }))
return res.status(403).json({ success: false, error: 'Sem permissão.' }); return res.status(403).json({ success: false, error: 'Sem permissão.' });
// Se a conversa estiver finalizada, reabre // Se a conversa estiver finalizada, reabre
@@ -620,8 +620,8 @@ class ChatController {
// Insere mensagem no banco // Insere mensagem no banco
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_DT_ENVIO, CME_MIDIA_ID) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_SITUACAO, CME_DT_ENVIO, CME_MIDIA_ID)
VALUES (?, ?, 'U', ?, ?, ?, ?, CURRENT_TIMESTAMP, ?) VALUES (?, ?, 'U', ?, ?, ?, ?, 'A', CURRENT_TIMESTAMP, ?)
`, [newId, id, usuarioId, textoFinal, tipo || 'text', privada === 'S' ? 'S' : 'N', midiaId]); `, [newId, id, usuarioId, textoFinal, tipo || 'text', privada === 'S' ? 'S' : 'N', midiaId]);
// Atualiza data da última mensagem // Atualiza data da última mensagem
@@ -663,7 +663,7 @@ class ChatController {
if (conv.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' }); if (conv.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' });
const c = conv[0]; const c = conv[0];
if (c.CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).includes(c.CON_EMPRESA_ID)) if (c.CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).some(function(e) { return String(e) === String(c.CON_EMPRESA_ID); }))
return res.status(403).json({ success: false, error: 'Sem permissão.' }); return res.status(403).json({ success: false, error: 'Sem permissão.' });
// ===== Fluxo de Resolução: valida motivo/resolução conforme config ===== // ===== Fluxo de Resolução: valida motivo/resolução conforme config =====
@@ -781,7 +781,7 @@ class ChatController {
// Verifica permissão // Verifica permissão
const userEmpresas = req.user?.empresas || []; const userEmpresas = req.user?.empresas || [];
if (!userEmpresas.includes(parseInt(empresaId))) { if (!userEmpresas.some(function(e) { return String(e) === String(empresaId); })) {
return res.status(403).json({ success: false, error: 'Sem permissão para esta empresa.' }); return res.status(403).json({ success: false, error: 'Sem permissão para esta empresa.' });
} }
@@ -827,8 +827,8 @@ class ChatController {
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
const msgId = (maxMsgId[0]?.ID || 0) + 1; const msgId = (maxMsgId[0]?.ID || 0) + 1;
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'U', ?, ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [msgId, conversaId, req.user?.id, mensagem]); `, [msgId, conversaId, req.user?.id, mensagem]);
await db.execute(alias, await db.execute(alias,
@@ -860,8 +860,8 @@ class ChatController {
// Insere conversa // Insere conversa
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS (CON_CODIGO_ID, CON_EMPRESA_ID, CON_INSTANCIA_ID, CON_NUMERO, INSERT INTO CHATC2_CONVERSAS (CON_CODIGO_ID, CON_EMPRESA_ID, CON_INSTANCIA_ID, CON_NUMERO,
CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_SITUACAO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG)
VALUES (?, ?, ?, ?, ?, ?, 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) VALUES (?, ?, ?, ?, ?, ?, 'A', 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [newId, empresaId, instId, numeroLimpo, nomeContato || numeroLimpo, clienteId || null]); `, [newId, empresaId, instId, numeroLimpo, nomeContato || numeroLimpo, clienteId || null]);
// Insere primeira mensagem // Insere primeira mensagem
@@ -869,8 +869,8 @@ class ChatController {
const msgId = (maxMsgId[0]?.ID || 0) + 1; const msgId = (maxMsgId[0]?.ID || 0) + 1;
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'U', ?, ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'U', ?, ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [msgId, newId, req.user?.id, mensagem]); `, [msgId, newId, req.user?.id, mensagem]);
// Envia via Evolution API // Envia via Evolution API
@@ -1058,7 +1058,7 @@ class ChatController {
mimeType = mimeType.split(';')[0].trim(); mimeType = mimeType.split(';')[0].trim();
const nomeArquivo = (m.MAT_NOME_ARQUIVO || '').trim() || 'arquivo.bin'; const nomeArquivo = (m.MAT_NOME_ARQUIVO || '').trim() || 'arquivo.bin';
// MAT_ARQUIVO chega como Buffer (postgres bytea / firebird blob) ou string // MAT_ARQUIVO chega como Buffer (firebird blob) ou string
let rawBuffer = m.MAT_ARQUIVO; let rawBuffer = m.MAT_ARQUIVO;
if (rawBuffer == null) { if (rawBuffer == null) {
return respond(404, { success: false, error: 'Mídia sem conteúdo.' }); return respond(404, { success: false, error: 'Mídia sem conteúdo.' });
@@ -1179,7 +1179,7 @@ class ChatController {
const cli = await db.query(alias, const cli = await db.query(alias,
'SELECT CLI_EMPRESA_ID FROM CLIENTES WHERE CLI_CODIGO_ID = ?', [cid]); 'SELECT CLI_EMPRESA_ID FROM CLIENTES WHERE CLI_CODIGO_ID = ?', [cid]);
if (cli.length === 0) return { status: 404 }; if (cli.length === 0) return { status: 404 };
if (chk.empresaId && cli[0].CLI_EMPRESA_ID && cli[0].CLI_EMPRESA_ID !== chk.empresaId) if (chk.empresaId && cli[0].CLI_EMPRESA_ID && String(cli[0].CLI_EMPRESA_ID) !== String(chk.empresaId))
return { status: 403 }; return { status: 403 };
return { ok: true, clienteId: cid }; return { ok: true, clienteId: cid };
} }
@@ -1282,8 +1282,8 @@ class ChatController {
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
const newId = (maxId[0]?.ID || 0) + 1; const newId = (maxId[0]?.ID || 0) + 1;
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_USUARIO_ID, CME_TEXTO, CME_TIPO, CME_PRIVADA, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'U', ?, ?, 'text', 'N', CURRENT_TIMESTAMP) VALUES (?, ?, 'U', ?, ?, 'text', 'N', 'A', CURRENT_TIMESTAMP)
`, [newId, id, req.user?.id, texto]); `, [newId, id, req.user?.id, texto]);
await db.execute(alias, await db.execute(alias,
'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?', [id]); 'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?', [id]);
+1 -31
View File
@@ -89,35 +89,6 @@ class DatabaseController {
return res.status(400).json({ success: false, error: 'database é obrigatório.' }); return res.status(400).json({ success: false, error: 'database é obrigatório.' });
} }
var driver = (req.body.driver || DEFAULT_DRIVER).toLowerCase();
if (driver === 'postgres') {
var pg = require('pg');
var schema = String(req.body.schema || 'public').trim();
var safe = /^[A-Za-z_][A-Za-z0-9_$]*$/.test(schema) ? schema : 'public';
var searchPath = safe === 'public' ? 'public' : safe + ',public';
var client = new pg.Client({
host: req.body.host || '127.0.0.1',
port: req.body.port || 5432,
database: req.body.database,
user: req.body.user || 'postgres',
password: req.body.password || 'postgres',
ssl: req.body.ssl ? { rejectUnauthorized: false } : false,
options: '-c search_path=' + searchPath,
connectionTimeoutMillis: 8000,
});
try {
await client.connect();
await client.query('SELECT 1');
// Confirma que o schema existe
var sc = await client.query('SELECT 1 FROM information_schema.schemata WHERE schema_name = $1', [safe]);
if (sc.rowCount === 0) {
throw new Error('Schema "' + safe + '" não encontrado no banco "' + req.body.database + '".');
}
} finally {
await client.end().catch(function () {});
}
} else {
var Firebird = require('node-firebird'); var Firebird = require('node-firebird');
var config = { var config = {
host: req.body.host || 'localhost', host: req.body.host || 'localhost',
@@ -139,9 +110,8 @@ class DatabaseController {
}); });
}); });
}); });
}
res.json({ success: true, message: '✅ Conexão (' + driver + ') estabelecida com sucesso!' }); res.json({ success: true, message: '✅ Conexão (firebird) estabelecida com sucesso!' });
} catch (err) { } catch (err) {
res.status(500).json({ success: false, error: err.message }); res.status(500).json({ success: false, error: err.message });
} }
+1 -1
View File
@@ -38,7 +38,7 @@ class EmpresaController {
} catch (fe) { /* ignora e tenta o BLOB */ } } catch (fe) { /* ignora e tenta o BLOB */ }
} }
// 2. BLOB EMP_FOTO (Buffer no postgres/firebird; string base64 em alguns casos) // 2. BLOB EMP_FOTO (Buffer no firebird; string base64 em alguns casos)
const blobData = e.EMP_FOTO; const blobData = e.EMP_FOTO;
let base64; let base64;
if (Buffer.isBuffer(blobData)) { if (Buffer.isBuffer(blobData)) {
+28 -15
View File
@@ -242,18 +242,18 @@ class EvolutionController {
}); });
instanceCreated = true; instanceCreated = true;
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_INSTANCIAS (INS_CODIGO_ID, INS_EMPRESA_ID, INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME, INS_STATUS) INSERT INTO CHATC2_INSTANCIAS (INS_CODIGO_ID, INS_EMPRESA_ID, INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME, INS_STATUS, INS_SITUACAO)
VALUES (?, ?, ?, ?, ?, ?, 'C') VALUES (?, ?, ?, ?, ?, ?, 'C', 'A')
`, [newId, INS_EMPRESA_ID || userEmpresas[0], INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME]); `, [newId, INS_EMPRESA_ID || userEmpresas[0], INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME]);
break; break;
} catch (e) { /* Tenta próximo endpoint */ } } catch (e) { /* Tenta próximo endpoint */ }
} }
if (!instanceCreated) { if (!instanceCreated) {
// Salva mesmo sem conseguir criar na Evolution // Salva mesmo sem conseguir criar na Evolution (status 'D' = desconectado)
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_INSTANCIAS (INS_CODIGO_ID, INS_EMPRESA_ID, INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME, INS_STATUS) INSERT INTO CHATC2_INSTANCIAS (INS_CODIGO_ID, INS_EMPRESA_ID, INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME, INS_STATUS, INS_SITUACAO)
VALUES (?, ?, ?, ?, ?, ?, 'D') VALUES (?, ?, ?, ?, ?, ?, 'D', 'A')
`, [newId, INS_EMPRESA_ID || userEmpresas[0], INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME]); `, [newId, INS_EMPRESA_ID || userEmpresas[0], INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME]);
} }
@@ -270,8 +270,21 @@ class EvolutionController {
static async listInstances(req, res) { static async listInstances(req, res) {
try { try {
const { alias } = req.params; const { alias } = req.params;
const empresaId = parseInt(req.query.empresaId) || req.user?.empresas?.[0];
if (!req.user?.empresas?.includes(empresaId)) return res.status(403).json({ success: false, error: 'Sem permissão.' }); // Garante que userEmpresas está carregado (fallback para DB se JWT não tiver)
let userEmpresas = req.user?.empresas || [];
if (userEmpresas.length === 0 && req.user?.id) {
try {
const empresasDb = await db.query(alias,
'SELECT USE_EMPRESA_ID FROM USUARIOS_EMPRESA WHERE USE_USUARIO_ID = ?',
[req.user.id]
);
userEmpresas = empresasDb.map(e => e.USE_EMPRESA_ID);
} catch (e) { /* ignora falha */ }
}
const empresaId = parseInt(req.query.empresaId) || userEmpresas[0];
if (!userEmpresas.includes(empresaId)) return res.status(403).json({ success: false, error: 'Sem permissão.' });
const result = await db.query(alias, const result = await db.query(alias,
'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_EMPRESA_ID = ? AND INS_SITUACAO = \'A\' ORDER BY INS_NOME', 'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_EMPRESA_ID = ? AND INS_SITUACAO = \'A\' ORDER BY INS_NOME',
@@ -772,8 +785,8 @@ async function processWebhook(alias, body) {
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS (CON_CODIGO_ID, CON_EMPRESA_ID, CON_INSTANCIA_ID, CON_NUMERO, INSERT INTO CHATC2_CONVERSAS (CON_CODIGO_ID, CON_EMPRESA_ID, CON_INSTANCIA_ID, CON_NUMERO,
CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_SITUACAO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG)
VALUES (?, ?, ?, ?, ?, ?, 'E', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) VALUES (?, ?, ?, ?, ?, ?, 'E', 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, [newId, empresaId, instanciaId, numero, nomeFinal, clienteId]); `, [newId, empresaId, instanciaId, numero, nomeFinal, clienteId]);
conversa = await db.query(alias, 'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [newId]); conversa = await db.query(alias, 'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [newId]);
@@ -789,8 +802,8 @@ async function processWebhook(alias, body) {
if (msgSaudacao) { if (msgSaudacao) {
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxMsgId[0]?.ID || 0) + 1, conversa.CON_CODIGO_ID, msgSaudacao]); `, [(maxMsgId[0]?.ID || 0) + 1, conversa.CON_CODIGO_ID, msgSaudacao]);
await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_SAUDACAO_ENVIADA = 'S' WHERE CON_CODIGO_ID = ?", [conversa.CON_CODIGO_ID]); await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_SAUDACAO_ENVIADA = 'S' WHERE CON_CODIGO_ID = ?", [conversa.CON_CODIGO_ID]);
@@ -1205,13 +1218,13 @@ async function processWebhook(alias, body) {
if (midiaId) { if (midiaId) {
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO, CME_MIDIA_ID) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO, CME_MIDIA_ID)
VALUES (?, ?, 'C', ?, ?, CURRENT_TIMESTAMP, ?) VALUES (?, ?, 'C', ?, ?, 'A', CURRENT_TIMESTAMP, ?)
`, [(maxMsgId[0]?.ID || 0) + 1, conversa.CON_CODIGO_ID, messageText, messageType, midiaId]); `, [(maxMsgId[0]?.ID || 0) + 1, conversa.CON_CODIGO_ID, messageText, messageType, midiaId]);
} else { } else {
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'C', ?, ?, CURRENT_TIMESTAMP) VALUES (?, ?, 'C', ?, ?, 'A', CURRENT_TIMESTAMP)
`, [(maxMsgId[0]?.ID || 0) + 1, conversa.CON_CODIGO_ID, messageText, messageType]); `, [(maxMsgId[0]?.ID || 0) + 1, conversa.CON_CODIGO_ID, messageText, messageType]);
} }
+3 -3
View File
@@ -145,14 +145,14 @@ class RoutesController {
'GET /api/databases': { desc: 'Lista as conexões de banco cadastradas (senhas ocultas).' }, 'GET /api/databases': { desc: 'Lista as conexões de banco cadastradas (senhas ocultas).' },
'POST /api/databases': { 'POST /api/databases': {
desc: 'Adiciona ou atualiza uma conexão de banco (persistida em databases_custom.json).', desc: 'Adiciona ou atualiza uma conexão de banco (persistida em databases_custom.json).',
body: { alias: 'novo_dev', config: { driver: 'postgres', host: '127.0.0.1', port: 15433, database: 'novo_local', schema: 'dev', user: 'postgres', password: 'postgres' } }, body: { alias: 'novo_dev', config: { driver: 'firebird', host: '127.0.0.1', port: 3050, database: '/opt/chatc2/db/NOVO.FDB', user: 'SYSDBA', password: 'masterkey' } },
req: ['alias', 'config'], req: ['alias', 'config'],
note: 'driver: "postgres" (usa schema) ou "firebird" (database = caminho do .FDB).', note: 'driver: "firebird" (database = caminho do .FDB).',
}, },
'DELETE /api/databases/{alias}': { desc: 'Remove uma conexão customizada (estáticas não podem ser removidas).' }, 'DELETE /api/databases/{alias}': { desc: 'Remove uma conexão customizada (estáticas não podem ser removidas).' },
'POST /api/databases/test': { 'POST /api/databases/test': {
desc: 'Testa uma conexão sem cadastrá-la.', desc: 'Testa uma conexão sem cadastrá-la.',
body: { driver: 'postgres', host: '127.0.0.1', port: 15433, database: 'novo_local', schema: 'public', user: 'postgres', password: 'postgres' }, body: { driver: 'firebird', host: '127.0.0.1', port: 3050, database: '/opt/chatc2/db/NOVO.FDB', user: 'SYSDBA', password: 'masterkey' },
req: ['database'], req: ['database'],
}, },
+32 -32
View File
@@ -72,8 +72,8 @@ class TriageController {
// Salva mensagem do sistema // Salva mensagem do sistema
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'triagem', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'triagem', 'A', CURRENT_TIMESTAMP)
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, menuText]); `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, menuText]);
// Envia via Evolution // Envia via Evolution
@@ -121,8 +121,8 @@ class TriageController {
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'triagem', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'triagem', 'A', CURRENT_TIMESTAMP)
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, menuText]); `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, menuText]);
await TriageController.sendEvolution(alias, instanciaId, numero, menuText); await TriageController.sendEvolution(alias, instanciaId, numero, menuText);
@@ -246,8 +246,8 @@ class TriageController {
if (msgAfter) { if (msgAfter) {
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msgAfter]); `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msgAfter]);
await TriageController.sendEvolution(alias, instanciaId, numero, msgAfter); await TriageController.sendEvolution(alias, instanciaId, numero, msgAfter);
} }
@@ -377,8 +377,8 @@ class TriageController {
if (textoMsg) { if (textoMsg) {
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, textoMsg]); `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, textoMsg]);
await TriageController.sendEvolution(alias, instanciaId, numero, textoMsg); await TriageController.sendEvolution(alias, instanciaId, numero, textoMsg);
} }
@@ -396,8 +396,8 @@ class TriageController {
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msgPrompt]); `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msgPrompt]);
await TriageController.sendEvolution(alias, instanciaId, numero, msgPrompt); await TriageController.sendEvolution(alias, instanciaId, numero, msgPrompt);
@@ -521,8 +521,8 @@ class TriageController {
if (resultado) { if (resultado) {
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, resultado]); `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, resultado]);
await TriageController.sendEvolution(alias, instanciaId, numero, resultado); await TriageController.sendEvolution(alias, instanciaId, numero, resultado);
} }
@@ -579,8 +579,8 @@ class TriageController {
var msgNaoEncontrado = 'Este número não está associado a nenhum contrato no sistema.\n\nPor favor, selecione um setor para que um atendente possa ajudá-lo a associar seu número.'; var msgNaoEncontrado = 'Este número não está associado a nenhum contrato no sistema.\n\nPor favor, selecione um setor para que um atendente possa ajudá-lo a associar seu número.';
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msgNaoEncontrado]); `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msgNaoEncontrado]);
await TriageController.sendEvolution(alias, instanciaId, numero, msgNaoEncontrado); await TriageController.sendEvolution(alias, instanciaId, numero, msgNaoEncontrado);
await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_MENU_ESTADO = 'root' WHERE CON_CODIGO_ID = ?", [conversaId]); await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_MENU_ESTADO = 'root' WHERE CON_CODIGO_ID = ?", [conversaId]);
@@ -605,8 +605,8 @@ class TriageController {
logTriage('Boleto - NENHUM título em aberto'); logTriage('Boleto - NENHUM título em aberto');
const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msg]); `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msg]);
await TriageController.sendEvolution(alias, instanciaId, numero, msg); await TriageController.sendEvolution(alias, instanciaId, numero, msg);
await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_MENU_ESTADO = 'root' WHERE CON_CODIGO_ID = ?", [conversaId]); await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_MENU_ESTADO = 'root' WHERE CON_CODIGO_ID = ?", [conversaId]);
@@ -624,8 +624,8 @@ class TriageController {
const maxMsgId2 = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxMsgId2 = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxMsgId2[0]?.ID || 0) + 1, conversaId, msgBoleto]); `, [(maxMsgId2[0]?.ID || 0) + 1, conversaId, msgBoleto]);
await TriageController.sendEvolution(alias, instanciaId, numero, msgBoleto); await TriageController.sendEvolution(alias, instanciaId, numero, msgBoleto);
@@ -652,8 +652,8 @@ class TriageController {
const msg = 'Opção inválida. Tente novamente.'; const msg = 'Opção inválida. Tente novamente.';
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxId[0]?.ID || 0) + 1, conversaId, msg]); `, [(maxId[0]?.ID || 0) + 1, conversaId, msg]);
await TriageController.sendEvolution(alias, instanciaId, numero, msg); await TriageController.sendEvolution(alias, instanciaId, numero, msg);
return; return;
@@ -838,8 +838,8 @@ class TriageController {
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxId[0]?.ID || 0) + 1, conversaId, textoBoleto]); `, [(maxId[0]?.ID || 0) + 1, conversaId, textoBoleto]);
await TriageController.sendEvolution(alias, instanciaId, numero, textoBoleto); await TriageController.sendEvolution(alias, instanciaId, numero, textoBoleto);
} }
@@ -848,8 +848,8 @@ class TriageController {
var msgCont = '📄 Boleto enviado!\n\nDeseja continuar com o atendimento?\n\n1 - Sim\n2 - Não, finalizar'; var msgCont = '📄 Boleto enviado!\n\nDeseja continuar com o atendimento?\n\n1 - Sim\n2 - Não, finalizar';
const maxIdCont = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxIdCont = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxIdCont[0]?.ID || 0) + 1, conversaId, msgCont]); `, [(maxIdCont[0]?.ID || 0) + 1, conversaId, msgCont]);
await TriageController.sendEvolution(alias, instanciaId, numero, msgCont); await TriageController.sendEvolution(alias, instanciaId, numero, msgCont);
@@ -889,8 +889,8 @@ class TriageController {
var msgFinal = 'Atendimento finalizado. Obrigado pelo contato!'; var msgFinal = 'Atendimento finalizado. Obrigado pelo contato!';
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxId[0]?.ID || 0) + 1, conversaId, msgFinal]); `, [(maxId[0]?.ID || 0) + 1, conversaId, msgFinal]);
await TriageController.sendEvolution(alias, instanciaId, numero, msgFinal); await TriageController.sendEvolution(alias, instanciaId, numero, msgFinal);
await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_STATUS = 'F', CON_DT_FINAL = CURRENT_TIMESTAMP, CON_MENU_ESTADO = NULL WHERE CON_CODIGO_ID = ?", [conversaId]); await db.execute(alias, "UPDATE CHATC2_CONVERSAS SET CON_STATUS = 'F', CON_DT_FINAL = CURRENT_TIMESTAMP, CON_MENU_ESTADO = NULL WHERE CON_CODIGO_ID = ?", [conversaId]);
@@ -898,8 +898,8 @@ class TriageController {
var msgInv = 'Por favor, responda:\n\n1 - Sim\n2 - Não, finalizar'; var msgInv = 'Por favor, responda:\n\n1 - Sim\n2 - Não, finalizar';
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxId[0]?.ID || 0) + 1, conversaId, msgInv]); `, [(maxId[0]?.ID || 0) + 1, conversaId, msgInv]);
await TriageController.sendEvolution(alias, instanciaId, numero, msgInv); await TriageController.sendEvolution(alias, instanciaId, numero, msgInv);
} }
@@ -916,8 +916,8 @@ class TriageController {
const msg = 'Opção inválida. Tente novamente.'; const msg = 'Opção inválida. Tente novamente.';
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxId[0]?.ID || 0) + 1, conversaId, msg]); `, [(maxId[0]?.ID || 0) + 1, conversaId, msg]);
await TriageController.sendEvolution(alias, instanciaId, numero, msg); await TriageController.sendEvolution(alias, instanciaId, numero, msg);
@@ -949,8 +949,8 @@ class TriageController {
if (texto) { if (texto) {
const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS');
await db.execute(alias, ` await db.execute(alias, `
INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO)
VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) VALUES (?, ?, 'S', ?, 'text', 'A', CURRENT_TIMESTAMP)
`, [(maxId[0]?.ID || 0) + 1, conversaId, texto]); `, [(maxId[0]?.ID || 0) + 1, conversaId, texto]);
await TriageController.sendEvolution(alias, instanciaId, numero, texto); await TriageController.sendEvolution(alias, instanciaId, numero, texto);
} }
+60 -415
View File
@@ -5,13 +5,11 @@
* *
* 1) CONFIGURAÇÃO — aliases, drivers, conexões estáticas e customizadas * 1) CONFIGURAÇÃO — aliases, drivers, conexões estáticas e customizadas
* 2) DRIVER FIREBIRD * 2) DRIVER FIREBIRD
* 3) DRIVER POSTGRES (pool + tradutor de SQL + schema) * 3) DISPATCHER — API pública usada por todo o sistema
* 4) DISPATCHER — API pública usada por todo o sistema
* *
* Suporta múltiplos drivers ('postgres' padrão, 'firebird' legado). Cada * Suporte ao banco Firebird. Cada conexão declara o campo `driver`.
* conexão declara o campo `driver`. Conexões adicionadas em runtime (via API * Conexões adicionadas em runtime (via API /api/databases) são persistidas
* /api/databases) são persistidas em databases_custom.json (ao lado deste * em databases_custom.json (ao lado deste arquivo).
* arquivo) — é um arquivo de DADOS, não de código.
*/ */
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
@@ -31,22 +29,11 @@ try {
console.error('[Databases] Erro ao carregar databases_custom.json:', e.message); console.error('[Databases] Erro ao carregar databases_custom.json:', e.message);
} }
const DRIVERS = ['postgres', 'firebird']; const DRIVERS = ['firebird'];
const DEFAULT_DRIVER = (process.env.DB_DRIVER || 'postgres').toLowerCase(); const DEFAULT_DRIVER = 'firebird';
// Valores padrão por driver // Valores padrão para Firebird
const DRIVER_DEFAULTS = { const DRIVER_DEFAULTS = {
postgres: {
host: '127.0.0.1',
port: 5432,
user: 'postgres',
password: 'postgres',
schema: 'public',
ssl: false,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 10000,
},
firebird: { firebird: {
host: 'localhost', host: 'localhost',
port: 3050, port: 3050,
@@ -61,22 +48,10 @@ const DRIVER_DEFAULTS = {
/** /**
* Conexões estáticas. * Conexões estáticas.
* - `novo_local` → PostgreSQL (conexão principal) * - `novo_local` → Firebird (informe o CAMINHO do arquivo .FDB em `database`)
* - `firebird_local`→ Firebird (informe o CAMINHO do arquivo .FDB em `database`) * Sobrescrevível pelo .env (DB_*).
* Sobrescrevíveis pelo .env (PG_* para Postgres, DB_* para Firebird).
*/ */
const databases = { const databases = {
novo: {
driver: 'postgres',
host: process.env.PG_HOST || 'db.assantos.app.br',
port: parseInt(process.env.PG_PORT, 10) || 443,
user: process.env.PG_USER || 'atendimento_user',
password: process.env.PG_PASSWORD || 'PSSQDfPHilIZF1mbGzuR2RrEh0cTEGaF',
database: process.env.PG_DATABASE || 'atendimento',
schema: process.env.PG_SCHEMA || 'atendi',
ssl: process.env.PG_SSL === 'true' ? true : process.env.PG_SSL === 'false' ? false : (parseInt(process.env.PG_PORT, 10) === 443 ? { rejectUnauthorized: false } : false),
},
novo_local: { novo_local: {
driver: 'firebird', driver: 'firebird',
host: process.env.DB_HOST || 'localhost', host: process.env.DB_HOST || 'localhost',
@@ -105,30 +80,10 @@ function salvarCustomDatabases() {
/** Normaliza uma config bruta aplicando o driver e seus defaults. */ /** Normaliza uma config bruta aplicando o driver e seus defaults. */
function normalize(raw) { function normalize(raw) {
const driver = DRIVERS.includes((raw.driver || '').toLowerCase()) const d = DRIVER_DEFAULTS['firebird'];
? raw.driver.toLowerCase()
: DEFAULT_DRIVER;
const d = DRIVER_DEFAULTS[driver];
if (driver === 'postgres') {
return { return {
driver, driver: 'firebird',
host: raw.host || d.host,
port: raw.port || d.port,
database: raw.database,
user: raw.user || d.user,
password: raw.password || d.password,
schema: (raw.schema || d.schema || 'public'),
ssl: raw.ssl !== undefined ? raw.ssl : d.ssl,
max: raw.max || d.max,
idleTimeoutMillis: raw.idleTimeoutMillis || d.idleTimeoutMillis,
connectionTimeoutMillis: raw.connectionTimeoutMillis || d.connectionTimeoutMillis,
};
}
// firebird
return {
driver,
host: raw.host || d.host, host: raw.host || d.host,
port: raw.port || d.port, port: raw.port || d.port,
database: raw.database, database: raw.database,
@@ -178,14 +133,10 @@ function addDatabase(alias, config) {
throw new Error('Alias e database são obrigatórios.'); throw new Error('Alias e database são obrigatórios.');
} }
var aliasLower = alias.toLowerCase().replace(/[^a-z0-9_]/g, '_'); var aliasLower = alias.toLowerCase().replace(/[^a-z0-9_]/g, '_');
var driver = DRIVERS.includes((config.driver || '').toLowerCase())
? config.driver.toLowerCase()
: DEFAULT_DRIVER;
var stored = { driver: driver }; var stored = { driver: 'firebird' };
['host', 'port', 'database', 'schema', 'user', 'password', 'ssl', ['host', 'port', 'database', 'user', 'password',
'encoding', 'lowercase_keys', 'role', 'pageSize', 'wireCrypt', 'encoding', 'lowercase_keys', 'role', 'pageSize', 'wireCrypt']
'max', 'idleTimeoutMillis', 'connectionTimeoutMillis']
.forEach(function (k) { .forEach(function (k) {
if (config[k] !== undefined && config[k] !== null && config[k] !== '') { if (config[k] !== undefined && config[k] !== null && config[k] !== '') {
stored[k] = config[k]; stored[k] = config[k];
@@ -208,10 +159,9 @@ function removeDatabase(alias) {
// ============================================================ // ============================================================
// 2) DRIVER FIREBIRD // 2) DRIVER FIREBIRD
// ============================================================ // ============================================================
const firebirdDriver = (() => { const Firebird = require('node-firebird');
const Firebird = require('node-firebird');
function readBlob(blobFunc) { function readBlob(blobFunc) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
if (typeof blobFunc !== 'function') return resolve(blobFunc); if (typeof blobFunc !== 'function') return resolve(blobFunc);
blobFunc(function (err, name, emitter) { blobFunc(function (err, name, emitter) {
@@ -224,9 +174,9 @@ const firebirdDriver = (() => {
emitter.on('error', reject); emitter.on('error', reject);
}); });
}); });
} }
function query(config, sql, params = []) { function firebirdQuery(config, sql, params = []) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Firebird.attach(config, (err, db) => { Firebird.attach(config, (err, db) => {
if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`)); if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`));
@@ -248,9 +198,9 @@ const firebirdDriver = (() => {
}); });
}); });
}); });
} }
function execute(config, sql, params = []) { function firebirdExecute(config, sql, params = []) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Firebird.attach(config, (err, db) => { Firebird.attach(config, (err, db) => {
if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`)); if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`));
@@ -274,25 +224,25 @@ const firebirdDriver = (() => {
}); });
}); });
}); });
} }
async function testConnection(config) { async function firebirdTestConnection(config) {
await query(config, 'SELECT 1 FROM RDB$DATABASE'); await firebirdQuery(config, 'SELECT 1 FROM RDB$DATABASE');
return true; return true;
} }
async function listTables(config) { async function firebirdListTables(config) {
const rows = await query(config, ` const rows = await firebirdQuery(config, `
SELECT TRIM(RDB$RELATION_NAME) AS TABLE_NAME SELECT TRIM(RDB$RELATION_NAME) AS TABLE_NAME
FROM RDB$RELATIONS FROM RDB$RELATIONS
WHERE RDB$SYSTEM_FLAG = 0 AND RDB$RELATION_TYPE = 0 WHERE RDB$SYSTEM_FLAG = 0 AND RDB$RELATION_TYPE = 0
ORDER BY RDB$RELATION_NAME ORDER BY RDB$RELATION_NAME
`); `);
return rows.map((r) => (r.TABLE_NAME || '').trim()).filter(Boolean); return rows.map((r) => (r.TABLE_NAME || '').trim()).filter(Boolean);
} }
async function tableInfo(config, tableName) { async function firebirdTableInfo(config, tableName) {
const rows = await query(config, ` const rows = await firebirdQuery(config, `
SELECT SELECT
rf.RDB$FIELD_NAME AS COLUMN_NAME, rf.RDB$FIELD_NAME AS COLUMN_NAME,
rf.RDB$FIELD_POSITION AS ORDINAL_POSITION, rf.RDB$FIELD_POSITION AS ORDINAL_POSITION,
@@ -323,363 +273,58 @@ const firebirdDriver = (() => {
scale: row.FIELD_SCALE, scale: row.FIELD_SCALE,
nullable: row.NULL_FLAG !== 1, nullable: row.NULL_FLAG !== 1,
})); }));
} }
async function tableExists(config, tableName) { async function firebirdTableExists(config, tableName) {
const r = await query(config, const r = await firebirdQuery(config,
"SELECT COUNT(*) AS CT FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = ?", "SELECT COUNT(*) AS CT FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = ?",
[String(tableName).toUpperCase()]); [String(tableName).toUpperCase()]);
return (r[0] && r[0].CT > 0) || false; return (r[0] && r[0].CT > 0) || false;
} }
async function columnExists(config, tableName, columnName) { async function firebirdColumnExists(config, tableName, columnName) {
const r = await query(config, const r = await firebirdQuery(config,
"SELECT COUNT(*) AS CT FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = ? AND RDB$FIELD_NAME = ?", "SELECT COUNT(*) AS CT FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = ? AND RDB$FIELD_NAME = ?",
[String(tableName).toUpperCase(), String(columnName).toUpperCase()]); [String(tableName).toUpperCase(), String(columnName).toUpperCase()]);
return (r[0] && r[0].CT > 0) || false; return (r[0] && r[0].CT > 0) || false;
} }
async function close() { /* Firebird abre conexão por consulta; nada a encerrar */ } async function firebirdClose() { /* Firebird abre conexão por consulta; nada a encerrar */ }
return { query, execute, testConnection, listTables, tableInfo, tableExists, columnExists, close };
})();
// ============================================================ // ============================================================
// 3) DRIVER POSTGRES (pool + tradutor de SQL Firebird->PG + schema) // 3) DISPATCHER — API pública
// ============================================================ // ============================================================
const postgresDriver = (() => { const driver = {
const pg = require('pg'); query: firebirdQuery,
execute: firebirdExecute,
testConnection: firebirdTestConnection,
listTables: firebirdListTables,
tableInfo: firebirdTableInfo,
tableExists: firebirdTableExists,
columnExists: firebirdColumnExists,
close: firebirdClose,
};
// COUNT()/bigint chegam como string no pg; o código histórico usa números. function getConn(alias) {
pg.types.setTypeParser(20, (v) => (v === null ? null : parseInt(v, 10))); // int8 / bigint
pg.types.setTypeParser(1700, (v) => (v === null ? null : parseFloat(v))); // numeric / decimal
const pools = new Map();
function safeSchema(schema) {
const s = String(schema || 'public').trim();
return /^[A-Za-z_][A-Za-z0-9_$]*$/.test(s) ? s : 'public';
}
function poolKey(config) {
return [config.host, config.port, config.database, config.user, safeSchema(config.schema)].join('|');
}
// Schemas que o pool enxerga (configurado + public como fallback)
function schemasFor(config) {
const schema = safeSchema(config.schema);
return schema === 'public' ? ['public'] : [schema, 'public'];
}
function getEntry(config) {
const key = poolKey(config);
let entry = pools.get(key);
if (!entry) {
const schemas = schemasFor(config);
const pool = new pg.Pool({
host: config.host,
port: config.port,
database: config.database,
user: config.user,
password: config.password,
ssl: config.ssl ? { rejectUnauthorized: false } : false,
options: '-c search_path=' + schemas.join(','),
max: config.max || 10,
idleTimeoutMillis: config.idleTimeoutMillis || 30000,
connectionTimeoutMillis: config.connectionTimeoutMillis || 10000,
});
pool.on('error', (err) => console.error('[postgres] Erro inesperado no pool:', err.message));
entry = { pool, schemas, tableSet: null, loadingTables: null };
pools.set(key, entry);
}
return entry;
}
// Carrega (uma vez por pool) os nomes de tabela MAIÚSCULOS p/ decidir aspas
async function ensureTableSet(entry) {
if (entry.tableSet) return entry.tableSet;
if (!entry.loadingTables) {
entry.loadingTables = entry.pool
.query('SELECT table_name FROM information_schema.tables WHERE table_schema = ANY($1)', [entry.schemas])
.then((res) => {
entry.tableSet = new Set(res.rows.map((r) => String(r.table_name).toUpperCase()));
return entry.tableSet;
})
.catch((err) => {
console.error('[postgres] Falha ao carregar nomes de tabela:', err.message);
entry.tableSet = new Set();
return entry.tableSet;
});
}
return entry.loadingTables;
}
// ? -> $1, $2, ... (ignora literais de string)
function convertPlaceholders(sql) {
let out = '', i = 0, n = 1, inStr = false;
while (i < sql.length) {
const ch = sql[i];
if (inStr) {
out += ch;
if (ch === "'") {
if (sql[i + 1] === "'") { out += sql[i + 1]; i += 2; continue; }
inStr = false;
}
i++;
continue;
}
if (ch === "'") { inStr = true; out += ch; i++; continue; }
if (ch === '?') { out += '$' + (n++); i++; continue; }
out += ch; i++;
}
return out;
}
// Aspas nos nomes de tabela conhecidos (MAIÚSCULOS) após FROM/JOIN/INTO/UPDATE/ALTER/DROP
function quoteTableNames(sql, tableSet) {
if (!tableSet || tableSet.size === 0) return sql;
return sql.replace(
/(\b(?:FROM|JOIN|INTO|UPDATE|ALTER\s+TABLE|DROP\s+TABLE)\s+)("?)([A-Za-z_][A-Za-z0-9_$]*)("?)/gi,
(match, kw, q1, name, q2) => {
if (q1 === '"' || q2 === '"') return match;
if (tableSet.has(name.toUpperCase())) return kw + '"' + name.toUpperCase() + '"';
return match;
}
);
}
// Rede de segurança: FIRST n [SKIP m] no topo -> LIMIT/OFFSET (SQL legado)
function translateFirstSkip(sql) {
return sql.replace(
/^(\s*SELECT\s+)FIRST\s+(\d+)(?:\s+SKIP\s+(\d+))?\s+/i,
(m, sel, first, skip) => sel + ' LIMITTAIL LIMIT ' + first + (skip ? ' OFFSET ' + skip : '') + ' '
);
}
function applyLimitTail(sql) {
const marker = / LIMITTAIL( LIMIT \d+(?: OFFSET \d+)?) /;
const m = sql.match(marker);
if (!m) return sql;
return sql.replace(marker, ' ').trimEnd() + m[1];
}
// Coloca aspas em nomes de colunas MAIÚSCULOS (formato NOME_COLUNA)
// Reconhece padrao: letras_maiusculas com _ (ex: USU_CODIGO_ID, CON_NUMERO)
// Ignora SQL keywords, literais string e numeros
function quoteColumnNames(sql) {
// Lista de keywords SQL que nao devem ser colocadas em aspas
var sqlKeywords = ['AS', 'ON', 'AND', 'OR', 'NOT', 'IS', 'IN', 'NULL', 'TRUE', 'FALSE',
'ASC', 'DESC', 'DISTINCT', 'ALL', 'ANY', 'SOME', 'BETWEEN', 'LIKE', 'ILIKE',
'FIRST', 'SKIP', 'LIMIT', 'OFFSET', 'ORDER', 'GROUP', 'HAVING', 'CROSS', 'INNER',
'LEFT', 'RIGHT', 'FULL', 'OUTER', 'NATURAL', 'USING', 'EXCEPT', 'INTERSECT', 'UNION',
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'COALESCE', 'CAST', 'CASE', 'WHEN', 'THEN', 'ELSE',
'END', 'EXISTS', 'CURRENT_TIMESTAMP', 'CURRENT_DATE', 'CURRENT_TIME', 'NOW', 'UPPER', 'LOWER', 'TRIM',
'ROW', 'ROWS', 'RANGE', 'FETCH', 'NEXT', 'ONLY', 'PERCENT', 'WITH', 'TIES', 'OVER', 'PARTITION',
'RETURNING', 'ACTION', 'ADD', 'ALTER', 'COLUMN', 'CONSTRAINT', 'CREATE', 'DELETE',
'DROP', 'EXECUTE', 'INSERT', 'INTO', 'TABLE', 'TRUNCATE', 'UPDATE', 'VALUES', 'WHERE',
'SET', 'SELECT', 'FROM', 'JOIN', 'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'UNIQUE',
'CHECK', 'DEFAULT', 'INDEX', 'VIEW', 'MATERIALIZED', 'SCHEMA', 'DATABASE', 'GRANT',
'REVOKE', 'ABSOLUTE', 'ACTION', 'ADMIN', 'AFTER', 'AGGREGATE', 'ALSO', 'ANALYSE',
'ANALYZE', 'BEFORE', 'BEGIN', 'BINARY', 'BOTH', 'CALLED', 'CASCADE', 'CASCADED', 'CATALOG',
'CHAIN', 'CHARACTER', 'CHECKPOINT', 'CLASS', 'CLOSE', 'CLUSTER', 'COMMENT', 'COMMIT',
'COMMITTED', 'CONCURRENTLY', 'CONFIGURATION', 'CONFLICT', 'CONNECTION', 'CONSTRAINTS',
'CONTENT', 'CONTINUE', 'COPY', 'COSTS', 'CURRENT', 'CURSOR', 'CYCLE', 'DATA', 'DATABASE',
'DAY', 'DEALLOCATE', 'DEC', 'DECLARE', 'DEFERRABLE', 'DEFERRED', 'DEFINER', 'DELETE',
'DELIMITER', 'DELIMITERS', 'DEPENDS', 'DESCRIBE', 'DICTIONARY', 'DISABLE', 'DISCARD',
'DOMAIN', 'DOUBLE', 'DROP', 'EACH', 'ENABLE', 'ENCODING', 'ENCRYPTED', 'ENUM', 'ESCAPE',
'EVENT', 'EXCHANGE', 'EXCLUDE', 'EXCLUDING', 'EXCLUSIVE', 'EXECUTE', 'EXPLAIN',
'EXTENSION', 'EXTERNAL', 'FAMILY', 'FILTER', 'FIRST', 'FLOOR', 'FOLLOWING', 'FORCE',
'FORMAT', 'FORWARD', 'FREEZE', 'FUNCTION', 'FUNCTIONS', 'GLOBAL', 'GRANTED', 'GREATEST',
'GROUPING', 'GROUPS', 'HANDLER', 'HEADER', 'HOLD', 'HOUR', 'IDENTITY', 'IF', 'IMMEDIATE',
'IMMUTABLE', 'IMPLICIT', 'IMPORT', 'INCLUDING', 'INCREMENT', 'INDENT', 'INDEXES',
'INHERIT', 'INHERITS', 'INLINE', 'INPUT', 'INSENSITIVE', 'INSERTION', 'INSTEAD',
'INVOKER', 'ISOLATION', 'KEY', 'LABEL', 'LANGUAGE', 'LARGE', 'LAST', 'LEADING', 'LEAKPROOF',
'LEAST', 'LEVEL', 'LISTEN', 'LOAD', 'LOCAL', 'LOCATION', 'LOCK', 'LOCKED', 'LOGGED',
'MAPPING', 'MATCH', 'MATCHED', 'MAXVALUE', 'METHOD', 'MINUTE', 'MINVALUE', 'MODE', 'MONTH',
'MOVE', 'NAMES', 'NESTED', 'NEW', 'NEXT', 'NO', 'NONE', 'NOTHING', 'NOTIFY', 'NOWAIT',
'NULLS', 'OBJECT', 'OF', 'OFF', 'OIDS', 'OLD', 'OPERATOR', 'OPTION', 'OPTIONS', 'ORDERED',
'ORDINALITY', 'OTHERS', 'OUT', 'OVERLAY', 'OVERRIDING', 'OWNED', 'OWNER', 'PARALLEL',
'PARSER', 'PARTITION', 'PASSING', 'PASSWORD', 'PLACING', 'PLANS', 'POLICY', 'POSITION',
'PRECEDING', 'PRECISION', 'PREPARE', 'PREPARED', 'PRESERVE', 'PRIOR', 'PRIVILEGES',
'PROCEDURAL', 'PROCEDURE', 'PROCEDURES', 'PROGRAM', 'PUBLICATION', 'QUOTE', 'RANGE',
'READ', 'REASSIGN', 'RECHECK', 'RECURSIVE', 'REFERENCING', 'REFRESH', 'REINDEX',
'RELATIVE', 'RELEASE', 'RENAME', 'REPEATABLE', 'REPLACE', 'REPLICA', 'RESET',
'RESTART', 'RESTRICT', 'RETURN', 'RETURNS', 'REVOKE', 'ROLE', 'ROLLBACK', 'ROLLUP',
'ROUTINE', 'ROUTINES', 'RULE', 'SAVEPOINT', 'SCROLL', 'SEARCH', 'SECOND',
'SECURITY', 'SEQUENCE', 'SEQUENCES', 'SERIALIZABLE', 'SERVER', 'SESSION', 'SHARE',
'SHOW', 'SHUTDOWN', 'SIMPLE', 'SKIP', 'SNAPSHOT', 'SOME', 'SOURCE', 'SPACE', 'SPECIFIC',
'SPECIFICTYPE', 'SQL', 'SQLCODE', 'SQLERROR', 'SQLEXCEPTION', 'SQLSTATE', 'SQLWARNING',
'STABLE', 'STANDALONE', 'START', 'STATE', 'STATEMENT', 'STATIC', 'STATISTICS', 'STDIN',
'STDOUT', 'STORAGE', 'STRICT', 'STRIP', 'SUBSCRIPTION', 'SYSID', 'SYSTEM', 'TABLES',
'TABLESAMPLE', 'TABLESPACE', 'TEMP', 'TEMPLATE', 'TEMPORARY', 'TEXT', 'TEXT_SEARCH',
'THEN', 'TIME', 'TRANSACTION', 'TRIGGER', 'TRUNCATE', 'TRUSTED', 'TYPE', 'TYPES',
'UNBOUNDED', 'UNCOMMITTED', 'UNDER', 'UNENCRYPTED', 'UNKNOWN', 'UNLISTEN', 'UNLOGGED',
'UNTIL', 'UPDATE', 'USE', 'USER', 'USERS', 'VACUUM', 'VALID', 'VALIDATE', 'VALIDATOR',
'VALUE', 'VARIADIC', 'VARYING', 'VERBOSE', 'VERSION', 'VIEW', 'VIEWS', 'VOLATILE',
'WHITESPACE', 'WITHIN', 'WITHOUT', 'WORK', 'WRAPPER', 'WRITE', 'XML', 'XMLATTRIBUTES',
'XMLCONCAT', 'XMLELEMENT', 'XMLEXISTS', 'XMLFOREST', 'XMLNAMESPACES', 'XMLPARSE',
'XMLPI', 'XMLROOT', 'XMLSERIALIZE', 'XMLTABLE', 'YEAR', 'YES', 'ZONE'];
var kwSet = new Set(sqlKeywords.map(function(k) { return k.toUpperCase(); }));
return sql.replace(/("?)([A-Z][A-Z_0-9]{2,})("?)(?=[\s,)]|$)/g, function(match, q1, name, q2) {
if (q1 === '"' || q2 === '"') return match; // ja tem aspas
if (kwSet.has(name)) return match; // SQL keyword
if (/^\d/.test(name)) return match;
return '"' + name + '"';
});
}
function translateSql(sql, tableSet) {
let out = sql;
out = out.replace(/\bCONTAINING\s+(\?|\$\d+|'(?:[^']|'')*')/gi, "ILIKE ('%' || $1 || '%')");
out = out.replace(/\bFROM\s+RDB\$DATABASE\b/gi, '');
out = quoteTableNames(out, tableSet);
out = quoteColumnNames(out);
out = translateFirstSkip(out);
out = applyLimitTail(out);
out = convertPlaceholders(out);
return out;
}
function upperKeys(rows) {
return rows.map((row) => {
const o = {};
for (const k in row) o[k.toUpperCase()] = row[k];
return o;
});
}
async function query(config, sql, params = []) {
const entry = getEntry(config);
await ensureTableSet(entry);
const text = translateSql(sql, entry.tableSet);
try {
const res = await entry.pool.query(text, params);
return upperKeys(res.rows);
} catch (err) {
throw new Error(`Erro na consulta (postgres): ${err.message}`);
}
}
async function execute(config, sql, params = []) {
const entry = getEntry(config);
await ensureTableSet(entry);
const text = translateSql(sql, entry.tableSet);
try {
const res = await entry.pool.query(text, params);
return { affectedRows: res.rowCount || 0, result: upperKeys(res.rows || []) };
} catch (err) {
throw new Error(`Erro na execução (postgres): ${err.message}`);
}
}
async function testConnection(config) {
const entry = getEntry(config);
await entry.pool.query('SELECT 1');
return true;
}
async function listTables(config) {
const entry = getEntry(config);
const res = await entry.pool.query(
`SELECT table_name FROM information_schema.tables
WHERE table_schema = $1 AND table_type = 'BASE TABLE'
ORDER BY table_name`,
[safeSchema(config.schema)]
);
return res.rows.map((r) => r.table_name);
}
async function tableInfo(config, tableName) {
const entry = getEntry(config);
// Resolve no primeiro schema do search_path que contém a tabela
const res = await entry.pool.query(
`SELECT column_name, ordinal_position, data_type,
character_maximum_length, numeric_precision, numeric_scale, is_nullable
FROM information_schema.columns
WHERE UPPER(table_name) = UPPER($2)
AND table_schema = (
SELECT table_schema FROM information_schema.tables
WHERE UPPER(table_name) = UPPER($2) AND table_schema = ANY($1)
ORDER BY array_position($1, table_schema) LIMIT 1
)
ORDER BY ordinal_position`,
[entry.schemas, tableName]
);
return res.rows.map((row) => ({
name: row.column_name,
position: row.ordinal_position,
type: (row.data_type || '').toUpperCase(),
length: row.character_maximum_length,
precision: row.numeric_precision,
scale: row.numeric_scale,
nullable: row.is_nullable === 'YES',
}));
}
async function tableExists(config, tableName) {
const entry = getEntry(config);
await ensureTableSet(entry);
if (entry.tableSet) return entry.tableSet.has(String(tableName).toUpperCase());
const res = await entry.pool.query(
`SELECT 1 FROM information_schema.tables
WHERE table_schema = ANY($1) AND UPPER(table_name) = UPPER($2) LIMIT 1`,
[entry.schemas, tableName]
);
return res.rowCount > 0;
}
async function columnExists(config, tableName, columnName) {
const entry = getEntry(config);
const res = await entry.pool.query(
`SELECT 1 FROM information_schema.columns
WHERE table_schema = ANY($1) AND UPPER(table_name) = UPPER($2)
AND UPPER(column_name) = UPPER($3) LIMIT 1`,
[entry.schemas, tableName, columnName]
);
return res.rowCount > 0;
}
async function close() {
const all = Array.from(pools.values()).map((e) => e.pool.end().catch(() => {}));
pools.clear();
await Promise.all(all);
}
return {
query, execute, testConnection, listTables, tableInfo, tableExists, columnExists, close,
_translateSql: translateSql, // exportado para testes
};
})();
// ============================================================
// 4) DISPATCHER — API pública
// ============================================================
const drivers = { postgres: postgresDriver, firebird: firebirdDriver };
function getDriver(alias) {
const config = getConfig(alias); const config = getConfig(alias);
const driver = drivers[config.driver];
if (!driver) {
throw new Error(`Driver "${config.driver}" não suportado para o alias "${alias}".`);
}
return { config, driver }; return { config, driver };
} }
function query(alias, sql, params = []) { function query(alias, sql, params = []) {
let d; let d;
try { d = getDriver(alias); } catch (err) { return Promise.reject(err); } try { d = getConn(alias); } catch (err) { return Promise.reject(err); }
return d.driver.query(d.config, sql, params).catch((err) => { throw new Error(`[${alias}] ${err.message}`); }); return d.driver.query(d.config, sql, params).catch((err) => { throw new Error(`[${alias}] ${err.message}`); });
} }
function execute(alias, sql, params = []) { function execute(alias, sql, params = []) {
let d; let d;
try { d = getDriver(alias); } catch (err) { return Promise.reject(err); } try { d = getConn(alias); } catch (err) { return Promise.reject(err); }
return d.driver.execute(d.config, sql, params).catch((err) => { throw new Error(`[${alias}] ${err.message}`); }); return d.driver.execute(d.config, sql, params).catch((err) => { throw new Error(`[${alias}] ${err.message}`); });
} }
async function testConnection(alias) { async function testConnection(alias) {
try { try {
const { config, driver } = getDriver(alias); const { config } = getConn(alias);
await driver.testConnection(config); await driver.testConnection(config);
return true; return true;
} catch (err) { } catch (err) {
@@ -697,33 +342,33 @@ async function testAllConnections() {
} }
function listTables(alias) { function listTables(alias) {
const { config, driver } = getDriver(alias); const { config } = getConn(alias);
return driver.listTables(config); return driver.listTables(config);
} }
function tableInfo(alias, tableName) { function tableInfo(alias, tableName) {
const { config, driver } = getDriver(alias); const { config } = getConn(alias);
return driver.tableInfo(config, tableName); return driver.tableInfo(config, tableName);
} }
function tableExists(alias, tableName) { function tableExists(alias, tableName) {
const { config, driver } = getDriver(alias); const { config } = getConn(alias);
return driver.tableExists(config, tableName); return driver.tableExists(config, tableName);
} }
function columnExists(alias, tableName, columnName) { function columnExists(alias, tableName, columnName) {
const { config, driver } = getDriver(alias); const { config } = getConn(alias);
return driver.columnExists(config, tableName, columnName); return driver.columnExists(config, tableName, columnName);
} }
/** Nome do driver de um alias (ex.: 'postgres'). */ /** Nome do driver de um alias (sempre 'firebird'). */
function driverOf(alias) { function driverOf(alias) {
return getConfig(alias).driver; return 'firebird';
} }
/** Encerra os pools de todos os drivers (shutdown gracioso). */ /** Encerra recursos (noop para Firebird). */
async function closeAll() { async function closeAll() {
await Promise.all(Object.values(drivers).map((d) => d.close && d.close())); await driver.close();
} }
module.exports = { module.exports = {
+18 -1
View File
@@ -50,6 +50,23 @@ async function authenticateToken(req, res, next) {
.map(function (e) { return Number(e); }) .map(function (e) { return Number(e); })
.filter(function (n) { return !isNaN(n); }); .filter(function (n) { return !isNaN(n); });
} }
// Se o JWT não tem o array empresas (tokens legados), tenta carregar do BD
if (!Array.isArray(decoded.empresas) || decoded.empresas.length === 0) {
const alias = req.params.alias;
if (alias && decoded.id) {
try {
const empresas = await db.query(alias,
'SELECT USE_EMPRESA_ID FROM USUARIOS_EMPRESA WHERE USE_USUARIO_ID = ?',
[decoded.id]
);
decoded.empresas = empresas.map(e => Number(e.USE_EMPRESA_ID)).filter(n => !isNaN(n));
} catch (e) {
decoded.empresas = [];
}
} else {
decoded.empresas = [];
}
}
req.user = decoded; req.user = decoded;
req.authType = 'jwt'; req.authType = 'jwt';
return next(); return next();
@@ -90,7 +107,7 @@ async function authenticateToken(req, res, next) {
FROM USUARIOS FROM USUARIOS
WHERE USU_TOKEN = ? WHERE USU_TOKEN = ?
AND USU_STATUS = 'A' AND USU_STATUS = 'A'
AND COALESCE(USU_ACESSO_WEB, 0) = 1`, AND COALESCE(USU_ACESSO_WEB, '0') IN ('1', 'S')`,
[token] [token]
); );
+1 -1
View File
@@ -1,6 +1,6 @@
/** /**
* Estrutura do "Fluxo de Resolução" (motivos + resolução do atendimento). * Estrutura do "Fluxo de Resolução" (motivos + resolução do atendimento).
* Cria de forma idempotente, em Postgres ou Firebird: * Cria de forma idempotente no Firebird:
* - Tabela CHATC2_MOTIVOS_ATENDIMENTO * - Tabela CHATC2_MOTIVOS_ATENDIMENTO
* - Flags em CHATC2_CONFIGURACOES_EMPRESA (CFE_MOTIVO_*, CFE_RESOLUCAO_*) * - Flags em CHATC2_CONFIGURACOES_EMPRESA (CFE_MOTIVO_*, CFE_RESOLUCAO_*)
* - Colunas CON_MOTIVO_ID / CON_RESOLUCAO em CHATC2_CONVERSAS * - Colunas CON_MOTIVO_ID / CON_RESOLUCAO em CHATC2_CONVERSAS