diff --git a/.env.example b/.env.example index 304359f..42412e0 100644 --- a/.env.example +++ b/.env.example @@ -2,23 +2,11 @@ # Configuracao do Chatc2 # ============================================================ -# Driver padrao para novas conexoes (postgres | firebird) -DB_DRIVER=postgres +# Driver padrao (firebird) +DB_DRIVER=firebird # ------------------------------------------------------------ -# Banco principal: PostgreSQL (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) +# Banco Firebird (alias "novo_local") # ------------------------------------------------------------ DB_HOST=localhost DB_PORT=3050 diff --git a/deploy-linux/install.sh b/deploy-linux/install.sh index a48b385..fa00427 100644 --- a/deploy-linux/install.sh +++ b/deploy-linux/install.sh @@ -20,14 +20,13 @@ SERVER_DOMAIN="${SERVER_DOMAIN:-}" # Opcional: seu-dominio.com.br JWT_SECRET="" 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. -PG_HOST="${PG_HOST:-}" -PG_PORT="${PG_PORT:-5432}" -PG_USER="${PG_USER:-postgres}" -PG_PASSWORD="${PG_PASSWORD:-}" -PG_DATABASE="${PG_DATABASE:-postgres}" -PG_SCHEMA="${PG_SCHEMA:-public}" +DB_HOST="${DB_HOST:-localhost}" +DB_PORT="${DB_PORT:-3050}" +DB_USER="${DB_USER:-SYSDBA}" +DB_PASSWORD="${DB_PASSWORD:-masterkey}" +DB_DATABASE="${DB_DATABASE:-}" # Cores 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 -DB_DRIVER=postgres +DB_DRIVER=firebird # ------------------------------------------------------------ -# PostgreSQL (EXTERNO) — banco principal "novo_local" +# Firebird (alias "novo_local") # ------------------------------------------------------------ -PG_HOST=${PG_HOST} -PG_PORT=${PG_PORT} -PG_USER=${PG_USER} -PG_PASSWORD=${PG_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_HOST=${DB_HOST} +DB_PORT=${DB_PORT} +DB_USER=${DB_USER} +DB_PASSWORD=${DB_PASSWORD} DB_ENCODING=UTF-8 # Caminho do .FDB (vazio = usa ../NOVO.FDB na raiz do projeto) -DB_DATABASE= +DB_DATABASE=${DB_DATABASE} # Servidor PORT=3000 @@ -289,12 +278,9 @@ EXTERNAL_URL=${EXTERNAL_URL} EOF chown "$CHATC2_USER:$CHATC2_USER" "$CHATC2_DIR/.env" 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 " 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 @@ -314,23 +300,22 @@ else warn "Apos copiar, execute: chown $CHATC2_USER:firebird $CHATC2_DIR/db/*.FDB && chmod 660 $CHATC2_DIR/db/*.FDB" fi -info "Passo 9/12: Migracoes (Firebird legado)..." -# O schema do PostgreSQL e gerenciado no banco EXTERNO — nao migramos aqui. -# Migracoes Firebird so rodam se houver um .FDB local (alias firebird_local). +info "Passo 9/12: Migracoes (Firebird)..." +# Migracoes Firebird rodam se houver um .FDB local (alias novo_local). if ls "$CHATC2_DIR/db/"*.FDB "$CHATC2_DIR/"*.FDB >/dev/null 2>&1; 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" fi else - info "Sem .FDB local — pulando migracoes Firebird (schema do Postgres e externo)." + info "Sem .FDB local — pulando migracoes Firebird." fi # ████████████████████████████████████████████████████ # PASSO 9b: HABILITAR USUARIOS ADMIN PARA 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' const db = require('DATABASE_PATH'); @@ -352,16 +337,11 @@ const db = require('DATABASE_PATH'); SCRIPTJS 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 - # 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 - log "Usuarios admin verificados (Postgres + Firebird se presente)" + log "Usuarios admin verificados (Firebird)" # ████████████████████████████████████████████████████ # PASSO 10: NGINX diff --git a/package-lock.json b/package-lock.json index 083f0a8..076df9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "chatc2", "version": "2.0.0", + "hasInstallScript": true, "license": "ISC", "dependencies": { "cors": "^2.8.6", @@ -14,8 +15,7 @@ "express": "^5.2.1", "ffmpeg-static": "^5.3.0", "jsonwebtoken": "^9.0.3", - "node-firebird": "^2.3.1", - "pg": "^8.21.0" + "node-firebird": "^2.3.1" } }, "node_modules/@derhuerst/http-basic": { @@ -832,134 +832,6 @@ "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": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -983,9 +855,9 @@ } }, "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -1212,15 +1084,6 @@ "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": { "version": "2.0.2", "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", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "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" - } } } } diff --git a/package.json b/package.json index 0309484..8effdd7 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "chatc2", "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", "scripts": { "start": "node src/server.js", - "dev": "node --watch src/server.js" + "dev": "node --watch src/server.js", + "postinstall": "node scripts/postinstall.js" }, "keywords": [ - "postgres", "firebird", "api", "express" @@ -22,7 +22,6 @@ "express": "^5.2.1", "ffmpeg-static": "^5.3.0", "jsonwebtoken": "^9.0.3", - "node-firebird": "^2.3.1", - "pg": "^8.21.0" + "node-firebird": "^2.3.1" } } diff --git a/scripts/corrigir-situacao.js b/scripts/corrigir-situacao.js new file mode 100644 index 0000000..dda15f9 --- /dev/null +++ b/scripts/corrigir-situacao.js @@ -0,0 +1,74 @@ +/** + * Script para corrigir registros existentes com SITUACAO = NULL + * + * Uso: node scripts/corrigir-situacao.js [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); +}); diff --git a/scripts/migracoes.js b/scripts/migracoes.js index a18988f..743e317 100644 --- a/scripts/migracoes.js +++ b/scripts/migracoes.js @@ -22,9 +22,8 @@ const db = require('../src/database'); // Este script aplica DDL no dialeto Firebird (BLOB SUB_TYPE, etc.). -// O schema do PostgreSQL é gerenciado externamente (banco externo) — use este -// script apenas para bancos Firebird. Alias padrão: firebird_local. -const alias = process.argv[2] || 'firebird_local'; +// Alias padrão: novo_local. +const alias = process.argv[2] || 'novo_local'; // ============================================================ // CONTROLE DE MIGRAÇÕES @@ -428,8 +427,7 @@ async function main() { catch (e) { console.error('❌', e.message); process.exit(1); } if (driver !== '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 firebird_local\n'); + console.log(' Para migrar um banco Firebird: node scripts/migracoes.js novo_local\n'); process.exit(0); } diff --git a/scripts/postinstall.js b/scripts/postinstall.js new file mode 100644 index 0000000..ea2c805 --- /dev/null +++ b/scripts/postinstall.js @@ -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'); +} diff --git a/src/app.js b/src/app.js index 5c757ee..943bfea 100644 --- a/src/app.js +++ b/src/app.js @@ -62,9 +62,9 @@ app.use(routes); // Rota raiz app.get('/', (req, res) => { res.json({ - name: 'API Firebird - Chatc2', + name: 'API Chatc2 - Firebird', version: '2.0.0', - description: 'API multi-banco com sistema de aliases', + description: 'API Firebird com sistema de aliases', auth: { loginPage: 'GET /app/:alias/login', login: 'POST /app/:alias/login', diff --git a/src/controllers/authController.js b/src/controllers/authController.js index cbcfebb..8b6a48f 100644 --- a/src/controllers/authController.js +++ b/src/controllers/authController.js @@ -75,7 +75,7 @@ class AuthController { FROM USUARIOS WHERE USU_LOGIN = ? 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' `; diff --git a/src/controllers/chatController.js b/src/controllers/chatController.js index 2350958..bb3cb29 100644 --- a/src/controllers/chatController.js +++ b/src/controllers/chatController.js @@ -148,7 +148,7 @@ class ChatController { const statusFilter = req.query.status || 'A,E'; 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.' }); // Status via parâmetros (evita SQL injection). Aceita apenas A/E/F. @@ -260,7 +260,7 @@ class ChatController { var minhasEquipes = await db.query(alias, 'SELECT EQU_EQUIPE_ID FROM CHATC2_USU_EQUIPES WHERE EQU_USUARIO_ID = ?', [userId]); 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, `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]); @@ -296,7 +296,7 @@ class ChatController { const row = result[0]; 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.' }); // Verifica se o número pertence a um dependente @@ -471,7 +471,7 @@ class ChatController { if (r.length === 0) return { status: 404 }; const emp = r[0].CON_EMPRESA_ID; 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 }; } @@ -542,7 +542,7 @@ class ChatController { 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[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.' }); // Se a conversa estiver finalizada, reabre @@ -620,8 +620,8 @@ class ChatController { // Insere mensagem no banco 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) - VALUES (?, ?, 'U', ?, ?, ?, ?, CURRENT_TIMESTAMP, ?) + 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', ?, ?, ?, ?, 'A', CURRENT_TIMESTAMP, ?) `, [newId, id, usuarioId, textoFinal, tipo || 'text', privada === 'S' ? 'S' : 'N', midiaId]); // 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.' }); 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.' }); // ===== Fluxo de Resolução: valida motivo/resolução conforme config ===== @@ -781,7 +781,7 @@ class ChatController { // Verifica permissão 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.' }); } @@ -827,8 +827,8 @@ class ChatController { const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); const msgId = (maxMsgId[0]?.ID || 0) + 1; 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) - VALUES (?, ?, 'U', ?, ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [msgId, conversaId, req.user?.id, mensagem]); await db.execute(alias, @@ -860,8 +860,8 @@ class ChatController { // Insere conversa await db.execute(alias, ` 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) - VALUES (?, ?, ?, ?, ?, ?, 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_SITUACAO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) + VALUES (?, ?, ?, ?, ?, ?, 'A', 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `, [newId, empresaId, instId, numeroLimpo, nomeContato || numeroLimpo, clienteId || null]); // Insere primeira mensagem @@ -869,8 +869,8 @@ class ChatController { const msgId = (maxMsgId[0]?.ID || 0) + 1; 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) - VALUES (?, ?, 'U', ?, ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [msgId, newId, req.user?.id, mensagem]); // Envia via Evolution API @@ -1058,7 +1058,7 @@ class ChatController { mimeType = mimeType.split(';')[0].trim(); 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; if (rawBuffer == null) { return respond(404, { success: false, error: 'Mídia sem conteúdo.' }); @@ -1179,7 +1179,7 @@ class ChatController { const cli = await db.query(alias, 'SELECT CLI_EMPRESA_ID FROM CLIENTES WHERE CLI_CODIGO_ID = ?', [cid]); 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 { 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 newId = (maxId[0]?.ID || 0) + 1; 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) - VALUES (?, ?, 'U', ?, ?, 'text', 'N', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [newId, id, req.user?.id, texto]); await db.execute(alias, 'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?', [id]); diff --git a/src/controllers/databaseController.js b/src/controllers/databaseController.js index a6ad16b..e26f201 100644 --- a/src/controllers/databaseController.js +++ b/src/controllers/databaseController.js @@ -89,59 +89,29 @@ class DatabaseController { 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 config = { - host: req.body.host || 'localhost', - port: req.body.port || 3050, - database: req.body.database, - user: req.body.user || 'SYSDBA', - password: req.body.password || 'masterkey', - encoding: 'UTF-8', - lowercase_keys: false, - pageSize: 4096, - }; - await new Promise(function (resolve, reject) { - Firebird.attach(config, function (err, conn) { - if (err) return reject(new Error('Falha ao conectar: ' + err.message)); - conn.query('SELECT 1 AS TEST FROM RDB$DATABASE', [], function (qErr, rows) { - conn.detach(); - if (qErr) return reject(new Error('Falha na consulta: ' + qErr.message)); - resolve(rows); - }); + var Firebird = require('node-firebird'); + var config = { + host: req.body.host || 'localhost', + port: req.body.port || 3050, + database: req.body.database, + user: req.body.user || 'SYSDBA', + password: req.body.password || 'masterkey', + encoding: 'UTF-8', + lowercase_keys: false, + pageSize: 4096, + }; + await new Promise(function (resolve, reject) { + Firebird.attach(config, function (err, conn) { + if (err) return reject(new Error('Falha ao conectar: ' + err.message)); + conn.query('SELECT 1 AS TEST FROM RDB$DATABASE', [], function (qErr, rows) { + conn.detach(); + if (qErr) return reject(new Error('Falha na consulta: ' + qErr.message)); + resolve(rows); }); }); - } + }); - res.json({ success: true, message: '✅ Conexão (' + driver + ') estabelecida com sucesso!' }); + res.json({ success: true, message: '✅ Conexão (firebird) estabelecida com sucesso!' }); } catch (err) { res.status(500).json({ success: false, error: err.message }); } diff --git a/src/controllers/empresaController.js b/src/controllers/empresaController.js index 77faf79..ce11ccf 100644 --- a/src/controllers/empresaController.js +++ b/src/controllers/empresaController.js @@ -38,7 +38,7 @@ class EmpresaController { } 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; let base64; if (Buffer.isBuffer(blobData)) { diff --git a/src/controllers/evolutionController.js b/src/controllers/evolutionController.js index 7b71983..e10eaab 100644 --- a/src/controllers/evolutionController.js +++ b/src/controllers/evolutionController.js @@ -242,18 +242,18 @@ class EvolutionController { }); instanceCreated = true; 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) - VALUES (?, ?, ?, ?, ?, ?, 'C') + 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', 'A') `, [newId, INS_EMPRESA_ID || userEmpresas[0], INS_NOME, INS_URL, INS_API_KEY, INS_INSTANCE_NAME]); break; } catch (e) { /* Tenta próximo endpoint */ } } if (!instanceCreated) { - // Salva mesmo sem conseguir criar na Evolution + // Salva mesmo sem conseguir criar na Evolution (status 'D' = desconectado) 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) - VALUES (?, ?, ?, ?, ?, ?, 'D') + 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', 'A') `, [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) { try { 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, '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, ` 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) - VALUES (?, ?, ?, ?, ?, ?, 'E', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + CON_NOME_CONTATO, CON_CLIENTE_ID, CON_STATUS, CON_SITUACAO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) + VALUES (?, ?, ?, ?, ?, ?, 'E', 'A', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `, [newId, empresaId, instanciaId, numero, nomeFinal, clienteId]); 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) { const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(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]); @@ -1205,13 +1218,13 @@ async function processWebhook(alias, body) { if (midiaId) { 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) - VALUES (?, ?, 'C', ?, ?, CURRENT_TIMESTAMP, ?) + 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', ?, ?, 'A', CURRENT_TIMESTAMP, ?) `, [(maxMsgId[0]?.ID || 0) + 1, conversa.CON_CODIGO_ID, messageText, messageType, midiaId]); } else { await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'C', ?, ?, CURRENT_TIMESTAMP) + INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_SITUACAO, CME_DT_ENVIO) + VALUES (?, ?, 'C', ?, ?, 'A', CURRENT_TIMESTAMP) `, [(maxMsgId[0]?.ID || 0) + 1, conversa.CON_CODIGO_ID, messageText, messageType]); } diff --git a/src/controllers/routesController.js b/src/controllers/routesController.js index 51c7043..9a6b73a 100644 --- a/src/controllers/routesController.js +++ b/src/controllers/routesController.js @@ -145,14 +145,14 @@ class RoutesController { 'GET /api/databases': { desc: 'Lista as conexões de banco cadastradas (senhas ocultas).' }, 'POST /api/databases': { 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'], - 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).' }, 'POST /api/databases/test': { 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'], }, diff --git a/src/controllers/triageController.js b/src/controllers/triageController.js index b7cde63..a73a2a5 100644 --- a/src/controllers/triageController.js +++ b/src/controllers/triageController.js @@ -72,8 +72,8 @@ class TriageController { // Salva mensagem do sistema const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'triagem', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, menuText]); // 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'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'triagem', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, menuText]); await TriageController.sendEvolution(alias, instanciaId, numero, menuText); @@ -246,8 +246,8 @@ class TriageController { if (msgAfter) { const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msgAfter]); await TriageController.sendEvolution(alias, instanciaId, numero, msgAfter); } @@ -377,8 +377,8 @@ class TriageController { if (textoMsg) { const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, 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'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, msgPrompt]); await TriageController.sendEvolution(alias, instanciaId, numero, msgPrompt); @@ -521,8 +521,8 @@ class TriageController { if (resultado) { const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, 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.'; const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, 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]); @@ -605,8 +605,8 @@ class TriageController { logTriage('Boleto - NENHUM título em aberto'); const maxMsgId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxMsgId[0]?.ID || 0) + 1, conversaId, 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]); @@ -624,8 +624,8 @@ class TriageController { const maxMsgId2 = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxMsgId2[0]?.ID || 0) + 1, conversaId, msgBoleto]); await TriageController.sendEvolution(alias, instanciaId, numero, msgBoleto); @@ -652,8 +652,8 @@ class TriageController { 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'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxId[0]?.ID || 0) + 1, conversaId, msg]); await TriageController.sendEvolution(alias, instanciaId, numero, msg); return; @@ -838,8 +838,8 @@ class TriageController { const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxId[0]?.ID || 0) + 1, conversaId, 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'; const maxIdCont = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxIdCont[0]?.ID || 0) + 1, conversaId, msgCont]); await TriageController.sendEvolution(alias, instanciaId, numero, msgCont); @@ -889,8 +889,8 @@ class TriageController { var msgFinal = 'Atendimento finalizado. Obrigado pelo contato!'; const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxId[0]?.ID || 0) + 1, conversaId, 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]); @@ -898,8 +898,8 @@ class TriageController { 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'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxId[0]?.ID || 0) + 1, conversaId, msgInv]); await TriageController.sendEvolution(alias, instanciaId, numero, msgInv); } @@ -916,8 +916,8 @@ class TriageController { 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'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxId[0]?.ID || 0) + 1, conversaId, msg]); await TriageController.sendEvolution(alias, instanciaId, numero, msg); @@ -949,8 +949,8 @@ class TriageController { if (texto) { const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); await db.execute(alias, ` - INSERT INTO CHATC2_CONVERSAS_MENSAGENS (CME_CODIGO_ID, CME_CONVERSA_ID, CME_REMETENTE, CME_TEXTO, CME_TIPO, CME_DT_ENVIO) - VALUES (?, ?, 'S', ?, 'text', CURRENT_TIMESTAMP) + 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', 'A', CURRENT_TIMESTAMP) `, [(maxId[0]?.ID || 0) + 1, conversaId, texto]); await TriageController.sendEvolution(alias, instanciaId, numero, texto); } diff --git a/src/database.js b/src/database.js index 5c54d7d..c84b601 100644 --- a/src/database.js +++ b/src/database.js @@ -5,13 +5,11 @@ * * 1) CONFIGURAÇÃO — aliases, drivers, conexões estáticas e customizadas * 2) DRIVER FIREBIRD - * 3) DRIVER POSTGRES (pool + tradutor de SQL + schema) - * 4) DISPATCHER — API pública usada por todo o sistema + * 3) DISPATCHER — API pública usada por todo o sistema * - * Suporta múltiplos drivers ('postgres' padrão, 'firebird' legado). Cada - * conexão declara o campo `driver`. Conexões adicionadas em runtime (via API - * /api/databases) são persistidas em databases_custom.json (ao lado deste - * arquivo) — é um arquivo de DADOS, não de código. + * Suporte ao banco Firebird. Cada conexão declara o campo `driver`. + * Conexões adicionadas em runtime (via API /api/databases) são persistidas + * em databases_custom.json (ao lado deste arquivo). */ const path = require('path'); const fs = require('fs'); @@ -31,22 +29,11 @@ try { console.error('[Databases] Erro ao carregar databases_custom.json:', e.message); } -const DRIVERS = ['postgres', 'firebird']; -const DEFAULT_DRIVER = (process.env.DB_DRIVER || 'postgres').toLowerCase(); +const DRIVERS = ['firebird']; +const DEFAULT_DRIVER = 'firebird'; -// Valores padrão por driver +// Valores padrão para Firebird 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: { host: 'localhost', port: 3050, @@ -61,22 +48,10 @@ const DRIVER_DEFAULTS = { /** * Conexões estáticas. - * - `novo_local` → PostgreSQL (conexão principal) - * - `firebird_local`→ Firebird (informe o CAMINHO do arquivo .FDB em `database`) - * Sobrescrevíveis pelo .env (PG_* para Postgres, DB_* para Firebird). + * - `novo_local` → Firebird (informe o CAMINHO do arquivo .FDB em `database`) + * Sobrescrevível pelo .env (DB_*). */ 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: { driver: 'firebird', host: process.env.DB_HOST || 'localhost', @@ -105,30 +80,10 @@ function salvarCustomDatabases() { /** Normaliza uma config bruta aplicando o driver e seus defaults. */ function normalize(raw) { - const driver = DRIVERS.includes((raw.driver || '').toLowerCase()) - ? raw.driver.toLowerCase() - : DEFAULT_DRIVER; - const d = DRIVER_DEFAULTS[driver]; + const d = DRIVER_DEFAULTS['firebird']; - if (driver === 'postgres') { - return { - driver, - 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, + driver: 'firebird', host: raw.host || d.host, port: raw.port || d.port, database: raw.database, @@ -178,14 +133,10 @@ function addDatabase(alias, config) { throw new Error('Alias e database são obrigatórios.'); } 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 }; - ['host', 'port', 'database', 'schema', 'user', 'password', 'ssl', - 'encoding', 'lowercase_keys', 'role', 'pageSize', 'wireCrypt', - 'max', 'idleTimeoutMillis', 'connectionTimeoutMillis'] + var stored = { driver: 'firebird' }; + ['host', 'port', 'database', 'user', 'password', + 'encoding', 'lowercase_keys', 'role', 'pageSize', 'wireCrypt'] .forEach(function (k) { if (config[k] !== undefined && config[k] !== null && config[k] !== '') { stored[k] = config[k]; @@ -208,478 +159,172 @@ function removeDatabase(alias) { // ============================================================ // 2) DRIVER FIREBIRD // ============================================================ -const firebirdDriver = (() => { - const Firebird = require('node-firebird'); +const Firebird = require('node-firebird'); - function readBlob(blobFunc) { - return new Promise(function (resolve, reject) { - if (typeof blobFunc !== 'function') return resolve(blobFunc); - blobFunc(function (err, name, emitter) { - if (err) return reject(err); - if (!emitter || typeof emitter.on !== 'function') return resolve(null); - var chunks = []; - var total = 0; - emitter.on('data', function (c) { chunks.push(c); total += c.length; }); - emitter.on('end', function () { resolve(Buffer.concat(chunks, total)); }); - emitter.on('error', reject); - }); +function readBlob(blobFunc) { + return new Promise(function (resolve, reject) { + if (typeof blobFunc !== 'function') return resolve(blobFunc); + blobFunc(function (err, name, emitter) { + if (err) return reject(err); + if (!emitter || typeof emitter.on !== 'function') return resolve(null); + var chunks = []; + var total = 0; + emitter.on('data', function (c) { chunks.push(c); total += c.length; }); + emitter.on('end', function () { resolve(Buffer.concat(chunks, total)); }); + emitter.on('error', reject); }); - } + }); +} - function query(config, sql, params = []) { - return new Promise((resolve, reject) => { - Firebird.attach(config, (err, db) => { - if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`)); - var allRows = []; - db.sequentially(sql, params, function (row) { - var keys = Object.keys(row); - var blobPromises = keys.map(function (key) { - var val = row[key]; - if (typeof val === 'function') { - return readBlob(val).then(function (data) { row[key] = data; }); - } - return Promise.resolve(); - }); - return Promise.all(blobPromises).then(function () { allRows.push(row); }); - }, function (queryErr) { - db.detach(); - if (queryErr) return reject(new Error(`Erro na consulta (firebird): ${queryErr.message}`)); - resolve(allRows); - }); - }); - }); - } - - function execute(config, sql, params = []) { - return new Promise((resolve, reject) => { - Firebird.attach(config, (err, db) => { - if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`)); - db.transaction(Firebird.ISOLATION_READ_COMMITTED, (transErr, transaction) => { - if (transErr) { - db.detach(); - return reject(new Error(`Erro ao iniciar transação (firebird): ${transErr.message}`)); +function firebirdQuery(config, sql, params = []) { + return new Promise((resolve, reject) => { + Firebird.attach(config, (err, db) => { + if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`)); + var allRows = []; + db.sequentially(sql, params, function (row) { + var keys = Object.keys(row); + var blobPromises = keys.map(function (key) { + var val = row[key]; + if (typeof val === 'function') { + return readBlob(val).then(function (data) { row[key] = data; }); } - transaction.query(sql, params, (queryErr, result) => { - if (queryErr) { - transaction.rollback(); - db.detach(); - return reject(new Error(`Erro na execução (firebird): ${queryErr.message}`)); - } - transaction.commit((commitErr) => { - db.detach(); - if (commitErr) return reject(new Error(`Erro ao commitar (firebird): ${commitErr.message}`)); - resolve({ affectedRows: result ? result.length : 0, result }); - }); + return Promise.resolve(); + }); + return Promise.all(blobPromises).then(function () { allRows.push(row); }); + }, function (queryErr) { + db.detach(); + if (queryErr) return reject(new Error(`Erro na consulta (firebird): ${queryErr.message}`)); + resolve(allRows); + }); + }); + }); +} + +function firebirdExecute(config, sql, params = []) { + return new Promise((resolve, reject) => { + Firebird.attach(config, (err, db) => { + if (err) return reject(new Error(`Erro ao conectar (firebird): ${err.message}`)); + db.transaction(Firebird.ISOLATION_READ_COMMITTED, (transErr, transaction) => { + if (transErr) { + db.detach(); + return reject(new Error(`Erro ao iniciar transação (firebird): ${transErr.message}`)); + } + transaction.query(sql, params, (queryErr, result) => { + if (queryErr) { + transaction.rollback(); + db.detach(); + return reject(new Error(`Erro na execução (firebird): ${queryErr.message}`)); + } + transaction.commit((commitErr) => { + db.detach(); + if (commitErr) return reject(new Error(`Erro ao commitar (firebird): ${commitErr.message}`)); + resolve({ affectedRows: result ? result.length : 0, result }); }); }); }); }); - } + }); +} - async function testConnection(config) { - await query(config, 'SELECT 1 FROM RDB$DATABASE'); - return true; - } +async function firebirdTestConnection(config) { + await firebirdQuery(config, 'SELECT 1 FROM RDB$DATABASE'); + return true; +} - async function listTables(config) { - const rows = await query(config, ` - SELECT TRIM(RDB$RELATION_NAME) AS TABLE_NAME - FROM RDB$RELATIONS - WHERE RDB$SYSTEM_FLAG = 0 AND RDB$RELATION_TYPE = 0 - ORDER BY RDB$RELATION_NAME - `); - return rows.map((r) => (r.TABLE_NAME || '').trim()).filter(Boolean); - } +async function firebirdListTables(config) { + const rows = await firebirdQuery(config, ` + SELECT TRIM(RDB$RELATION_NAME) AS TABLE_NAME + FROM RDB$RELATIONS + WHERE RDB$SYSTEM_FLAG = 0 AND RDB$RELATION_TYPE = 0 + ORDER BY RDB$RELATION_NAME + `); + return rows.map((r) => (r.TABLE_NAME || '').trim()).filter(Boolean); +} - async function tableInfo(config, tableName) { - const rows = await query(config, ` - SELECT - rf.RDB$FIELD_NAME AS COLUMN_NAME, - rf.RDB$FIELD_POSITION AS ORDINAL_POSITION, - CASE f.RDB$FIELD_TYPE - WHEN 7 THEN 'SMALLINT' WHEN 8 THEN 'INTEGER' WHEN 16 THEN 'BIGINT' - WHEN 9 THEN 'QUAD' WHEN 10 THEN 'FLOAT' WHEN 27 THEN 'DOUBLE PRECISION' - WHEN 12 THEN 'DATE' WHEN 13 THEN 'TIME' WHEN 35 THEN 'TIMESTAMP' - WHEN 37 THEN 'VARCHAR' WHEN 40 THEN 'CSTRING' WHEN 45 THEN 'BLOB_ID' - WHEN 261 THEN 'BLOB' WHEN 14 THEN 'CHAR' WHEN 41 THEN 'NUMERIC' - ELSE 'UNKNOWN' - END AS DATA_TYPE, - f.RDB$FIELD_LENGTH AS FIELD_LENGTH, - f.RDB$FIELD_SCALE AS FIELD_SCALE, - f.RDB$FIELD_PRECISION AS FIELD_PRECISION, - rf.RDB$NULL_FLAG AS NULL_FLAG - FROM RDB$RELATION_FIELDS rf - INNER JOIN RDB$FIELDS f ON rf.RDB$FIELD_SOURCE = f.RDB$FIELD_NAME - WHERE rf.RDB$RELATION_NAME = ? - ORDER BY rf.RDB$FIELD_POSITION - `, [String(tableName).toUpperCase()]); +async function firebirdTableInfo(config, tableName) { + const rows = await firebirdQuery(config, ` + SELECT + rf.RDB$FIELD_NAME AS COLUMN_NAME, + rf.RDB$FIELD_POSITION AS ORDINAL_POSITION, + CASE f.RDB$FIELD_TYPE + WHEN 7 THEN 'SMALLINT' WHEN 8 THEN 'INTEGER' WHEN 16 THEN 'BIGINT' + WHEN 9 THEN 'QUAD' WHEN 10 THEN 'FLOAT' WHEN 27 THEN 'DOUBLE PRECISION' + WHEN 12 THEN 'DATE' WHEN 13 THEN 'TIME' WHEN 35 THEN 'TIMESTAMP' + WHEN 37 THEN 'VARCHAR' WHEN 40 THEN 'CSTRING' WHEN 45 THEN 'BLOB_ID' + WHEN 261 THEN 'BLOB' WHEN 14 THEN 'CHAR' WHEN 41 THEN 'NUMERIC' + ELSE 'UNKNOWN' + END AS DATA_TYPE, + f.RDB$FIELD_LENGTH AS FIELD_LENGTH, + f.RDB$FIELD_SCALE AS FIELD_SCALE, + f.RDB$FIELD_PRECISION AS FIELD_PRECISION, + rf.RDB$NULL_FLAG AS NULL_FLAG + FROM RDB$RELATION_FIELDS rf + INNER JOIN RDB$FIELDS f ON rf.RDB$FIELD_SOURCE = f.RDB$FIELD_NAME + WHERE rf.RDB$RELATION_NAME = ? + ORDER BY rf.RDB$FIELD_POSITION + `, [String(tableName).toUpperCase()]); - return rows.map((row) => ({ - name: (row.COLUMN_NAME || '').trim(), - position: row.ORDINAL_POSITION, - type: (row.DATA_TYPE || '').trim(), - length: row.FIELD_LENGTH, - precision: row.FIELD_PRECISION, - scale: row.FIELD_SCALE, - nullable: row.NULL_FLAG !== 1, - })); - } + return rows.map((row) => ({ + name: (row.COLUMN_NAME || '').trim(), + position: row.ORDINAL_POSITION, + type: (row.DATA_TYPE || '').trim(), + length: row.FIELD_LENGTH, + precision: row.FIELD_PRECISION, + scale: row.FIELD_SCALE, + nullable: row.NULL_FLAG !== 1, + })); +} - async function tableExists(config, tableName) { - const r = await query(config, - "SELECT COUNT(*) AS CT FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = ?", - [String(tableName).toUpperCase()]); - return (r[0] && r[0].CT > 0) || false; - } +async function firebirdTableExists(config, tableName) { + const r = await firebirdQuery(config, + "SELECT COUNT(*) AS CT FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = ?", + [String(tableName).toUpperCase()]); + return (r[0] && r[0].CT > 0) || false; +} - async function columnExists(config, tableName, columnName) { - const r = await query(config, - "SELECT COUNT(*) AS CT FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = ? AND RDB$FIELD_NAME = ?", - [String(tableName).toUpperCase(), String(columnName).toUpperCase()]); - return (r[0] && r[0].CT > 0) || false; - } +async function firebirdColumnExists(config, tableName, columnName) { + const r = await firebirdQuery(config, + "SELECT COUNT(*) AS CT FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = ? AND RDB$FIELD_NAME = ?", + [String(tableName).toUpperCase(), String(columnName).toUpperCase()]); + return (r[0] && r[0].CT > 0) || false; +} - async function close() { /* Firebird abre conexão por consulta; nada a encerrar */ } - - return { query, execute, testConnection, listTables, tableInfo, tableExists, columnExists, close }; -})(); +async function firebirdClose() { /* Firebird abre conexão por consulta; nada a encerrar */ } // ============================================================ -// 3) DRIVER POSTGRES (pool + tradutor de SQL Firebird->PG + schema) +// 3) DISPATCHER — API pública // ============================================================ -const postgresDriver = (() => { - const pg = require('pg'); +const driver = { + 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. - 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) { +function getConn(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 }; } function query(alias, sql, params = []) { 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}`); }); } function execute(alias, sql, params = []) { 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}`); }); } async function testConnection(alias) { try { - const { config, driver } = getDriver(alias); + const { config } = getConn(alias); await driver.testConnection(config); return true; } catch (err) { @@ -697,33 +342,33 @@ async function testAllConnections() { } function listTables(alias) { - const { config, driver } = getDriver(alias); + const { config } = getConn(alias); return driver.listTables(config); } function tableInfo(alias, tableName) { - const { config, driver } = getDriver(alias); + const { config } = getConn(alias); return driver.tableInfo(config, tableName); } function tableExists(alias, tableName) { - const { config, driver } = getDriver(alias); + const { config } = getConn(alias); return driver.tableExists(config, tableName); } function columnExists(alias, tableName, columnName) { - const { config, driver } = getDriver(alias); + const { config } = getConn(alias); 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) { - return getConfig(alias).driver; + return 'firebird'; } -/** Encerra os pools de todos os drivers (shutdown gracioso). */ +/** Encerra recursos (noop para Firebird). */ async function closeAll() { - await Promise.all(Object.values(drivers).map((d) => d.close && d.close())); + await driver.close(); } module.exports = { diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js index 708ed12..eb00c1d 100644 --- a/src/middlewares/auth.js +++ b/src/middlewares/auth.js @@ -50,6 +50,23 @@ async function authenticateToken(req, res, next) { .map(function (e) { return Number(e); }) .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.authType = 'jwt'; return next(); @@ -90,7 +107,7 @@ async function authenticateToken(req, res, next) { FROM USUARIOS WHERE USU_TOKEN = ? AND USU_STATUS = 'A' - AND COALESCE(USU_ACESSO_WEB, 0) = 1`, + AND COALESCE(USU_ACESSO_WEB, '0') IN ('1', 'S')`, [token] ); diff --git a/src/resolucaoSetup.js b/src/resolucaoSetup.js index 524cd4f..ac25bb4 100644 --- a/src/resolucaoSetup.js +++ b/src/resolucaoSetup.js @@ -1,6 +1,6 @@ /** * 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 * - Flags em CHATC2_CONFIGURACOES_EMPRESA (CFE_MOTIVO_*, CFE_RESOLUCAO_*) * - Colunas CON_MOTIVO_ID / CON_RESOLUCAO em CHATC2_CONVERSAS