From ae629d1dc2471234ba62e34fab6c4e6986be38e1 Mon Sep 17 00:00:00 2001 From: Ayron Santos Date: Wed, 17 Jun 2026 10:02:59 -0300 Subject: [PATCH] =?UTF-8?q?Migra=C3=A7=C3=A3o=20para=20PostgreSQL=20multi-?= =?UTF-8?q?driver=20+=20corre=C3=A7=C3=B5es=20de=20seguran=C3=A7a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Camada de banco unificada (src/database.js): drivers Postgres/Firebird, tradutor de SQL, suporte a schema e pool de conexões - Conexões: novo_local (Postgres externo) e firebird_local (legado) - Tela de rotas da API redesenhada (auth, params, exemplos de body) - Correções de segurança (críticos/altos/médios/baixos): XSS no chat, escalonamento de privilégio, mídia autenticada, SQL restrito a gerente, JWT sem fallback + issuer, IDOR em conversas, CORS por allowlist, rate-limit no login, limites de corpo por rota - Deploy alinhado: install.sh grava .env com PG_*, migracoes.js driver-aware Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 16 + CONTEXTO.md | 176 +++ deploy-linux/deploy.ps1 | 81 ++ deploy-linux/install.sh | 482 ++++++++ deploy-linux/patch_node_firebird.py | 127 ++ package-lock.json | 1328 ++++++++++++++++++++ package.json | 28 + scripts/definir-senha-web.js | 59 + scripts/gerar-token-jwt.js | 130 ++ scripts/gerar-token-usuario.js | 67 + scripts/habilitar-acesso-web.js | 63 + scripts/listar-tokens.js | 55 + scripts/migracoes.js | 490 ++++++++ src/app.js | 109 ++ src/config/auth.js | 19 + src/controllers/authController.js | 185 +++ src/controllers/chatController.js | 1329 ++++++++++++++++++++ src/controllers/clientController.js | 987 +++++++++++++++ src/controllers/configController.js | 246 ++++ src/controllers/databaseController.js | 151 +++ src/controllers/empresaController.js | 74 ++ src/controllers/evolutionController.js | 1223 +++++++++++++++++++ src/controllers/genericController.js | 136 +++ src/controllers/menuController.js | 250 ++++ src/controllers/routesController.js | 401 ++++++ src/controllers/transcriber.js | 177 +++ src/controllers/triageController.js | 1076 ++++++++++++++++ src/database.js | 682 +++++++++++ src/middlewares/auth.js | 149 +++ src/middlewares/roles.js | 27 + src/public/admin-conversations.html | 182 +++ src/public/chat.html | 1548 ++++++++++++++++++++++++ src/public/client-detail.html | 741 ++++++++++++ src/public/client-list.html | 346 ++++++ src/public/csat.html | 208 ++++ src/public/css/dark-mode.css | 1054 ++++++++++++++++ src/public/css/main.css | 648 ++++++++++ src/public/dashboard.html | 188 +++ src/public/js/dark-mode.js | 44 + src/public/login.html | 359 ++++++ src/public/routes.html | 336 +++++ src/public/settings.html | 858 +++++++++++++ src/routes/authRoutes.js | 59 + src/routes/chatRoutes.js | 22 + src/routes/configRoutes.js | 38 + src/routes/databaseRoutes.js | 11 + src/routes/genericRoutes.js | 66 + src/routes/index.js | 42 + src/routes/menuRoutes.js | 12 + src/server.js | 52 + 50 files changed, 17137 insertions(+) create mode 100644 .gitignore create mode 100644 CONTEXTO.md create mode 100644 deploy-linux/deploy.ps1 create mode 100644 deploy-linux/install.sh create mode 100644 deploy-linux/patch_node_firebird.py create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/definir-senha-web.js create mode 100644 scripts/gerar-token-jwt.js create mode 100644 scripts/gerar-token-usuario.js create mode 100644 scripts/habilitar-acesso-web.js create mode 100644 scripts/listar-tokens.js create mode 100644 scripts/migracoes.js create mode 100644 src/app.js create mode 100644 src/config/auth.js create mode 100644 src/controllers/authController.js create mode 100644 src/controllers/chatController.js create mode 100644 src/controllers/clientController.js create mode 100644 src/controllers/configController.js create mode 100644 src/controllers/databaseController.js create mode 100644 src/controllers/empresaController.js create mode 100644 src/controllers/evolutionController.js create mode 100644 src/controllers/genericController.js create mode 100644 src/controllers/menuController.js create mode 100644 src/controllers/routesController.js create mode 100644 src/controllers/transcriber.js create mode 100644 src/controllers/triageController.js create mode 100644 src/database.js create mode 100644 src/middlewares/auth.js create mode 100644 src/middlewares/roles.js create mode 100644 src/public/admin-conversations.html create mode 100644 src/public/chat.html create mode 100644 src/public/client-detail.html create mode 100644 src/public/client-list.html create mode 100644 src/public/csat.html create mode 100644 src/public/css/dark-mode.css create mode 100644 src/public/css/main.css create mode 100644 src/public/dashboard.html create mode 100644 src/public/js/dark-mode.js create mode 100644 src/public/login.html create mode 100644 src/public/routes.html create mode 100644 src/public/settings.html create mode 100644 src/routes/authRoutes.js create mode 100644 src/routes/chatRoutes.js create mode 100644 src/routes/configRoutes.js create mode 100644 src/routes/databaseRoutes.js create mode 100644 src/routes/genericRoutes.js create mode 100644 src/routes/index.js create mode 100644 src/routes/menuRoutes.js create mode 100644 src/server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9adf2e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +node_modules/ +.env +.env.bak.* +src/databases_custom.json + +# Bancos / dumps +*.FDB +*.fdb +*.rar +relatorio_migracao_*.json + +# Logs / temporários +*.log +_inspect*.js +whisper/ +.claude diff --git a/CONTEXTO.md b/CONTEXTO.md new file mode 100644 index 0000000..3eb4a64 --- /dev/null +++ b/CONTEXTO.md @@ -0,0 +1,176 @@ +a # Chatc2 - Contexto do Projeto + +_Arquivo gerado em 05/06/2026 para retomada em qualquer computador._ + +--- + +## 📋 Missão Atual + +Plataforma multi-empresa de atendimento com WhatsApp (Evolution API) + Firebase Firebird, incluindo: +- Chat com atendimento humano e triagem automatizada +- Geração de boletos em PDF via API externa +- Transcrição de áudio com Whisper.cpp (local) +- Fluxo de atendimento personalizado por empresa (submenus, etiquetas) +- Breve: integração com Asterisk para ligações telefônicas + +--- + +## 🗂️ Estrutura de Arquivos (49 arquivos) + +``` +src/ +├── app.js # Express app setup +├── server.js # Entry point (porta 3000) +├── database.js # Conexão Firebird (query, execute, readBlob) +├── config/ +│ ├── databases.js # Conexões estáticas + custom (JSON) +│ ├── databases_custom.json # Conexões adicionadas via API +│ ├── database.js # Config padrão do banco +│ └── auth.js # JWT secret/expires +├── middlewares/ +│ └── auth.js # JWT + USU_TOKEN dual auth +├── controllers/ +│ ├── authController.js # Login, /me, dashboard +│ ├── chatController.js # Conversas, mensagens, mídia, Evolution send +│ ├── clientController.js # CRUD clientes, carnês, dependentes, convalescentes, listcarne +│ ├── configController.js # Config empresa, equipes, etiquetas, instâncias +│ ├── evolutionController.js # Webhook Evolution, QR Code, instâncias +│ ├── genericController.js # Health, tables, query, aliases +│ ├── menuController.js # CRUD CHATC2_MENUS_EMPRESA (submenus do fluxo) +│ ├── triageController.js # Fluxo de triagem (sendMenu, processResponse, boleto) +│ ├── databaseController.js # CRUD de conexões de banco (custom JSON) +│ ├── routesController.js # Descoberta automática de rotas +│ ├── empresaController.js # Logo da empresa +│ └── transcriber.js # Whisper.cpp para transcrição de áudio +├── routes/ +│ ├── index.js # Agregador de rotas +│ ├── authRoutes.js, chatRoutes.js, configRoutes.js +│ ├── genericRoutes.js, menuRoutes.js, databaseRoutes.js +└── public/ + ├── login.html, dashboard.html, chat.html, client-list.html + ├── client-detail.html, settings.html, routes.html + ├── admin-conversations.html, csat.html + ├── css/dark-mode.css, css/main.css + └── js/dark-mode.js +scripts/ +├── migracoes.js # 19 migrações de banco (controle via CHATC2_CONTROLE_MIGRACOES) +├── criar_tabelas_chat.js # Script original de criação de tabelas +├── gerar-token-jwt.js, gerar-token-usuario.js +├── habilitar-acesso-web.js, definir-senha-web.js +└── listar-tokens.js +deploy/ +├── cloud-init.yml # Deploy automático Ubuntu 22.04 +├── setup.sh # Instalação manual +└── README_INSTALACAO.md # Guia completo +``` + +--- + +## 🔥 Principais Funcionalidades Implementadas + +### 1. Autenticação +- JWT + USU_TOKEN (dual auth) +- Login com USU_LOGIN + USU_SENHA (ou USU_SENHA_WEB) +- Roles: Agente (A) e Gerente/Admin (G) via USU_TIPO_CHAT +- `/app/:alias/me` retorna dados do usuário + +### 2. Webhook Evolution API (v2.3.7) +- URL: `POST /api/webhook/evolution` (sem alias na URL) +- Detecta alias automaticamente pelo nome da instância +- Cria conversas novas ou reusa existentes (últimos 8 dígitos) +- Busca cliente por telefone (CLI_CELULAR, CLI_FONE1, CLI_FONE2, DEPENDENTES_CLI) +- Sempre corrige CON_CLIENTE_ID pelo telefone (novas + existentes) +- Mídia: tenta download via URL, depois /chat/getMedia/, depois base64 embutido + +### 3. Chat (Tempo Real) +- Sidebar: Minhas / Sem Atendimento / Equipe / Todas (com contagens) +- Painel central com mensagens +- Painel direito: atendente, equipe, etiquetas +- Envio de texto, imagem, áudio (MediaRecorder → base64), documento +- Prévia do áudio antes de enviar +- Transcrição de áudio via Whisper.cpp (exibida abaixo do player) + +### 4. Triagem / Fluxo Automático +- Menu principal = Equipes (ordenadas por EQU_ORDEM) + Boleto (fixo) +- **Ao escolher equipe: CON_EQUIPE_ID atribuído IMEDIATAMENTE** +- Submenus via CHATC2_MENUS_EMPRESA (M=submenu, T=texto, R=rota) +- Navegação multi-nível com "0 - Voltar" +- Etiquetas associadas a cada submenu (adicionadas automaticamente) +- Boleto: lista títulos → seleciona → gera PDF via API externa → envia via Evolution → pergunta "continuar?" + +### 5. Boleto +- Geração: `POST https://cobpagweb.com.br/boleto/cliente/index.php?base64` +- Payload: `{ empresa: {EMP_NOME, EMP_CNPJ, EMP_FOTO}, Cliente: {...}, Carnes: [...] }` +- Envio Evolution: `/message/sendMedia/` com `mediatype: 'document'` +- Fallback: texto com dados do boleto quando PDF não gerado +- Nome do arquivo: ` - .pdf` + +### 6. Whisper.cpp (Transcrição de Áudio) +- Instalado em `whisper/` (main.exe/main + modelo tiny) +- Transcreve áudios recebidos via webhook +- Salva transcrição em `MAT_TRANSCRICAO` (BLOB SUB_TYPE TEXT) +- Exibe no chat: player + transcrição abaixo +- **Limitação: Evolution API v2.3.7 não fornece áudio válido para download** + - Funcionará quando atualizar a Evolution API (> v2.5.0) + +### 7. Settings (Configurações) +- Abas: Equipe, Fluxo, Empresa, Etiquetas, Conexão +- Fluxo: CRUD de submenus por equipe (árvore), com tipos e etiquetas +- Empresa: saudação, triagem, CSAT, foto, nome do usuário +- Etiquetas: nome + cor +- Conexão: instâncias WhatsApp (criar, editar, QR Code) + +### 8. Banco de Dados (Migrações) +- Script: `scripts/migracoes.js` — 19 migrações +- Tabelas: CHATC2_INSTANCIAS, CHATC2_EQUIPES, CHATC2_USU_EQUIPES, CHATC2_ETIQUETAS +- CHATC2_CONVERSAS, CHATC2_CONVERSAS_MENSAGENS, CHATC2_MENSAGENS_ATENDIMENTOS +- CHATC2_CONVERSAS_ETIQUETAS, CHATC2_CSAT_AVALIACOES, CHATC2_CONFIGURACOES_EMPRESA +- CHATC2_MENUS_EMPRESA (com MNE_ACAO_ROTA, MNE_ETIQUETA_IDS) +- Controle via `CHATC2_CONTROLE_MIGRACOES` (cada migração roda uma vez) + +--- + +## 🎯 Próximos Passos (quando retomar) + +1. **Aguardar**: Usuário vai contratar SIP Trunk e instalar Asterisk +2. **Implementar**: Módulo Voice (AMI) para fazer ligações + transcrição +3. **Atualizar**: Evolution API (> v2.5.0) para download de mídia funcionar +4. **Corrigir pendências da conversa**: + - Etiquetas em submenus (não estava persistindo - adicionar logs `[Menu]` no console) + - Transcrição só funciona para áudio do agente (aguardando Evolution atualizar) + +--- + +## 🔗 URLs Importantes + +| Recurso | URL | +|---------|-----| +| Login | `/app/:alias/login` | +| Chat | `/app/:alias/company/:empresaId/conversation/:conversaId` | +| Clientes | `/app/:alias/clients` | +| Settings | `/app/:alias/settings` | +| Rotas | `/app/:alias/routes` | +| CSAT | `/app/:alias/csat` | +| Webhook | `POST /api/webhook/evolution` | +| Rotas API | `GET /api/routes` | +| Bancos | `GET /api/:alias/databases` | +| Carnê | `GET /api/:alias/clients/:id/listcarne` | +| Convalescentes | `GET /api/:alias/clients/:id/convalescentes` | + +## 🔑 Testes + +| Usuário | Senha | Base | Tipo | +|---------|-------|------|------| +| SUPORTE | 123456 | lajedo | Admin (G) | +| SUPORTE | 123456 | novo_local | Admin (G) | + +## ☁️ Deploy + +Arquivos em `deploy/`: +- `cloud-init.yml` — cloud-init (DigitalOcean, Vultr, etc.) +- `setup.sh` — instalação manual +- `README_INSTALACAO.md` — guia completo + +## 📦 Backup + +`ponto_1/` contém snapshot completo do código fonte. diff --git a/deploy-linux/deploy.ps1 b/deploy-linux/deploy.ps1 new file mode 100644 index 0000000..aa3cd6a --- /dev/null +++ b/deploy-linux/deploy.ps1 @@ -0,0 +1,81 @@ +# ============================================================= +# Chatc2 - Script de deploy Windows -> Linux +# Uso: .\deploy.ps1 -IP 192.168.1.100 +# ============================================================= + +param( + [Parameter(Mandatory=$true)] + [string]$IP, + [string]$User = "root", + [string]$ProjectPath = (Split-Path -Parent $PSScriptRoot), + [string]$RemotePath = "/home/chatc2/chatc2" +) + +Write-Host "╔════════════════════════════════════════════╗" -ForegroundColor Cyan +Write-Host "║ Chatc2 - Deploy Windows -> Linux ║" -ForegroundColor Cyan +Write-Host "╚════════════════════════════════════════════╝" -ForegroundColor Cyan +Write-Host "" + +# 1. Testar conexao SSH +Write-Host "[1/5] Testando conexao SSH..." -ForegroundColor Yellow +try { + $result = ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 "$User@$IP" "echo OK" 2>&1 + if ($LASTEXITCODE -ne 0) { throw "SSH falhou" } + Write-Host " ✓ Conectado a $IP" -ForegroundColor Green +} catch { + Write-Host " ✘ Erro: $_" -ForegroundColor Red + exit 1 +} + +# 2. Criar diretorios no servidor +Write-Host "[2/5] Criando diretorios no servidor..." -ForegroundColor Yellow +ssh "$User@$IP" "mkdir -p $RemotePath/{db,logs,tmp,uploads/audio,public,whisper/models} 2>/dev/null; chown -R chatc2:chatc2 $RemotePath 2>/dev/null; echo 'OK'" 2>$null +Write-Host " ✓ Diretorios criados" -ForegroundColor Green + +# 3. Enviar arquivos via SCP +Write-Host "[3/5] Enviando arquivos do projeto..." -ForegroundColor Yellow +Write-Host " (isso pode levar alguns minutos...)" -ForegroundColor Gray + +$exclude = @('node_modules', '.git', 'db/*.FDB', 'logs', 'tmp', 'Nova pasta', 'chatc2-py', 'ponto_1', 'whisper') +$excludeArgs = ($exclude | ForEach-Object { "--exclude=$_" }) -join ' ' + +$scpCommand = "scp -r $excludeArgs `"$ProjectPath\*`" $User@`"$IP`":$RemotePath/" +Write-Host " Executando: scp -r (arquivos do projeto) para $IP..." -ForegroundColor Gray + +try { + $result = scp -r ` + --exclude='node_modules' ` + --exclude='.git' ` + --exclude='db/*.FDB' ` + --exclude='logs' ` + --exclude='tmp' ` + --exclude='Nova pasta' ` + --exclude='chatc2-py' ` + --exclude='ponto_1' ` + --exclude='whisper' ` + "$ProjectPath\*" "$User@$IP`:$RemotePath/" 2>&1 + + if ($LASTEXITCODE -ne 0) { throw "SCP falhou" } + Write-Host " ✓ Arquivos enviados" -ForegroundColor Green +} catch { + Write-Host " ✘ Erro no SCP: $_" -ForegroundColor Red + Write-Host " Tente enviar manualmente via FileZilla ou WinSCP" -ForegroundColor Yellow +} + +# 4. Ajustar permissoes +Write-Host "[4/5] Ajustando permissoes..." -ForegroundColor Yellow +ssh "$User@$IP" "chown -R chatc2:chatc2 $RemotePath 2>/dev/null; chmod -R 755 $RemotePath 2>/dev/null; echo OK" 2>$null +Write-Host " ✓ Permissoes ajustadas" -ForegroundColor Green + +# 5. Instalar dependencias e rodar script de instalacao +Write-Host "[5/5] Instalando dependencias e configurando servico..." -ForegroundColor Yellow +ssh "$User@$IP" "cd $RemotePath/deploy-linux && sudo bash install.sh --continue" 2>&1 | Write-Host + +Write-Host "" +Write-Host "╔════════════════════════════════════════════╗" -ForegroundColor Cyan +Write-Host "║ Deploy Concluido! ║" -ForegroundColor Cyan +Write-Host "╚════════════════════════════════════════════╝" -ForegroundColor Cyan +Write-Host "" +Write-Host " Acesse: http://$IP" -ForegroundColor Green +Write-Host " SSH: ssh $User@$IP" -ForegroundColor Gray +Write-Host " Logs: ssh $User@$IP 'pm2 logs chatc2'" -ForegroundColor Gray diff --git a/deploy-linux/install.sh b/deploy-linux/install.sh new file mode 100644 index 0000000..a48b385 --- /dev/null +++ b/deploy-linux/install.sh @@ -0,0 +1,482 @@ +#!/bin/bash +# ============================================================= +# Chatc2 - Instalacao automatizada para Ubuntu 22.04 LTS +# Uso: curl -sL https://bit.ly/chatc2-install | bash +# ou: wget -qO- https://bit.ly/chatc2-install | bash +# ============================================================= +set -euo pipefail + +# ████████████████████████████████████████████████████ +# VARIAVEIS +# ████████████████████████████████████████████████████ +CHATC2_USER="chatc2" +CHATC2_DIR="/home/$CHATC2_USER/chatc2" +CHATC2_REPO="https://github.com/seu-usuario/chatc2/archive/refs/heads/main.tar.gz" +NODE_VERSION="22" +FIREBIRD_VERSION="3.0" +WHISPER_VERSION="1.5.4" +WHISPER_MODEL="ggml-tiny.bin" # 75MB - troque para ggml-base.bin (142MB) para mais precisao +SERVER_DOMAIN="${SERVER_DOMAIN:-}" # Opcional: seu-dominio.com.br +JWT_SECRET="" +NGROK_URL="" + +# PostgreSQL EXTERNO (banco principal "novo_local"). +# 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}" + +# Cores +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +log() { echo -e "${GREEN}[✓]${NC} $1"; } +warn() { echo -e "${YELLOW}[!]${NC} $1"; } +err() { echo -e "${RED}[✘]${NC} $1"; exit 1; } +info() { echo -e "${CYAN}[i]${NC} $1"; } + +# ████████████████████████████████████████████████████ +# VERIFICACAO INICIAL +# ████████████████████████████████████████████████████ +if [ "$EUID" -ne 0 ]; then + err "Execute como root: sudo bash install.sh" +fi + +echo "" +echo "╔══════════════════════════════════════════════════╗" +echo "║ Chatc2 - Instalacao Automatica ║" +echo "║ Ubuntu 22.04 LTS ║" +echo "╚══════════════════════════════════════════════════╝" +echo "" + +# ████████████████████████████████████████████████████ +# PASSO 1: SYSTEMA BASE +# ████████████████████████████████████████████████████ +info "Passo 1/12: Atualizando sistema..." +apt-get update -y +apt-get upgrade -y +apt-get install -y curl wget git unzip tar gzip \ + build-essential python3 python3-pip \ + ffmpeg nginx ufw certbot python3-certbot-nginx \ + firebird3.0-server firebird3.0-utils + +# ████████████████████████████████████████████████████ +# PASSO 2: NODE.JS +# ████████████████████████████████████████████████████ +info "Passo 2/12: Instalando Node.js $NODE_VERSION..." +curl -fsSL "https://deb.nodesource.com/setup_${NODE_VERSION}.x" | bash - +apt-get install -y nodejs +npm install -g pm2 +log "Node.js $(node -v) instalado" + +# ████████████████████████████████████████████████████ +# PASSO 3: FIREBIRD +# ████████████████████████████████████████████████████ +info "Passo 3/12: Configurando Firebird..." +systemctl enable firebird3.0 +systemctl start firebird3.0 2>/dev/null || true +sleep 2 +sed -i 's/^isc_password =.*/isc_password = masterkey/' /etc/firebird/3.0/firebird.conf 2>/dev/null || true +sed -i 's/^RemoteBindAddress = localhost/RemoteBindAddress = 0.0.0.0/' /etc/firebird/3.0/firebird.conf 2>/dev/null || true + +# ── Correção de compatibilidade node-firebird + Firebird 3.0 ── +# Usa Legacy_Auth (mais compatível) em vez de Srp256 (não suportado pelo node-firebird) +sed -i 's/^#AuthServer = Srp$/AuthServer = Legacy_Auth/' /etc/firebird/3.0/firebird.conf 2>/dev/null || true +sed -i 's/^#AuthClient = Srp, Srp256, Legacy_Auth #Non Windows clients$/AuthClient = Legacy_Auth/' /etc/firebird/3.0/firebird.conf 2>/dev/null || true +# WireCrypt = Enabled (compatível com Legacy_Auth — não exige criptografia) +sed -i 's/^#WireCrypt = Enabled (for client) \/ Required (for server)$/WireCrypt = Enabled/' /etc/firebird/3.0/firebird.conf 2>/dev/null || true + +systemctl restart firebird3.0 +log "Firebird 3.0 configurado (senha: masterkey, auth: Legacy_Auth, WireCrypt: Enabled)" +# Garante que o Firebird esta rodando antes de continuar +systemctl restart firebird3.0 2>/dev/null || systemctl start firebird3.0 2>/dev/null || true +sleep 1 + +# ████████████████████████████████████████████████████ +# PASSO 4: USUARIO E DIRETORIOS +# ████████████████████████████████████████████████████ +info "Passo 4/12: Criando usuario e diretorios..." +id -u "$CHATC2_USER" &>/dev/null || useradd -m -s /bin/bash "$CHATC2_USER" +mkdir -p "$CHATC2_DIR"/{db,logs,tmp,uploads/audio,public,whisper/models,scripts,app/config,app/controllers,app/routes,app/middlewares} +chown -R "$CHATC2_USER:$CHATC2_USER" "$CHATC2_DIR" + +# ████████████████████████████████████████████████████ +# PASSO 5: WHISPER.CPP (Transcricao de Audio) +# ████████████████████████████████████████████████████ +info "Passo 5/12: Instalando Whisper.cpp (compilacao nativa Linux)..." +mkdir -p "$CHATC2_DIR/whisper/models" + +# Compila whisper.cpp do source (Linux) - binario nativo +if [ ! -f "$CHATC2_DIR/whisper/main" ]; then + info "Compilando whisper.cpp v${WHISPER_VERSION}..." + cd /tmp + rm -rf whisper-build 2>/dev/null || true + git clone --depth 1 --branch v${WHISPER_VERSION} https://github.com/ggerganov/whisper.cpp.git whisper-build 2>&1 | tail -1 + cd whisper-build + make -j$(nproc) main 2>&1 | tail -3 + cp -f main "$CHATC2_DIR/whisper/main" + chmod +x "$CHATC2_DIR/whisper/main" + cd /tmp && rm -rf whisper-build + log "Whisper.cpp compilado e instalado em $CHATC2_DIR/whisper/main" +else + log "Whisper.cpp binario Linux ja existe, pulando compilacao" +fi + +info "Baixando modelo $WHISPER_MODEL (pode levar alguns minutos)..." +if [ ! -f "$CHATC2_DIR/whisper/models/$WHISPER_MODEL" ]; then + su - "$CHATC2_USER" -c "cd $CHATC2_DIR/whisper && \ + curl -sL -o models/$WHISPER_MODEL \ + https://huggingface.co/ggerganov/whisper.cpp/resolve/main/$WHISPER_MODEL" + log "Modelo $WHISPER_MODEL baixado" +else + log "Modelo $WHISPER_MODEL ja existe" +fi +log "Whisper.cpp instalado em $CHATC2_DIR/whisper/" + +# ████████████████████████████████████████████████████ +# PASSO 6: ARQUIVOS DO PROJETO +# ████████████████████████████████████████████████████ +info "Passo 6/12: Preparando para receber arquivos..." +info "Transfira os arquivos do projeto para $CHATC2_DIR via SCP/SFTP" +info "Depois execute manualmente: cd $CHATC2_DIR && npm install" +echo "" +echo " ╔══════════════════════════════════════════════════════════╗" +echo " ║ Comando para enviar os arquivos (do seu computador): ║" +echo " ║ ║" +echo " ║ cd C:/projects/Chatc2 ║" +echo " ║ scp -r src/ scripts/ package.json package-lock.json ║" +echo " ║ .env.example deploy-linux/ ║" +echo " ║ root@:$CHATC2_DIR/ ║" +echo " ║ ║" +echo " ║ E depois rode novamente este script: ║" +echo " ║ bash $0 --continue ║" +echo " ╚══════════════════════════════════════════════════════════╝" +echo "" + +# Se o script foi chamado com --continue, pula a espera +if [ "${1:-}" != "--continue" ]; then + info "Aguardando transferencia dos arquivos..." + info "Apos transferir, execute: bash $0 --continue" + exit 0 +fi + +# ████████████████████████████████████████████████████ +# PASSO 7: NPM INSTALL +# ████████████████████████████████████████████████████ +if [ ! -f "$CHATC2_DIR/package.json" ]; then + err "Arquivos do projeto nao encontrados em $CHATC2_DIR. Transfira via SCP primeiro." +fi + +info "Passo 7/12: Instalando dependencias Node..." +chown -R "$CHATC2_USER:$CHATC2_USER" "$CHATC2_DIR" + +# Remove node_modules anterior (se existir) para garantir instalação limpa +if [ -d "$CHATC2_DIR/node_modules" ]; then + info "Removendo node_modules anterior para instalacao limpa..." + rm -rf "$CHATC2_DIR/node_modules" +fi + +# Executa npm install com saida completa (sem suprimir erros) +if ! su - "$CHATC2_USER" -c "cd $CHATC2_DIR && npm install"; then + err "Falha ao instalar dependencias Node. Verifique a conexao e o package.json" +fi + +# Verifica se modulos criticos foram instalados +if [ ! -d "$CHATC2_DIR/node_modules/express" ]; then + err "Modulo 'express' nao encontrado apos npm install. Verifique o package.json" +fi +if [ ! -d "$CHATC2_DIR/node_modules/dotenv" ]; then + info "Instalando dotenv (carregamento de .env)..." + su - "$CHATC2_USER" -c "cd $CHATC2_DIR && npm install dotenv" || warn "Falha ao instalar dotenv" +fi + +# Garante que o server.js carrega dotenv (variaveis do .env) +SERVER_JS="$CHATC2_DIR/src/server.js" +if [ -f "$SERVER_JS" ] && ! grep -q "require('dotenv').config()" "$SERVER_JS" 2>/dev/null; then + sed -i "1s/^/require('dotenv').config();\n/" "$SERVER_JS" + log "dotenv configurado no server.js" +fi +log "Dependencias instaladas e verificadas" + +# ── Patch node-firebird: compatibilidade com Firebird 3.0 + sintaxe Node 22 ── +info "Passo 7b/12: Aplicando patches node-firebird..." +if [ -f "$(dirname "$0")/patch_node_firebird.py" ]; then + python3 "$(dirname "$0")/patch_node_firebird.py" "$CHATC2_DIR" 2>&1 + log "Patches node-firebird aplicados" +else + warn "patch_node_firebird.py nao encontrado" +fi + +# ── wireCrypt (Firebird 3.0) ── +# A partir da v2.0 a camada de banco foi unificada em src/database.js e o +# wireCrypt já vem embutido nos defaults do driver Firebird — nada a patchar. +info "Passo 7c/12: wireCrypt (Firebird) já embutido em src/database.js" +log "Nenhum patch de wireCrypt necessário" + +# ████████████████████████████████████████████████████ +# PASSO 8: ARQUIVO .ENV +# ████████████████████████████████████████████████████ +info "Passo 8/12: Configurando .env..." +JWT_SECRET="CHATc2_$(date +%s)_$(openssl rand -hex 16)" + +# Detecta IPs da maquina +LOCAL_IP=$(ip -4 addr show scope global | grep -oP 'inet \K[\d.]+' | head -1) +[ -z "$LOCAL_IP" ] && LOCAL_IP="127.0.0.1" +PUBLIC_IP=$(curl -s ifconfig.me 2>/dev/null || echo "") + +# Monta URLs +LOCAL_URL="http://${LOCAL_IP}:3000" +if [ -n "$SERVER_DOMAIN" ]; then + EXTERNAL_URL="https://${SERVER_DOMAIN}" +elif [ -n "$PUBLIC_IP" ]; then + EXTERNAL_URL="http://${PUBLIC_IP}" +else + EXTERNAL_URL="http://${LOCAL_IP}:3000" +fi + +# Sempre gera um .env limpo (remove paths Windows, comentarios invalidos, etc.) +if [ -f "$CHATC2_DIR/.env" ]; then + info ".env existente encontrado — fazendo backup e recriando..." + cp "$CHATC2_DIR/.env" "$CHATC2_DIR/.env.bak.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true +fi + +cat > "$CHATC2_DIR/.env" << EOF +# ============================================================ +# Configuracao do Chatc2 +# ============================================================ + +# Driver padrao do banco principal +DB_DRIVER=postgres + +# ------------------------------------------------------------ +# PostgreSQL (EXTERNO) — banco principal "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_ENCODING=UTF-8 +# Caminho do .FDB (vazio = usa ../NOVO.FDB na raiz do projeto) +DB_DATABASE= + +# Servidor +PORT=3000 +JWT_SECRET=$JWT_SECRET +JWT_EXPIRES_IN=1h + +# URLs de acesso +LOCAL_URL=${LOCAL_URL} +EXTERNAL_URL=${EXTERNAL_URL} + +# ------------------------------------------------------------ +# Seguranca (opcionais) +# ------------------------------------------------------------ +# Origens permitidas no CORS (vazio = usa LOCAL_URL + EXTERNAL_URL) +# CORS_ORIGINS=${EXTERNAL_URL} +# Token de verificacao do webhook Evolution (header apikey) +# WEBHOOK_TOKEN= +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 " 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 +# ████████████████████████████████████████████████████ +# ── Permissões dos arquivos .FDB ── +info "Passo 8b/12: Ajustando permissoes dos arquivos .FDB..." +# O Firebird roda como usuario 'firebird'. Os arquivos .FDB precisam +# pertencer ao grupo firebird com permissao de leitura+escrita (660). +if ls "$CHATC2_DIR/db/"*.FDB 2>/dev/null; then + chown "$CHATC2_USER:firebird" "$CHATC2_DIR/db/"*.FDB 2>/dev/null || true + chmod 660 "$CHATC2_DIR/db/"*.FDB 2>/dev/null || true + # Garante que o diretório db/ também seja acessível + chmod 750 "$CHATC2_DIR/db/" 2>/dev/null || true + log "Permissoes ajustadas: $CHATC2_USER:firebird 660" +else + warn "Nenhum arquivo .FDB encontrado em $CHATC2_DIR/db/ — copie os bancos manualmente" + 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). +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" + log "Migracoes Firebird executadas" + fi +else + info "Sem .FDB local — pulando migracoes Firebird (schema do Postgres e externo)." +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) +cat > /tmp/chatc2-habilitar-web.js << 'SCRIPTJS' +const db = require('DATABASE_PATH'); + +(async () => { + const alias = process.argv[2]; + if (!alias) { console.log('Uso: node script '); process.exit(1); } + try { + // Habilita USU_ACESSO_WEB = 1 para usuarios ativos com perfil admin (USU_TIPO = A) + const result = await db.query(alias, + "SELECT USU_CODIGO_ID, USU_NOME, USU_LOGIN FROM USUARIOS WHERE USU_STATUS = 'A' AND USU_TIPO = 'A' AND COALESCE(USU_ACESSO_WEB, 0) = 0"); + for (const u of result) { + await db.execute(alias, 'UPDATE USUARIOS SET USU_ACESSO_WEB = 1 WHERE USU_CODIGO_ID = ?', [u.USU_CODIGO_ID]); + console.log(' ✅ ' + u.USU_NOME.trim() + ' (' + u.USU_LOGIN.trim() + ') — acesso web habilitado'); + } + if (result.length === 0) console.log(' Nenhum usuario admin pendente.'); + process.exit(0); + } catch(e) { console.error('Erro:', e.message); process.exit(1); } +})(); +SCRIPTJS + sed -i "s|DATABASE_PATH|$CHATC2_DIR/src/database|" /tmp/chatc2-habilitar-web.js + + # Postgres externo (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)" + +# ████████████████████████████████████████████████████ +# PASSO 10: NGINX +# ████████████████████████████████████████████████████ +info "Passo 10/12: Configurando Nginx..." +cat > /etc/nginx/sites-available/chatc2 << 'NGINX' +server { + listen 80; + server_name _; + client_max_body_size 100M; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + } +} +NGINX + +# Remove sites padrao que podem conflitar +rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true +rm -f /etc/nginx/sites-enabled/example.com 2>/dev/null || true + +# Habilita o site do chatc2 +if [ ! -L /etc/nginx/sites-enabled/chatc2 ]; then + ln -sf /etc/nginx/sites-available/chatc2 /etc/nginx/sites-enabled/ +fi + +# Testa configuracao antes de aplicar +if nginx -t 2>&1; then + systemctl enable nginx 2>/dev/null || true + systemctl restart nginx 2>/dev/null || systemctl start nginx 2>/dev/null || true + log "Nginx configurado e iniciado" +else + warn "Erro na configuracao do nginx. Verifique manualmente: nginx -t" +fi + +# SSL com Let's Encrypt (se dominio foi informado) +if [ -n "${SERVER_DOMAIN:-}" ]; then + info "Configurando SSL para $SERVER_DOMAIN..." + certbot --nginx -d "$SERVER_DOMAIN" --non-interactive --agree-tos \ + --email "admin@${SERVER_DOMAIN}" || warn "SSL falhou (verifique DNS)" + log "SSL configurado para $SERVER_DOMAIN" +fi + +# ████████████████████████████████████████████████████ +# PASSO 11: PM2 (INICIO AUTOMATICO) +# ████████████████████████████████████████████████████ +info "Passo 11/12: Configurando PM2..." + +# Para processos anteriores (se existirem) +pm2 delete chatc2 2>/dev/null || true + +# Inicia a aplicacao com PM2 como root (script ja exige root) +cd "$CHATC2_DIR" +if ! pm2 start src/server.js --name chatc2 --max-memory-restart 512M --time 2>&1; then + err "Falha ao iniciar a aplicacao com PM2. Verifique os logs." +fi + +# Aguarda o servidor iniciar e verifica se esta ouvindo na porta 3000 +sleep 2 +if ss -tlnp | grep -q ':3000'; then + log "Servidor Node.js ouvindo na porta 3000" +else + warn "Servidor pode nao estar ouvindo na porta 3000. Verifique: pm2 logs chatc2" +fi + +# Salva a lista de processos e configura inicio automatico no boot +pm2 save 2>/dev/null || true +pm2 startup systemd -u root --hp /root 2>/dev/null || true + +log "PM2 configurado - aplicacao iniciara automaticamente no boot" + +# ████████████████████████████████████████████████████ +# PASSO 12: FIREWALL +# ████████████████████████████████████████████████████ +info "Passo 12/12: Configurando firewall..." +ufw allow 22/tcp +ufw allow 80/tcp +ufw allow 443/tcp +ufw allow 3000/tcp +ufw --force enable +log "Firewall configurado (portas: 22, 80, 443, 3000)" + +# ████████████████████████████████████████████████████ +# FINAL +# ████████████████████████████████████████████████████ +PUBLIC_IP=$(curl -s ifconfig.me 2>/dev/null || echo "") + +echo "" +echo "╔══════════════════════════════════════════════════╗" +echo "║ Chatc2 - Instalacao Concluida! ║" +echo "╚══════════════════════════════════════════════════╝" +echo "" +echo " 📍 Acessar: http://$PUBLIC_IP" +echo " 📁 Diretorio: $CHATC2_DIR" +echo " 👤 Usuario: $CHATC2_USER" +echo " 🗄️ Firebird: senha: masterkey" +echo " 🎤 Whisper: modelo $WHISPER_MODEL" +echo "" +echo " 📋 Proximos passos:" +echo " 1. Copie seus bancos .FDB para $CHATC2_DIR/db/" +echo " 2. Edite $CHATC2_DIR/.env se necessario" +echo " 3. Crie usuario admin: node $CHATC2_DIR/scripts/gerar-token-usuario.js" +echo " 4. Configure o webhook na Evolution API:" +echo " POST http://$PUBLIC_IP/api/webhook/evolution" +echo " 5. Logs: pm2 logs chatc2" +echo " 6. Parar: pm2 stop chatc2" +echo " 7. Reset: pm2 restart chatc2" +echo "" diff --git a/deploy-linux/patch_node_firebird.py b/deploy-linux/patch_node_firebird.py new file mode 100644 index 0000000..da1f668 --- /dev/null +++ b/deploy-linux/patch_node_firebird.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +""" +patch_node_firebird.py - Aplica patches de compatibilidade node-firebird + +Patches aplicados: + 1. SRP empty buffer (Firebird 3.0 Legacy_Auth) + 2. Correcao de sintaxe para Node.js v22+ (colchete faltante) + +Uso: python3 patch_node_firebird.py +Ex: python3 patch_node_firebird.py /home/chatc2/chatc2 +""" +import sys +import os + +def patch_srp(project_dir): + """Patch 1: compatibilidade SRP com Firebird 3.0 Legacy_Auth""" + filepath = os.path.join(project_dir, 'node_modules', + 'node-firebird', 'lib', 'wire', 'connection.js') + if not os.path.isfile(filepath): + print(" [ERRO] Arquivo connection.js nao encontrado") + return False + + with open(filepath, 'r') as f: + content = f.read() + + if "No auth data from server" in content: + print(" [OK] Patch 1 (SRP) ja aplicado") + return True + + old = ( + ' // 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);' + ) + + new = ( + ' // 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 old in content: + content = content.replace(old, new, 1) + with open(filepath, 'w') as f: + f.write(content) + print(" [OK] Patch 1 (SRP) aplicado") + return True + else: + print(" [AVISO] Patch 1 (SRP) nao encontrado - codigo ja modificado") + return True # nao e erro + + +def patch_syntax(project_dir): + """Patch 2: colchete faltante (Node.js v22 nao tolera essa sintaxe)""" + filepath = os.path.join(project_dir, 'node_modules', + 'node-firebird', 'lib', 'wire', 'connection.js') + if not os.path.isfile(filepath): + return False + + with open(filepath, 'r') as f: + content = f.read() + + # Verifica se ja foi corrigido + if "fecha o else do if" in content: + print(" [OK] Patch 2 (sintaxe) ja aplicado") + return True + + # O erro: else block do if(!d||!d.buffer) nao tem chave de fechamento + # Linha original termina com: accept.sessionKey = proof.clientSessionKey; + # Linha seguinte: } else if (accept.pluginName === Const.AUTH_PLUGIN_LEGACY) + # Precisa de um } extra entre elas para fechar o else block + old = ( + ' accept.authData = proof.authData.toString(16);\n' + ' accept.sessionKey = proof.clientSessionKey;\n' + ' } else if (accept.pluginName === Const.AUTH_PLUGIN_LEGACY) {' + ) + new = ( + ' 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 old in content: + content = content.replace(old, new, 1) + with open(filepath, 'w') as f: + f.write(content) + print(" [OK] Patch 2 (sintaxe Node22) aplicado") + return True + else: + print(" [AVISO] Patch 2 (sintaxe) - padrao nao encontrado") + return True + + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Uso: python3 patch_node_firebird.py ") + sys.exit(1) + + project_dir = sys.argv[1] + print(f" Aplicando patches em: {project_dir}") + + r1 = patch_srp(project_dir) + r2 = patch_syntax(project_dir) + + if r1 and r2: + print(" Patches aplicados com sucesso") + sys.exit(0) + else: + print(" Alguns patches falharam") + sys.exit(1) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..083f0a8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1328 @@ +{ + "name": "chatc2", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chatc2", + "version": "2.0.0", + "license": "ISC", + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^16.4.7", + "express": "^5.2.1", + "ffmpeg-static": "^5.3.0", + "jsonwebtoken": "^9.0.3", + "node-firebird": "^2.3.1", + "pg": "^8.21.0" + } + }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "license": "MIT", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ffmpeg-static": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz", + "integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==", + "hasInstallScript": true, + "license": "GPL-3.0-or-later", + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "license": "MIT", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-firebird": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/node-firebird/-/node-firebird-2.3.1.tgz", + "integrity": "sha512-3yTwiLHwgtLYRv+aS4frzsQEwHMDXkP5Z1SPM/W103TvMemVRoCIaxPYO7axnUrKhgTfPHGXzqC5umYlQail/w==", + "license": "MPL-2.0" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "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", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "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 new file mode 100644 index 0000000..0309484 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "chatc2", + "version": "2.0.0", + "description": "API REST multi-banco (PostgreSQL + Firebird) para a plataforma de atendimento Chatc2", + "main": "src/server.js", + "scripts": { + "start": "node src/server.js", + "dev": "node --watch src/server.js" + }, + "keywords": [ + "postgres", + "firebird", + "api", + "express" + ], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^16.4.7", + "express": "^5.2.1", + "ffmpeg-static": "^5.3.0", + "jsonwebtoken": "^9.0.3", + "node-firebird": "^2.3.1", + "pg": "^8.21.0" + } +} diff --git a/scripts/definir-senha-web.js b/scripts/definir-senha-web.js new file mode 100644 index 0000000..d8e9fa7 --- /dev/null +++ b/scripts/definir-senha-web.js @@ -0,0 +1,59 @@ +/** + * Script para definir/alterar a senha web de um usuário. + * Uso: node scripts/definir-senha-web.js [alias] + * + * @param {string} LOGIN - Login do usuário + * @param {string} SENHA - Nova senha (máx. 30 caracteres) + * @param {string} [alias=lajedo] - Alias do banco de dados + */ +const db = require('../src/database'); + +async function main() { + const login = process.argv[2]; + const senha = process.argv[3]; + const alias = process.argv[4] || 'lajedo'; + + if (!login || !senha) { + console.log('Uso: node scripts/definir-senha-web.js [alias]'); + console.log('Ex: node scripts/definir-senha-web.js SUPORTE 123456 lajedo'); + process.exit(1); + } + + if (senha.length > 30) { + console.log('❌ A senha deve ter no máximo 30 caracteres.'); + process.exit(1); + } + + // Verifica se o usuário existe + const users = await db.query(alias, + 'SELECT USU_CODIGO_ID, USU_NOME, USU_LOGIN, USU_ACESSO_WEB FROM USUARIOS WHERE USU_LOGIN = ?', + [login] + ); + + if (users.length === 0) { + console.log(`❌ Usuário "${login}" não encontrado no alias "${alias}".`); + process.exit(1); + } + + const user = users[0]; + + // Atualiza a senha web + await db.execute(alias, + 'UPDATE USUARIOS SET USU_SENHA_WEB = ? WHERE USU_CODIGO_ID = ?', + [senha, user.USU_CODIGO_ID] + ); + + console.log(`✅ Senha web do usuário "${login}" definida com sucesso! (alias: ${alias})`); + + if (user.USU_ACESSO_WEB !== 1) { + console.log('⚠️ O usuário ainda não tem acesso WEB habilitado.'); + console.log(` Execute: node scripts/habilitar-acesso-web.js ${login} ${alias}`); + } + + process.exit(0); +} + +main().catch(err => { + console.error('Erro:', err.message); + process.exit(1); +}); diff --git a/scripts/gerar-token-jwt.js b/scripts/gerar-token-jwt.js new file mode 100644 index 0000000..3ab271c --- /dev/null +++ b/scripts/gerar-token-jwt.js @@ -0,0 +1,130 @@ +/** + * Gera um token JWT para um usuário em uma empresa específica. + * O token é armazenado no campo USU_TOKEN da tabela USUARIOS. + * + * Uso: node scripts/gerar-token-jwt.js [alias] + * + * Exemplos: + * node scripts/gerar-token-jwt.js SUPORTE 1 + * node scripts/gerar-token-jwt.js SUPORTE 1 lajedo + */ +const jwt = require('jsonwebtoken'); +const db = require('../src/database'); +const authConfig = require('../src/config/auth'); + +async function main() { + const login = process.argv[2]; + const empresaId = parseInt(process.argv[3], 10); + const alias = process.argv[4] || 'lajedo'; + + if (!login || !empresaId) { + console.log('Uso: node scripts/gerar-token-jwt.js [alias]'); + console.log('Ex: node scripts/gerar-token-jwt.js SUPORTE 1 lajedo'); + process.exit(1); + } + + // 1. Busca o usuário pelo login + const users = await db.query(alias, + `SELECT USU_CODIGO_ID, USU_NOME, USU_LOGIN, USU_EMAIL, USU_STATUS, + USU_ACESSO_WEB, USU_TIPO, USU_TOKEN + FROM USUARIOS WHERE USU_LOGIN = ?`, + [login] + ); + + if (users.length === 0) { + console.log(`❌ Usuário "${login}" não encontrado no alias "${alias}".`); + process.exit(1); + } + + const user = users[0]; + + // 2. Verifica se o usuário tem acesso à empresa + const empresas = await db.query(alias, + `SELECT USE_EMPRESA_ID FROM USUARIOS_EMPRESA + WHERE USE_USUARIO_ID = ? AND USE_EMPRESA_ID = ?`, + [user.USU_CODIGO_ID, empresaId] + ); + + if (empresas.length === 0) { + // Lista as empresas que o usuário tem acesso + const allEmpresas = await db.query(alias, + `SELECT USE_EMPRESA_ID FROM USUARIOS_EMPRESA + WHERE USE_USUARIO_ID = ?`, + [user.USU_CODIGO_ID] + ); + const empresasList = allEmpresas.map(e => e.USE_EMPRESA_ID).join(', '); + + console.log(`❌ Usuário "${login}" não tem acesso à empresa ${empresaId}.`); + console.log(` Empresas disponíveis para este usuário: ${empresasList || 'nenhuma'}`); + process.exit(1); + } + + // 3. Verifica status e permissão web + const statusOk = user.USU_STATUS === 'A'; + const webOk = user.USU_ACESSO_WEB === 1; + + if (!statusOk) { + console.log(`❌ Usuário "${login}" não está ativo (status: ${user.USU_STATUS}).`); + process.exit(1); + } + + if (!webOk) { + console.log(`⚠️ Usuário "${login}" não tem acesso WEB habilitado.`); + console.log(` Execute: node scripts/habilitar-acesso-web.js ${login} ${alias}`); + // Não impede de gerar o token, mas avisa + } + + // 4. Gera o payload do token + const payload = { + id: user.USU_CODIGO_ID, + empresaId: empresaId, + nome: user.USU_NOME.trim(), + login: user.USU_LOGIN.trim(), + email: user.USU_EMAIL ? user.USU_EMAIL.trim() : null, + tipo: user.USU_TIPO ? user.USU_TIPO.trim() : null, + alias: alias, + }; + + // 5. Gera o token JWT + const token = jwt.sign(payload, authConfig.secret, { + expiresIn: authConfig.expiresIn, + issuer: authConfig.issuer, + }); + + // 6. Armazena no campo USU_TOKEN + await db.execute(alias, + 'UPDATE USUARIOS SET USU_TOKEN = ? WHERE USU_CODIGO_ID = ?', + [token, user.USU_CODIGO_ID] + ); + + // 7. Exibe o resultado + console.log(''); + console.log('='.repeat(70)); + console.log(' ✅ TOKEN JWT GERADO COM SUCESSO'); + console.log('='.repeat(70)); + console.log(''); + console.log(` Alias: ${alias}`); + console.log(` Usuário: ${user.USU_NOME.trim()} (${user.USU_LOGIN.trim()})`); + console.log(` ID Usuário: ${user.USU_CODIGO_ID}`); + console.log(` Empresa: ${empresaId}`); + console.log(` Status: ${statusOk ? '✅ Ativo' : '❌ Inativo'}`); + console.log(` Acesso Web: ${webOk ? '✅ Sim' : '❌ Não'}`); + console.log(''); + console.log(' ┌─ TOKEN ─────────────────────────────────────────────────┐'); + console.log(` │ ${token}`); + console.log(' └─────────────────────────────────────────────────────────┘'); + console.log(''); + console.log(' Expira em: ' + authConfig.expiresIn); + console.log(''); + console.log(' 📌 Headers para usar nas requisições:'); + console.log(' Authorization: Bearer ' + token.substring(0, 50) + '...'); + console.log(' X-Usu-Token: ' + token.substring(0, 50) + '...'); + console.log(''); + + process.exit(0); +} + +main().catch(err => { + console.error('Erro:', err.message); + process.exit(1); +}); diff --git a/scripts/gerar-token-usuario.js b/scripts/gerar-token-usuario.js new file mode 100644 index 0000000..583dfec --- /dev/null +++ b/scripts/gerar-token-usuario.js @@ -0,0 +1,67 @@ +/** + * Script para gerar/renovar o USU_TOKEN de um usuário. + * Uso: node scripts/gerar-token-usuario.js [alias] + * + * Cada usuário recebe um token único (48 caracteres hex) armazenado + * no campo USU_TOKEN, que pode ser usado para autenticação na API + * via header X-Usu-Token. + * + * @param {string} LOGIN - Login do usuário + * @param {string} [alias=lajedo] - Alias do banco de dados + */ +const crypto = require('crypto'); +const db = require('../src/database'); + +async function main() { + const login = process.argv[2]; + const alias = process.argv[3] || 'lajedo'; + + if (!login) { + console.log('Uso: node scripts/gerar-token-usuario.js [alias]'); + console.log('Ex: node scripts/gerar-token-usuario.js SUPORTE lajedo'); + process.exit(1); + } + + // Verifica se o usuário existe + const users = await db.query(alias, + `SELECT USU_CODIGO_ID, USU_NOME, USU_LOGIN, USU_STATUS, USU_ACESSO_WEB + FROM USUARIOS WHERE USU_LOGIN = ?`, + [login] + ); + + if (users.length === 0) { + console.log(`❌ Usuário "${login}" não encontrado no alias "${alias}".`); + process.exit(1); + } + + const user = users[0]; + + // Gera um token único (48 caracteres hex) + const token = crypto.randomBytes(24).toString('hex'); + + // Armazena no banco + await db.execute(alias, + 'UPDATE USUARIOS SET USU_TOKEN = ? WHERE USU_CODIGO_ID = ?', + [token, user.USU_CODIGO_ID] + ); + + console.log('=== Token gerado com sucesso! ==='); + console.log(`Alias: ${alias}`); + console.log(`Usuário: ${user.USU_NOME.trim()} (${user.USU_LOGIN.trim()})`); + console.log(`ID: ${user.USU_CODIGO_ID}`); + console.log(`Status: ${user.USU_STATUS === 'A' ? '✅ Ativo' : '❌ Inativo'}`); + console.log(`Acesso Web: ${user.USU_ACESSO_WEB === 1 ? '✅ Sim' : '❌ Não'}`); + console.log(`\n🔑 USU_TOKEN:`); + console.log(`${token}`); + console.log(`\n📌 Use no header das requisições:`); + console.log(`X-Usu-Token: ${token}`); + console.log(`ou`); + console.log(`Authorization: Bearer ${token}`); + + process.exit(0); +} + +main().catch(err => { + console.error('Erro:', err.message); + process.exit(1); +}); diff --git a/scripts/habilitar-acesso-web.js b/scripts/habilitar-acesso-web.js new file mode 100644 index 0000000..3d463f1 --- /dev/null +++ b/scripts/habilitar-acesso-web.js @@ -0,0 +1,63 @@ +/** + * Script para habilitar acesso WEB a um usuário existente. + * Uso: node scripts/habilitar-acesso-web.js [alias] + * + * @param {string} LOGIN - Login do usuário + * @param {string} [alias=lajedo] - Alias do banco de dados + */ +const db = require('../src/database'); + +async function main() { + const login = process.argv[2]; + const alias = process.argv[3] || 'lajedo'; + + if (!login) { + console.log('Uso: node scripts/habilitar-acesso-web.js [alias]'); + console.log('Ex: node scripts/habilitar-acesso-web.js SUPORTE lajedo'); + process.exit(1); + } + + // Verifica se o usuário existe + const users = await db.query(alias, + 'SELECT USU_CODIGO_ID, USU_NOME, USU_LOGIN, USU_ACESSO_WEB, USU_STATUS, USU_SENHA_WEB FROM USUARIOS WHERE USU_LOGIN = ?', + [login] + ); + + if (users.length === 0) { + console.log(`❌ Usuário "${login}" não encontrado no alias "${alias}".`); + process.exit(1); + } + + const user = users[0]; + + console.log('=== Usuário encontrado ==='); + console.log(`Alias: ${alias}`); + console.log(`ID: ${user.USU_CODIGO_ID}`); + console.log(`Nome: ${user.USU_NOME.trim()}`); + console.log(`Login: ${user.USU_LOGIN.trim()}`); + console.log(`Acesso Web: ${user.USU_ACESSO_WEB === 1 ? '✅ Sim' : '❌ Não'}`); + console.log(`Status: ${user.USU_STATUS === 'A' ? '✅ Ativo' : '❌ Inativo'}`); + console.log(`Senha Web: ${user.USU_SENHA_WEB || '(vazia)'}`); + + // Habilita acesso web + await db.execute(alias, + 'UPDATE USUARIOS SET USU_ACESSO_WEB = 1 WHERE USU_CODIGO_ID = ?', + [user.USU_CODIGO_ID] + ); + + console.log('\n✅ Acesso WEB habilitado com sucesso!'); + console.log(` Agora o usuário "${login}" pode usar o app.`); + + if (!user.USU_SENHA_WEB) { + console.log('\n⚠️ ATENÇÃO: O usuário não possui senha web (USU_SENHA_WEB).'); + console.log(' Defina uma senha para que o login funcione.'); + console.log(' Ex: node scripts/definir-senha-web.js SUPORTE 123456 ' + alias); + } + + process.exit(0); +} + +main().catch(err => { + console.error('Erro:', err.message); + process.exit(1); +}); diff --git a/scripts/listar-tokens.js b/scripts/listar-tokens.js new file mode 100644 index 0000000..a5f195f --- /dev/null +++ b/scripts/listar-tokens.js @@ -0,0 +1,55 @@ +/** + * Lista todos os usuários com seus respectivos USU_TOKEN. + * Uso: node scripts/listar-tokens.js [LOGIN] [alias] + * + * @param {string} [LOGIN] - Filtrar por login (opcional) + * @param {string} [alias=lajedo] - Alias do banco de dados + */ +const db = require('../src/database'); + +async function main() { + const filtro = process.argv[2]; + const alias = process.argv[3] || 'lajedo'; + + const sql = filtro + ? `SELECT USU_CODIGO_ID, USU_NOME, USU_LOGIN, USU_STATUS, + USU_ACESSO_WEB, USU_TOKEN + FROM USUARIOS WHERE USU_LOGIN = ?` + : `SELECT USU_CODIGO_ID, USU_NOME, USU_LOGIN, USU_STATUS, + USU_ACESSO_WEB, USU_TOKEN + FROM USUARIOS`; + + const users = await db.query(alias, sql, filtro ? [filtro] : []); + + if (users.length === 0) { + console.log(`Nenhum usuário encontrado no alias "${alias}".`); + process.exit(0); + } + + console.log('='.repeat(80)); + console.log(` USUÁRIOS E TOKENS (alias: ${alias})`); + console.log('='.repeat(80)); + console.log(''); + + users.forEach((u, i) => { + const status = u.USU_STATUS?.trim() === 'A' ? '✅' : '❌'; + const web = u.USU_ACESSO_WEB === 1 ? '✅' : '❌'; + const token = u.USU_TOKEN?.trim(); + + console.log(`${i + 1}. ${u.USU_NOME.trim()} (${u.USU_LOGIN.trim()})`); + console.log(` ID: ${u.USU_CODIGO_ID} | Status: ${status} | Acesso Web: ${web}`); + console.log(` USU_TOKEN: ${token || '(vazio)'}`); + console.log(''); + }); + + console.log('='.repeat(80)); + console.log(`Total: ${users.length} usuário(s)`); + console.log('='.repeat(80)); + + process.exit(0); +} + +main().catch(err => { + console.error('Erro:', err.message); + process.exit(1); +}); diff --git a/scripts/migracoes.js b/scripts/migracoes.js new file mode 100644 index 0000000..a18988f --- /dev/null +++ b/scripts/migracoes.js @@ -0,0 +1,490 @@ +/** + * ARQUIVO DE MIGRAÇÕES DO BANCO DE DADOS + * ======================================== + * + * Este script contém TODAS as alterações estruturais feitas no banco de dados + * desde a criação inicial do projeto. + * + * USO: + * node scripts/migracoes.js [alias] + * + * Exemplo: + * node scripts/migracoes.js lajedo + * node scripts/migracoes.js novo + * + * IMPORTANTE: + * - Cada migração tem um ID único e só executa uma vez + * - O controle é feito via tabela CHATC2_CONTROLE_MIGRACOES + * - Para adicionar NOVA migração, adicione um novo objeto no array MIGRACOES + * - Nunca remova ou altere migrações já existentes (apenas adicione novas) + */ + +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'; + +// ============================================================ +// CONTROLE DE MIGRAÇÕES +// ============================================================ +async function garantirControle() { + try { + await db.execute(alias, ` + CREATE TABLE CHATC2_CONTROLE_MIGRACOES ( + MIG_ID INTEGER NOT NULL PRIMARY KEY, + MIG_DESCRICAO VARCHAR(200), + MIG_DATA_EXECUCAO TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + MIG_STATUS CHAR(1) DEFAULT 'A' + ) + `); + console.log('✅ Tabela CHATC2_CONTROLE_MIGRACOES criada'); + } catch(e) { + if (!e.message.includes('already exists')) { + console.log('⚠️ CHATC2_CONTROLE_MIGRACOES:', e.message.substring(0, 100)); + } + } +} + +async function migracaoJaExecutada(id) { + try { + var r = await db.query(alias, + 'SELECT COUNT(*) AS CT FROM CHATC2_CONTROLE_MIGRACOES WHERE MIG_ID = ?', [id]); + return r[0]?.CT > 0; + } catch(e) { + return false; + } +} + +async function registrarMigracao(id, descricao) { + try { + await db.execute(alias, + 'INSERT INTO CHATC2_CONTROLE_MIGRACOES (MIG_ID, MIG_DESCRICAO) VALUES (?, ?)', + [id, descricao]); + } catch(e) { + console.log('⚠️ Erro ao registrar migração:', e.message.substring(0, 80)); + } +} + +// ============================================================ +// LISTA DE MIGRAÇÕES +// ============================================================ +// SEMPRE adicione NOVAS migrações no FINAL do array, com ID sequencial. +// NUNCA remova ou modifique migrações já existentes. + +const MIGRACOES = [ + + // ---------------------------------------------------------- + // MIGRAÇÃO 1: Campo USU_TIPO_CHAT em USUARIOS + // ---------------------------------------------------------- + { + id: 1, + descricao: 'Adicionar USU_TIPO_CHAT em USUARIOS (A=Atendente, G=Gerente)', + sql: [ + `ALTER TABLE USUARIOS ADD USU_TIPO_CHAT CHAR(1) DEFAULT 'A'`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 2: Tabela CHATC2_INSTANCIAS (Evolution API) + // ---------------------------------------------------------- + { + id: 2, + descricao: 'Criar tabela CHATC2_INSTANCIAS para conexões WhatsApp', + sql: [ + `CREATE TABLE CHATC2_INSTANCIAS ( + INS_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, + INS_EMPRESA_ID INTEGER NOT NULL, + INS_NOME VARCHAR(100), + INS_URL VARCHAR(500), + INS_API_KEY VARCHAR(500), + INS_INSTANCE_NAME VARCHAR(100), + INS_STATUS CHAR(1) DEFAULT 'D', + INS_QR_CODE BLOB SUB_TYPE 0, + INS_DT_CONEXAO TIMESTAMP, + INS_DT_CADASTRO TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INS_SITUACAO CHAR(1) DEFAULT 'A' + )`, + `CREATE SEQUENCE GEN_INSTANCIAS`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 3: Tabela CHATC2_EQUIPES + // ---------------------------------------------------------- + { + id: 3, + descricao: 'Criar tabela CHATC2_EQUIPES', + sql: [ + `CREATE TABLE CHATC2_EQUIPES ( + EQU_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, + EQU_EMPRESA_ID INTEGER NOT NULL, + EQU_NOME VARCHAR(100), + EQU_ORDEM INTEGER DEFAULT 0, + EQU_SITUACAO CHAR(1) DEFAULT 'A' + )`, + `CREATE SEQUENCE GEN_EQUIPES`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 4: Tabela CHATC2_USU_EQUIPES + // ---------------------------------------------------------- + { + id: 4, + descricao: 'Criar tabela CHATC2_USU_EQUIPES (relação N:N)', + sql: [ + `CREATE TABLE CHATC2_USU_EQUIPES ( + EQU_EQUIPE_ID INTEGER NOT NULL, + EQU_USUARIO_ID INTEGER NOT NULL, + PRIMARY KEY (EQU_EQUIPE_ID, EQU_USUARIO_ID) + )`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 5: Tabela CHATC2_ETIQUETAS + // ---------------------------------------------------------- + { + id: 5, + descricao: 'Criar tabela CHATC2_ETIQUETAS', + sql: [ + `CREATE TABLE CHATC2_ETIQUETAS ( + ETI_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, + ETI_EMPRESA_ID INTEGER NOT NULL, + ETI_NOME VARCHAR(50), + ETI_COR VARCHAR(7) DEFAULT '#667eea', + ETI_SITUACAO CHAR(1) DEFAULT 'A' + )`, + `CREATE SEQUENCE GEN_ETIQUETAS`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 6: Tabela CHATC2_CONVERSAS + // ---------------------------------------------------------- + { + id: 6, + descricao: 'Criar tabela CHATC2_CONVERSAS', + sql: [ + `CREATE TABLE CHATC2_CONVERSAS ( + CON_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, + CON_EMPRESA_ID INTEGER NOT NULL, + CON_INSTANCIA_ID INTEGER, + CON_CLIENTE_ID INTEGER, + CON_NUMERO VARCHAR(20), + CON_NOME_CONTATO VARCHAR(100), + CON_STATUS CHAR(1) DEFAULT 'E', + CON_USUARIO_ID INTEGER, + CON_EQUIPE_ID INTEGER, + CON_DT_INICIO TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CON_DT_FINAL TIMESTAMP, + CON_DT_ULTIMA_MSG TIMESTAMP, + CON_SAUDACAO_ENVIADA CHAR(1) DEFAULT 'N', + CON_CSAT_ENVIADO CHAR(1) DEFAULT 'N', + CON_PRIMEIRA_MSG TIMESTAMP, + CON_ORIGEM VARCHAR(20) DEFAULT 'whatsapp', + CON_SITUACAO CHAR(1) DEFAULT 'A' + )`, + `CREATE SEQUENCE GEN_CONVERSAS`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 7: Tabela CHATC2_CONVERSAS_MENSAGENS + // ---------------------------------------------------------- + { + id: 7, + descricao: 'Criar tabela CHATC2_CONVERSAS_MENSAGENS', + sql: [ + `CREATE TABLE CHATC2_CONVERSAS_MENSAGENS ( + CME_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, + CME_CONVERSA_ID INTEGER NOT NULL, + CME_REMETENTE CHAR(1) DEFAULT 'C', + CME_USUARIO_ID INTEGER, + CME_MENSAGEM BLOB SUB_TYPE 1, + CME_TEXTO VARCHAR(4000), + CME_TIPO VARCHAR(20) DEFAULT 'text', + CME_DT_ENVIO TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CME_PRIVADA CHAR(1) DEFAULT 'N', + CME_LIDA CHAR(1) DEFAULT 'N', + CME_MIDIA_ID INTEGER, + CME_SITUACAO CHAR(1) DEFAULT 'A' + )`, + `CREATE SEQUENCE GEN_CONVERSAS_MENSAGENS`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 8: Tabela CHATC2_MENSAGENS_ATENDIMENTOS (blob mídias) + // ---------------------------------------------------------- + { + id: 8, + descricao: 'Criar tabela CHATC2_MENSAGENS_ATENDIMENTOS para mídias', + sql: [ + `CREATE TABLE CHATC2_MENSAGENS_ATENDIMENTOS ( + MAT_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, + MAT_CONVERSA_ID INTEGER, + MAT_MENSAGEM_ID INTEGER, + MAT_NOME_ARQUIVO VARCHAR(255), + MAT_TIPO_ARQUIVO VARCHAR(50), + MAT_ARQUIVO BLOB SUB_TYPE 0, + MAT_DT_CADASTRO TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + MAT_SITUACAO CHAR(1) DEFAULT 'A' + )`, + `CREATE SEQUENCE GEN_MENSAGENS_ATENDIMENTO`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 9: Tabela CHATC2_CONVERSAS_ETIQUETAS + // ---------------------------------------------------------- + { + id: 9, + descricao: 'Criar tabela CHATC2_CONVERSAS_ETIQUETAS', + sql: [ + `CREATE TABLE CHATC2_CONVERSAS_ETIQUETAS ( + CET_CONVERSA_ID INTEGER NOT NULL, + CET_ETIQUETA_ID INTEGER NOT NULL, + PRIMARY KEY (CET_CONVERSA_ID, CET_ETIQUETA_ID) + )`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 10: Tabela CHATC2_CSAT_AVALIACOES + // ---------------------------------------------------------- + { + id: 10, + descricao: 'Criar tabela CHATC2_CSAT_AVALIACOES', + sql: [ + `CREATE TABLE CHATC2_CSAT_AVALIACOES ( + CSA_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, + CSA_CONVERSA_ID INTEGER, + CSA_EMPRESA_ID INTEGER, + CSA_CLIENTE_ID INTEGER, + CSA_NOTA INTEGER, + CSA_COMENTARIO VARCHAR(500), + CSA_DT_AVALIACAO TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )`, + `CREATE SEQUENCE GEN_CSAT_AVALIACOES`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 11: Tabela CHATC2_CONFIGURACOES_EMPRESA (base) + // ---------------------------------------------------------- + { + id: 11, + descricao: 'Criar tabela CHATC2_CONFIGURACOES_EMPRESA (base)', + sql: [ + `CREATE TABLE CHATC2_CONFIGURACOES_EMPRESA ( + CFE_EMPRESA_ID INTEGER NOT NULL PRIMARY KEY, + CFE_INSTANCIA_PADRAO_ID INTEGER, + CFE_FOTO_CELULAR CHAR(1) DEFAULT 'N', + CFE_SAUDACAO_ATIVA CHAR(1) DEFAULT 'S', + CFE_SAUDACAO_MENSAGEM VARCHAR(500), + CFE_CSAT_ATIVO CHAR(1) DEFAULT 'N', + CFE_CSAT_MENSAGEM VARCHAR(500) DEFAULT 'Avalie seu atendimento de 1 a 5 estrelas' + )`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 12: EQU_ORDEM em CHATC2_EQUIPES + // ---------------------------------------------------------- + { + id: 12, + descricao: 'Adicionar EQU_ORDEM em CHATC2_EQUIPES (ordenação)', + sql: [ + `ALTER TABLE CHATC2_EQUIPES ADD EQU_ORDEM INTEGER DEFAULT 0`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 13: Campos de triagem em CHATC2_CONFIGURACOES_EMPRESA + // ---------------------------------------------------------- + { + id: 13, + descricao: 'Adicionar campos de triagem em CHATC2_CONFIGURACOES_EMPRESA', + sql: [ + `ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_ENVIAR_NOME_USUARIO CHAR(1) DEFAULT 'N'`, + `ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_TRIAGEM_ATIVA CHAR(1) DEFAULT 'N'`, + `ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_TRIAGEM_MSG_WELCOME VARCHAR(500)`, + `ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_TRIAGEM_MSG_AFTER VARCHAR(500)`, + `ALTER TABLE CHATC2_CONFIGURACOES_EMPRESA ADD CFE_TRIAGEM_BOLETO_NUMERO VARCHAR(10) DEFAULT '0'`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 14: CON_MENU_ESTADO em CHATC2_CONVERSAS + // ---------------------------------------------------------- + { + id: 14, + descricao: 'Adicionar CON_MENU_ESTADO em CHATC2_CONVERSAS (navegação triagem)', + sql: [ + `ALTER TABLE CHATC2_CONVERSAS ADD CON_MENU_ESTADO VARCHAR(100)`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 15: CON_ETIQUETAS_DESC em CHATC2_CONVERSAS + // ---------------------------------------------------------- + { + id: 15, + descricao: 'Adicionar CON_ETIQUETAS_DESC em CHATC2_CONVERSAS (estado legado)', + sql: [ + `ALTER TABLE CHATC2_CONVERSAS ADD CON_ETIQUETAS_DESC VARCHAR(500)`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 16: Tabela CHATC2_MENUS_EMPRESA (fluxo personalizado) + // ---------------------------------------------------------- + { + id: 16, + descricao: 'Criar tabela CHATC2_MENUS_EMPRESA para fluxo de atendimento personalizado', + sql: [ + `CREATE TABLE CHATC2_MENUS_EMPRESA ( + MNE_CODIGO_ID INTEGER NOT NULL PRIMARY KEY, + MNE_EMPRESA_ID INTEGER, + MNE_EQUIPE_ID INTEGER, + MNE_MENU_PAI_ID INTEGER, + MNE_ORDEM INTEGER DEFAULT 0, + MNE_TITULO VARCHAR(100), + MNE_TIPO CHAR(1) DEFAULT 'M', + MNE_TEXTO BLOB SUB_TYPE TEXT, + MNE_ACAO_ROTA VARCHAR(100), + MNE_ACAO_METODO VARCHAR(10), + MNE_ACAO_PROMPT VARCHAR(300), + MNE_SITUACAO CHAR(1) DEFAULT 'A' + )`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 17: CON_USUARIO_NOME, CON_EQUIPE_NOME, CON_ETIQUETAS_DESC em CHATC2_CONVERSAS + // ---------------------------------------------------------- + { + id: 17, + descricao: 'Adicionar CON_USUARIO_NOME, CON_EQUIPE_NOME, CON_ETIQUETAS_DESC em CHATC2_CONVERSAS', + sql: [ + `ALTER TABLE CHATC2_CONVERSAS ADD CON_USUARIO_NOME VARCHAR(100)`, + `ALTER TABLE CHATC2_CONVERSAS ADD CON_EQUIPE_NOME VARCHAR(100)`, + `ALTER TABLE CHATC2_CONVERSAS ADD CON_ETIQUETAS_DESC VARCHAR(500)`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 18: Etiquetas nos menus de fluxo + // ---------------------------------------------------------- + { + id: 18, + descricao: 'Adicionar MNE_ETIQUETA_IDS em CHATC2_MENUS_EMPRESA (etiquetas por etapa do fluxo)', + sql: [ + `ALTER TABLE CHATC2_MENUS_EMPRESA ADD MNE_ETIQUETA_IDS VARCHAR(200)`, + ], + }, + + // ---------------------------------------------------------- + // MIGRAÇÃO 19: Transcrição de áudio em CHATC2_MENSAGENS_ATENDIMENTOS + // ---------------------------------------------------------- + { + id: 19, + descricao: 'Adicionar MAT_TRANSCRICAO em CHATC2_MENSAGENS_ATENDIMENTOS (transcricao de audio)', + sql: [ + `ALTER TABLE CHATC2_MENSAGENS_ATENDIMENTOS ADD MAT_TRANSCRICAO BLOB SUB_TYPE TEXT`, + ], + }, + + // ============================================================ + // >>> ADICIONE NOVAS MIGRAÇÕES AQUI <<< + // ============================================================ + // + // Exemplo: + // { + // id: 19, + // descricao: 'Descrição clara do que esta migração faz', + // sql: [ + // `ALTER TABLE EXEMPLO ADD NOVO_CAMPO VARCHAR(100)`, + // `CREATE TABLE NOVA_TABELA ( ... )`, + // ], + // }, + // +]; + +// ============================================================ +// EXECUTOR DE MIGRAÇÕES +// ============================================================ +async function main() { + console.log(`\n=== MIGRAÇÕES DE BANCO DE DADOS ===`); + console.log(`Alias: ${alias}\n`); + + // Blindagem: a DDL aqui é Firebird. Se o alias for Postgres (banco externo, + // gerenciado fora), não há o que aplicar — sai sem erro. + let driver; + try { driver = db.driverOf(alias); } + 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'); + process.exit(0); + } + + await garantirControle(); + + var executadas = 0; + var pendentes = 0; + + for (var mig of MIGRACOES) { + var jaExecutou = await migracaoJaExecutada(mig.id); + + if (jaExecutou) { + console.log(`⏭️ [${mig.id}] ${mig.descricao} — já executada`); + continue; + } + + pendentes++; + console.log(`▶️ [${mig.id}] ${mig.descricao}...`); + + var erros = 0; + for (var cmd of mig.sql) { + try { + await db.execute(alias, cmd); + console.log(` ✅ ${cmd.substring(0, 80)}...`); + } catch(e) { + var msg = e.message; + // Firebird pode reportar "already exists" de várias formas + if (msg.includes('already exists') || msg.includes('Violation') || msg.includes('duplicate') || msg.includes('Unknow')) { + console.log(` ℹ️ Já existe: ${cmd.substring(0, 60)}...`); + } else { + console.log(` ❌ ${msg.substring(0, 120)}`); + erros++; + } + } + } + + if (erros === 0) { + await registrarMigracao(mig.id, mig.descricao); + executadas++; + console.log(` ✅ Migração ${mig.id} concluída\n`); + } else { + console.log(` ⚠️ Migração ${mig.id} concluída com ${erros} erro(s)\n`); + } + } + + console.log(`=== RESUMO ===`); + console.log(`Total de migrações: ${MIGRACOES.length}`); + console.log(`Executadas agora: ${executadas}`); + console.log(`Já executadas antes: ${MIGRACOES.length - pendentes}`); + console.log(`Pendentes (com erro): ${pendentes - executadas}`); + console.log(`\n✅ Finalizado!`); + process.exit(0); +} + +main().catch(function(err) { + console.error('❌ Erro fatal:', err.message); + process.exit(1); +}); diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..5c757ee --- /dev/null +++ b/src/app.js @@ -0,0 +1,109 @@ +const express = require('express'); +const cors = require('cors'); +const path = require('path'); +const routes = require('./routes'); +const authenticateToken = require('./middlewares/auth'); + +const app = express(); + +// Middlewares +// CORS: restringível via CORS_ORIGINS (lista separada por vírgula no .env). +// Requisições same-origin e server-to-server (sem header Origin) sempre passam. +const corsOrigins = (process.env.CORS_ORIGINS || [process.env.LOCAL_URL, process.env.EXTERNAL_URL].filter(Boolean).join(',')) + .split(',').map(s => s.trim()).filter(Boolean); +if (corsOrigins.length) { + app.use(cors({ + origin: function (origin, cb) { + if (!origin || corsOrigins.indexOf(origin) !== -1) return cb(null, true); + return cb(null, false); + }, + })); + console.log('🔒 CORS restrito a:', corsOrigins.join(', ')); +} else { + app.use(cors()); + console.log('⚠️ CORS aberto — defina CORS_ORIGINS no .env para restringir.'); +} + +// Limite de corpo por rota (anti-DoS): +// - rotas de mídia (envio de mensagem, webhook, evolution) → MAX_FILE_SIZE (grande) +// - demais rotas → JSON_LIMIT (pequeno), reduz superfície de ataque +const maxFileSize = process.env.MAX_FILE_SIZE || '50mb'; +const jsonPadrao = process.env.JSON_LIMIT || '2mb'; +console.log('📦 Limite de upload (mídia):', maxFileSize, '| padrão:', jsonPadrao); + +const bigJson = express.json({ limit: maxFileSize }); +const smallJson = express.json({ limit: jsonPadrao }); +const bigUrl = express.urlencoded({ extended: true, limit: maxFileSize }); +const smallUrl = express.urlencoded({ extended: true, limit: jsonPadrao }); + +function rotaComMidia(req) { + const p = req.path || ''; + return p.endsWith('/messages') || p.indexOf('/webhook/') !== -1 || p.indexOf('/evolution/') !== -1; +} +app.use((req, res, next) => (rotaComMidia(req) ? bigJson : smallJson)(req, res, next)); +app.use((req, res, next) => (rotaComMidia(req) ? bigUrl : smallUrl)(req, res, next)); + +// Static files +app.use(express.static(path.join(__dirname, 'public'))); + +// Log de requisições +app.use((req, res, next) => { + const start = Date.now(); + res.on('finish', () => { + const duration = Date.now() - start; + console.log(`${req.method} ${req.originalUrl} - ${res.statusCode} (${duration}ms)`); + }); + next(); +}); + +// Rotas (a autenticação é aplicada internamente em cada grupo de rotas) +app.use(routes); + +// Rota raiz +app.get('/', (req, res) => { + res.json({ + name: 'API Firebird - Chatc2', + version: '2.0.0', + description: 'API multi-banco com sistema de aliases', + auth: { + loginPage: 'GET /app/:alias/login', + login: 'POST /app/:alias/login', + myData: 'GET /app/:alias/me', + dashboard: 'GET /app/:alias/dashboard', + }, + api: { + aliases: 'GET /api/aliases', + health: 'GET /api/:alias/health', + tables: 'GET /api/:alias/tables', + tableInfo: 'GET /api/:alias/tables/:tableName', + query: 'POST /api/:alias/query', + execute: 'POST /api/:alias/execute', + }, + authentication: { + jwt: 'Authorization: Bearer ', + usu_token: 'X-Usu-Token: ', + }, + routes: 'GET /app/:alias/routes', + obs: 'Substitua :alias pelo nome do banco (ex: lajedo, alfenas, ...)', + }); +}); + +// Middleware de erro global — loga o detalhe internamente e responde genérico +// (evita vazar mensagens de erro do banco/SQL para o cliente) +app.use((err, req, res, next) => { + console.error('Erro não tratado:', err); + res.status(err.status || 500).json({ + success: false, + error: 'Erro interno do servidor.', + }); +}); + +// Middleware para rotas não encontradas +app.use((req, res) => { + res.status(404).json({ + success: false, + error: `Rota ${req.method} ${req.originalUrl} não encontrada`, + }); +}); + +module.exports = app; diff --git a/src/config/auth.js b/src/config/auth.js new file mode 100644 index 0000000..cd3739a --- /dev/null +++ b/src/config/auth.js @@ -0,0 +1,19 @@ +/** + * Configuração de autenticação JWT + */ +const secret = process.env.JWT_SECRET; + +// Segurança: sem segredo padrão. Se a variável não estiver definida, o boot +// falha de forma explícita — evita assinar tokens com um segredo previsível. +if (!secret) { + throw new Error( + 'JWT_SECRET não definido. Configure JWT_SECRET no .env antes de iniciar o servidor ' + + '(não há segredo padrão por motivos de segurança).' + ); +} + +module.exports = { + secret: secret, + expiresIn: process.env.JWT_EXPIRES_IN || '8h', + issuer: 'chatc2-api', +}; diff --git a/src/controllers/authController.js b/src/controllers/authController.js new file mode 100644 index 0000000..130d6e7 --- /dev/null +++ b/src/controllers/authController.js @@ -0,0 +1,185 @@ +const jwt = require('jsonwebtoken'); +const path = require('path'); +const db = require('../database'); +const authConfig = require('../config/auth'); + +// ── Rate limit de login (em memória) — proteção anti força bruta ── +const LOGIN_MAX = 10; // tentativas falhas permitidas +const LOGIN_JANELA_MS = 15 * 60 * 1000; // por janela de 15 minutos +const loginTentativas = new Map(); +function chaveLogin(req, login) { return (req.ip || '') + '|' + (login || ''); } +function loginBloqueado(req, login) { + const e = loginTentativas.get(chaveLogin(req, login)); + if (!e) return false; + if (Date.now() - e.inicio > LOGIN_JANELA_MS) { loginTentativas.delete(chaveLogin(req, login)); return false; } + return e.count >= LOGIN_MAX; +} +function registrarFalhaLogin(req, login) { + const k = chaveLogin(req, login); + const agora = Date.now(); + const e = loginTentativas.get(k); + if (!e || agora - e.inicio > LOGIN_JANELA_MS) loginTentativas.set(k, { count: 1, inicio: agora }); + else e.count++; +} +function limparTentativasLogin(req, login) { loginTentativas.delete(chaveLogin(req, login)); } + +class AuthController { + /** + * Exibe a página de login + * Endpoint: GET /app/:alias/login + */ + static async loginPage(req, res) { + const loginPath = path.resolve(__dirname, '../public/login.html'); + res.sendFile(loginPath); + } + + /** + * Login do usuário no aplicativo + * Endpoint: POST /app/:alias/login + * Body: { USU_LOGIN, USU_SENHA } + */ + static async login(req, res) { + try { + const { alias } = req.params; + const { USU_LOGIN, USU_SENHA } = req.body; + + // Validação dos campos obrigatórios + if (!USU_LOGIN || !USU_SENHA) { + return res.status(400).json({ + success: false, + error: 'Os campos USU_LOGIN e USU_SENHA são obrigatórios.', + }); + } + + if (loginBloqueado(req, USU_LOGIN)) { + return res.status(429).json({ + success: false, + error: 'Muitas tentativas de login. Aguarde alguns minutos e tente novamente.', + }); + } + + // Busca o usuário pelo login + const sql = ` + SELECT + USU_CODIGO_ID, + USU_NIVEL_ID, + USU_NOME, + USU_LOGIN, + USU_EMAIL, + USU_STATUS, + USU_ACESSO_WEB, + USU_CAMINHO_FOTO, + USU_DT_ULTIMO_ACESSO, + USU_TIPO, + USU_TIPO_CHAT + FROM USUARIOS + WHERE USU_LOGIN = ? + AND (USU_SENHA = ? OR USU_SENHA_WEB = ?) + AND COALESCE(USU_ACESSO_WEB, 0) = 1 + AND USU_STATUS = 'A' + `; + + const result = await db.query(alias, sql, [USU_LOGIN, USU_SENHA, USU_SENHA]); + + if (result.length === 0) { + registrarFalhaLogin(req, USU_LOGIN); + return res.status(401).json({ + success: false, + error: 'Credenciais inválidas ou usuário sem permissão de acesso.', + }); + } + + // Login bem-sucedido: zera o contador de tentativas + limparTentativasLogin(req, USU_LOGIN); + + const user = result[0]; + + // Busca as empresas que o usuário tem acesso + const empresas = await db.query(alias, + 'SELECT USE_EMPRESA_ID FROM USUARIOS_EMPRESA WHERE USE_USUARIO_ID = ?', + [user.USU_CODIGO_ID] + ); + const empresasIds = empresas.map(e => e.USE_EMPRESA_ID); + + // Atualiza a data do último acesso + await db.execute( + alias, + `UPDATE USUARIOS SET USU_DT_ULTIMO_ACESSO = CURRENT_DATE WHERE USU_CODIGO_ID = ?`, + [user.USU_CODIGO_ID] + ); + + // Dados do usuário para o token + const payload = { + id: user.USU_CODIGO_ID, + nivelId: user.USU_NIVEL_ID, + nome: user.USU_NOME.trim(), + login: user.USU_LOGIN.trim(), + email: user.USU_EMAIL ? user.USU_EMAIL.trim() : null, + tipo: user.USU_TIPO ? user.USU_TIPO.trim() : null, + tipoChat: user.USU_TIPO_CHAT ? user.USU_TIPO_CHAT.trim() : null, + alias, + empresas: empresasIds, // empresas que o usuário pode acessar + }; + + // Gera o token JWT + const token = jwt.sign(payload, authConfig.secret, { + expiresIn: authConfig.expiresIn, + issuer: authConfig.issuer, + }); + + // Retorna o token e dados do usuário + return res.json({ + success: true, + message: 'Login realizado com sucesso.', + alias, + token, + expiresIn: authConfig.expiresIn, + user: { + ...payload, + foto: user.USU_CAMINHO_FOTO ? user.USU_CAMINHO_FOTO.trim() : null, + ultimoAcesso: user.USU_DT_ULTIMO_ACESSO, + }, + }); + } catch (err) { + console.error('Erro no login:', err); + return res.status(500).json({ + success: false, + error: `Erro interno ao realizar login: ${err.message}`, + }); + } + } + + /** + * Valida o token atual e retorna dados atualizados do usuário + * Endpoint: GET /app/:alias/me + */ + static async me(req, res) { + try { + return res.json({ + success: true, + alias: req.params.alias, + user: { + ...req.user, + authType: req.authType || 'jwt', + }, + }); + } catch (err) { + console.error('Erro ao buscar usuário:', err); + return res.status(500).json({ + success: false, + error: `Erro ao buscar dados do usuário: ${err.message}`, + }); + } + } + + /** + * Dashboard do app + * Endpoint: GET /app/:alias/dashboard + */ + static async dashboard(req, res) { + const dashboardPath = path.resolve(__dirname, '../public/dashboard.html'); + res.sendFile(dashboardPath); + } +} + +module.exports = AuthController; diff --git a/src/controllers/chatController.js b/src/controllers/chatController.js new file mode 100644 index 0000000..e031f5f --- /dev/null +++ b/src/controllers/chatController.js @@ -0,0 +1,1329 @@ +const db = require('../database'); +const ffmpegPath = require('ffmpeg-static'); +const { execFile } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +/** + * Comprime vídeo para tamanho aceitável pelo WhatsApp/Evolution API + * Reduz bitrate e resolução se necessário (alvo: ~10MB) + * @param {Buffer} inputBuffer - Buffer do video original + * @returns {Promise} Buffer do video comprimido + */ +function compressVideo(inputBuffer) { + return new Promise(function(resolve, reject) { + var tmpDir = path.join(__dirname, '../../uploads/audio'); + if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true }); + + var inputPath = path.join(tmpDir, 'video_in_' + Date.now() + '.mp4'); + var outputPath = path.join(tmpDir, 'video_out_' + Date.now() + '.mp4'); + + fs.writeFileSync(inputPath, inputBuffer); + var inputSizeMB = (inputBuffer.length / (1024 * 1024)).toFixed(1); + + // Se o video ja tem <= 10MB, nao precisa comprimir muito + var targetBitrate = inputBuffer.length < 10 * 1024 * 1024 ? '1M' : '500k'; + console.log('[Video] Comprimindo video de ' + inputSizeMB + 'MB, bitrate alvo: ' + targetBitrate); + + execFile(ffmpegPath, [ + '-y', + '-i', inputPath, + '-c:v', 'libx264', + '-b:v', targetBitrate, + '-c:a', 'aac', + '-b:a', '64k', + '-vf', 'scale=720:-2', + '-movflags', '+faststart', + '-preset', 'fast', + '-maxrate', '1M', + '-bufsize', '2M', + outputPath + ], { timeout: 120000 }, function(err) { + try { fs.unlinkSync(inputPath); } catch(e) {} + + if (err) { + try { fs.unlinkSync(outputPath); } catch(e) {} + console.error('[Video] Erro na compressao:', err.message.substring(0, 100)); + return reject(err); + } + + var outputBuffer = fs.readFileSync(outputPath); + try { fs.unlinkSync(outputPath); } catch(e) {} + + var outputSizeMB = (outputBuffer.length / (1024 * 1024)).toFixed(1); + console.log('[Video] Compressao concluida: ' + inputSizeMB + 'MB -> ' + outputSizeMB + 'MB'); + resolve(outputBuffer); + }); + }); +} + +/** + * Converte buffer de áudio WebM para Ogg Opus usando ffmpeg + */ +function convertWebmToOgg(inputBuffer) { + return new Promise(function(resolve, reject) { + var tmpDir = path.join(__dirname, '../../uploads/audio'); + if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true }); + + var inputPath = path.join(tmpDir, 'input_' + Date.now() + '.webm'); + var outputPath = path.join(tmpDir, 'output_' + Date.now() + '.ogg'); + + fs.writeFileSync(inputPath, inputBuffer); + + execFile(ffmpegPath, [ + '-y', + '-i', inputPath, + '-c:a', 'libopus', + '-b:a', '16k', + '-ar', '16000', + '-ac', '1', + outputPath + ], { timeout: 30000 }, function(err) { + // Limpa arquivo temporário de entrada + try { fs.unlinkSync(inputPath); } catch(e) {} + + if (err) { + try { fs.unlinkSync(outputPath); } catch(e) {} + return reject(err); + } + + var outputBuffer = fs.readFileSync(outputPath); + try { fs.unlinkSync(outputPath); } catch(e) {} + + resolve(outputBuffer); + }); + }); +} + +/** + * Valida se um buffer tem cabeçalho de arquivo de mídia válido + * (defesa: rejeita JSON, HTML, ou dados criptografados salvos por engano) + * @param {Buffer} buf + * @param {string} mimeType + * @returns {boolean} + */ +function validarHeaderMidiaServida(buf, mimeType) { + if (!buf || buf.length < 4) return false; + var headerHex = buf.slice(0, 4).toString('hex'); + // Valida headers de audio conhecidos + var validHeaders = ['4f676753', // OggS (ogg/opus) + '52494646', // RIFF (wav) + '66747970', // ftyp (mp4/m4a) + '494433', // ID3 (mp3) + '1a45dfa3', // WebM/Matroska EBML + 'fffb', 'fff3', 'fffa', 'fff2']; // MP3 frames + for (var vh of validHeaders) { + if (headerHex.startsWith(vh)) return true; + } + // Para imagens: JPEG (FF D8 FF), PNG (89504e47), GIF (47494638), WebP (52494646 ja coberto) + if (mimeType.startsWith('image/')) { + if (headerHex.startsWith('ffd8ff') || headerHex.startsWith('89504e47') || + headerHex.startsWith('47494638')) return true; + } + // Para documentos: ZIP/Office (504b) + if (mimeType.startsWith('application/')) { + if (headerHex.startsWith('504b')) return true; + } + return false; +} + +class ChatController { + /** + * Página do chat + * GET /app/:alias/company/:empresaId/conversation/:conversaId + */ + static chatPage(req, res) { + res.sendFile(path.resolve(__dirname, '../public/chat.html')); + } + + /** + * Listar conversas (sidebar esquerda) + * GET /api/:alias/conversations?empresaId=&status=A,E + */ + static async listConversations(req, res) { + try { + const { alias } = req.params; + const empresaId = parseInt(req.query.empresaId) || req.user?.empresas?.[0]; + const statusFilter = req.query.status || 'A,E'; + const filter = req.query.filter || ''; // 'mine', 'unassigned', 'all' + + if (!req.user?.empresas?.includes(empresaId)) + return res.status(403).json({ success: false, error: 'Sem permissão.' }); + + // Status via parâmetros (evita SQL injection). Aceita apenas A/E/F. + const statusList = statusFilter.split(',').map(s => s.trim().toUpperCase()).filter(s => /^[AEF]$/.test(s)); + if (statusList.length === 0) statusList.push('A', 'E'); + const statusPh = statusList.map(() => '?').join(','); + + const params = [empresaId]; + statusList.forEach(s => params.push(s)); + + let extraFilter = ''; + if (filter === 'mine') { + extraFilter = ' AND c.CON_USUARIO_ID = ?'; + params.push(req.user?.id || 0); + } else if (filter === 'unassigned') { + extraFilter = ' AND c.CON_USUARIO_ID IS NULL AND c.CON_EQUIPE_ID IS NULL'; + } else if (filter === 'equipe') { + // Busca equipes do usuário logado + const minhasEquipes = await db.query(alias, + 'SELECT EQU_EQUIPE_ID FROM CHATC2_USU_EQUIPES WHERE EQU_USUARIO_ID = ?', [req.user?.id]); + if (minhasEquipes.length > 0) { + const eqPh = minhasEquipes.map(() => '?').join(','); + extraFilter = ' AND c.CON_EQUIPE_ID IN (' + eqPh + ')'; + minhasEquipes.forEach(e => params.push(e.EQU_EQUIPE_ID)); + } else { + extraFilter = ' AND 1=0'; // Nenhuma equipe + } + } + + const result = await db.query(alias, ` + SELECT c.*, + (SELECT CME_TEXTO FROM CHATC2_CONVERSAS_MENSAGENS WHERE CME_CONVERSA_ID = c.CON_CODIGO_ID ORDER BY CME_DT_ENVIO DESC FETCH FIRST 1 ROWS ONLY) AS ULTIMA_MSG, + (SELECT CME_DT_ENVIO FROM CHATC2_CONVERSAS_MENSAGENS WHERE CME_CONVERSA_ID = c.CON_CODIGO_ID ORDER BY CME_DT_ENVIO DESC FETCH FIRST 1 ROWS ONLY) AS DT_ULTIMA_MSG + FROM CHATC2_CONVERSAS c + WHERE c.CON_EMPRESA_ID = ? AND c.CON_STATUS IN (${statusPh}) AND c.CON_SITUACAO = 'A' ${extraFilter} + ORDER BY c.CON_DT_ULTIMA_MSG DESC + `, params); + + const conversations = await Promise.all(result.map(async (row) => { + // Busca etiquetas + const labels = await db.query(alias, ` + SELECT e.ETI_CODIGO_ID, e.ETI_NOME, e.ETI_COR + FROM CHATC2_ETIQUETAS e INNER JOIN CHATC2_CONVERSAS_ETIQUETAS ce ON e.ETI_CODIGO_ID = ce.CET_ETIQUETA_ID + WHERE ce.CET_CONVERSA_ID = ? AND e.ETI_SITUACAO = 'A' + `, [row.CON_CODIGO_ID]); + + // Busca foto do cliente se configurado + let foto = null; + if (row.CON_CLIENTE_ID) { + const config = await db.query(alias, + 'SELECT CFE_FOTO_CELULAR FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?', [empresaId]); + if (config.length > 0 && config[0].CFE_FOTO_CELULAR === 'S') { + const cli = await db.query(alias, + 'SELECT CLI_FOTO, CLI_NOME_FOTO FROM CLIENTES WHERE CLI_CODIGO_ID = ?', [row.CON_CLIENTE_ID]); + if (cli.length > 0) { + var rawFoto = cli[0].CLI_FOTO; + if (rawFoto) { + if (Buffer.isBuffer(rawFoto)) { + var str = rawFoto.toString('utf8'); + if (/^[A-Za-z0-9+/]+=*$/.test(str.slice(0, 30))) { + foto = str; + } else { + var prefixo = 'data:image/jpeg;base64,'; + if (rawFoto[0] === 0x89 && rawFoto[1] === 0x50) prefixo = 'data:image/png;base64,'; + else if (rawFoto[0] === 0x47 && rawFoto[1] === 0x49) prefixo = 'data:image/gif;base64,'; + else if (rawFoto[0] === 0x52 && rawFoto[1] === 0x49) prefixo = 'data:image/webp;base64,'; + foto = prefixo + rawFoto.toString('base64'); + } + } else { + foto = rawFoto; + } + } + } + } + } + + return { + id: row.CON_CODIGO_ID, + empresaId: row.CON_EMPRESA_ID, + instanciaId: row.CON_INSTANCIA_ID, + clienteId: row.CON_CLIENTE_ID, + numero: (row.CON_NUMERO || '').trim(), + nomeContato: (row.CON_NOME_CONTATO || '').trim(), + status: (row.CON_STATUS || '').trim(), + usuarioId: row.CON_USUARIO_ID, + equipeId: row.CON_EQUIPE_ID, + dtInicio: row.CON_DT_INICIO, + dtFinal: row.CON_DT_FINAL, + ultimaMsg: row.ULTIMA_MSG || '', + dtUltimaMsg: row.DT_ULTIMA_MSG || row.CON_DT_ULTIMA_MSG, + saudacaoEnviada: (row.CON_SAUDACAO_ENVIADA || 'N').trim(), + csatEnviado: (row.CON_CSAT_ENVIADO || 'N').trim(), + primeiraMsg: row.CON_PRIMEIRA_MSG, + labels, + foto, + }; + })); + + // Contagens para cada filtro + const userId = req.user?.id || 0; + var countMine = 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_USUARIO_ID = ?`, + [empresaId, userId]); + var countUnassigned = 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_USUARIO_ID IS NULL AND CON_EQUIPE_ID IS NULL`, + [empresaId]); + var countEquipe = 0; + if (filter === 'equipe' || filter === 'mine' || filter === 'unassigned' || filter === 'all') { + 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 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]); + countEquipe = countEq[0]?.CT || 0; + } + } + + res.json({ + success: true, + data: conversations, + contagens: { + mine: countMine[0]?.CT || 0, + unassigned: countUnassigned[0]?.CT || 0, + equipe: countEquipe, + }, + }); + } catch (err) { res.status(500).json({ success: false, error: err.message }); } + } + + /** + * Pegar/conversa específica + * GET /api/:alias/conversations/:id + */ + static async getConversation(req, res) { + try { + const { alias, id } = req.params; + + const result = await db.query(alias, + 'SELECT * FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]); + + if (result.length === 0) return res.status(404).json({ success: false, error: 'Conversa não encontrada.' }); + + const row = result[0]; + const empresaId = row.CON_EMPRESA_ID; + + if (!req.user?.empresas?.includes(empresaId)) + return res.status(403).json({ success: false, error: 'Sem permissão.' }); + + // Verifica se o número pertence a um dependente + let dependenteInfo = null; + if (!row.CON_CLIENTE_ID && row.CON_NUMERO) { + const depNumero = row.CON_NUMERO.replace(/\D/g, ''); + const depTermo = depNumero.slice(-8); + const dep = await db.query(alias, + `SELECT d.DEPC_NOME, d.DEPC_TELEFONE, d.DEPC_PARENTESCO, + c.CLI_CODIGO_ID AS TITULAR_ID, c.CLI_NOME AS TITULAR_NOME, + c.CLI_MATRICULA AS TITULAR_MATRICULA, c.CLI_PLANOS_ID AS TITULAR_PLANO_ID, + c.CLI_CIDADES_FAT_ID, c.CLI_BAIRROS_FAT_ID + FROM DEPENDENTES_CLI d + LEFT JOIN CLIENTES c ON d.DEPC_CLIENTE_ID = c.CLI_CODIGO_ID + WHERE d.DEPC_EMPRESA_ID = ? AND d.DEPC_SITUACAO = 'A' + AND REPLACE(REPLACE(REPLACE(COALESCE(d.DEPC_TELEFONE,''),'-',''),'(',''),')','') LIKE '%' || ? || '%'`, + [empresaId, depTermo] + ); + if (dep.length > 0) { + const d = dep[0]; + // Busca dados adicionais do titular + let cidadeNome = null, bairroNome = null, planoNome = null; + let inadimplente = false; + if (d.TITULAR_ID) { + if (d.CLI_CIDADES_FAT_ID) { + const cid = await db.query(alias, 'SELECT CID_NOME FROM CIDADES WHERE CID_CODIGO_ID = ?', [d.CLI_CIDADES_FAT_ID]); + if (cid.length > 0) cidadeNome = cid[0].CID_NOME?.trim(); + } + if (d.CLI_BAIRROS_FAT_ID) { + const bai = await db.query(alias, 'SELECT BAR_DESCRICAO FROM BAIRROS WHERE BAR_CODIGO_ID = ?', [d.CLI_BAIRROS_FAT_ID]); + if (bai.length > 0) bairroNome = bai[0].BAR_DESCRICAO?.trim(); + } + if (d.TITULAR_PLANO_ID) { + const pla = await db.query(alias, 'SELECT PLA_NOME FROM PLANOS WHERE PLA_CODIGO_ID = ?', [d.TITULAR_PLANO_ID]); + if (pla.length > 0) planoNome = pla[0].PLA_NOME?.trim(); + } + const tit = await db.query(alias, + 'SELECT COUNT(*) AS T FROM CARNES WHERE CAR_CLIENTE_ID = ? AND CAR_SITUACAO = 0 AND CAR_DT_VENCIMENTO < CURRENT_DATE', + [d.TITULAR_ID]); + inadimplente = tit[0]?.T > 0; + } + dependenteInfo = { + nome: (d.DEPC_NOME || '').trim(), + telefone: (d.DEPC_TELEFONE || '').trim(), + parentesco: (d.DEPC_PARENTESCO || '').trim(), + titularId: d.TITULAR_ID || d.CLI_CODIGO_ID || null, + titularNome: (d.TITULAR_NOME || '').trim(), + titularMatricula: (d.TITULAR_MATRICULA || '').trim(), + titularCidade: cidadeNome, + titularBairro: bairroNome, + titularPlano: planoNome, + titularInadimplente: inadimplente, + }; + } + } + + // Info do cliente se existir + let clienteInfo = null; + if (row.CON_CLIENTE_ID) { + const cli = await db.query(alias, ` + SELECT CLI_CODIGO_ID, CLI_NOME, CLI_MATRICULA, CLI_SITUACAO, CLI_EMAIL, CLI_CELULAR, + CLI_FONE1, CLI_ENDERECO_FAT, CLI_NUMERO_FAT, CLI_CIDADES_FAT_ID, CLI_BAIRROS_FAT_ID, + CLI_COBRADOR_ID, CLI_DIA_COBRANCA, CLI_FOTO, CLI_NOME_FOTO, CLI_PLANOS_ID + FROM CLIENTES WHERE CLI_CODIGO_ID = ? + `, [row.CON_CLIENTE_ID]); + + if (cli.length > 0) { + const c = cli[0]; + + // Busca plano + let planoNome = null; + if (c.CLI_PLANOS_ID) { + const plano = await db.query(alias, 'SELECT PLA_NOME FROM PLANOS WHERE PLA_CODIGO_ID = ?', [c.CLI_PLANOS_ID]); + if (plano.length > 0) planoNome = plano[0].PLA_NOME?.trim(); + } + + // Busca cidade/bairro + let cidadeNome = null, bairroNome = null; + if (c.CLI_CIDADES_FAT_ID) { + const cidade = await db.query(alias, 'SELECT CID_NOME FROM CIDADES WHERE CID_CODIGO_ID = ?', [c.CLI_CIDADES_FAT_ID]); + if (cidade.length > 0) cidadeNome = cidade[0].CID_NOME?.trim(); + } + if (c.CLI_BAIRROS_FAT_ID) { + const bairro = await db.query(alias, 'SELECT BAR_DESCRICAO FROM BAIRROS WHERE BAR_CODIGO_ID = ?', [c.CLI_BAIRROS_FAT_ID]); + if (bairro.length > 0) bairroNome = bairro[0].BAR_DESCRICAO?.trim(); + } + + // Inadimplente? Verifica se tem títulos abertos vencidos + let inadimplente = false; + const titulos = await db.query(alias, + `SELECT COUNT(*) AS T FROM CARNES WHERE CAR_CLIENTE_ID = ? AND CAR_SITUACAO = 0 AND CAR_DT_VENCIMENTO < CURRENT_DATE`, + [c.CLI_CODIGO_ID]); + inadimplente = titulos[0].T > 0; + + clienteInfo = { + id: c.CLI_CODIGO_ID, + nome: (c.CLI_NOME || '').trim(), + matricula: (c.CLI_MATRICULA || '').trim(), + situacao: (c.CLI_SITUACAO || '').trim(), + email: (c.CLI_EMAIL || '').trim(), + celular: (c.CLI_CELULAR || '').trim(), + telefone: (c.CLI_FONE1 || '').trim(), + endereco: (c.CLI_ENDERECO_FAT || '').trim(), + numero: (c.CLI_NUMERO_FAT || '').trim(), + cidade: cidadeNome, + bairro: bairroNome, + plano: planoNome, + inadimplente, + foto: (function(){ + var f = c.CLI_FOTO; + if (!f) return null; + if (Buffer.isBuffer(f)) { + var s = f.toString('utf8'); + if (/^[A-Za-z0-9+/]+=*$/.test(s.slice(0, 30))) return s; + var prefixo = 'data:image/jpeg;base64,'; + if (f[0] === 0x89 && f[1] === 0x50) prefixo = 'data:image/png;base64,'; + else if (f[0] === 0x47 && f[1] === 0x49) prefixo = 'data:image/gif;base64,'; + else if (f[0] === 0x52 && f[1] === 0x49) prefixo = 'data:image/webp;base64,'; + return prefixo + f.toString('base64'); + } + return f; + })(), + }; + } + } + + // Etiquetas + const labels = await db.query(alias, ` + SELECT e.* FROM CHATC2_ETIQUETAS e INNER JOIN CHATC2_CONVERSAS_ETIQUETAS ce ON e.ETI_CODIGO_ID = ce.CET_ETIQUETA_ID + WHERE ce.CET_CONVERSA_ID = ? AND e.ETI_SITUACAO = 'A' + `, [id]); + + res.json({ + success: true, + data: { + id: row.CON_CODIGO_ID, + empresaId: row.CON_EMPRESA_ID, + instanciaId: row.CON_INSTANCIA_ID, + clienteId: row.CON_CLIENTE_ID, + numero: (row.CON_NUMERO || '').trim(), + nomeContato: (row.CON_NOME_CONTATO || '').trim(), + status: (row.CON_STATUS || '').trim(), + usuarioId: row.CON_USUARIO_ID, + equipeId: row.CON_EQUIPE_ID, + dtInicio: row.CON_DT_INICIO, + dtFinal: row.CON_DT_FINAL, + dtUltimaMsg: row.CON_DT_ULTIMA_MSG, + saudacaoEnviada: (row.CON_SAUDACAO_ENVIADA || 'N').trim(), + csatEnviado: (row.CON_CSAT_ENVIADO || 'N').trim(), + primeiraMsg: row.CON_PRIMEIRA_MSG, + cliente: clienteInfo, + dependente: dependenteInfo, + labels: labels.map(l => ({ id: l.ETI_CODIGO_ID, nome: (l.ETI_NOME || '').trim(), cor: (l.ETI_COR || '#667eea').trim() })), + } + }); + } catch (err) { res.status(500).json({ success: false, error: err.message }); } + } + + /** + * Pegar mensagens de uma conversa + * GET /api/:alias/conversations/:id/messages?page=&limit= + */ + /** + * Verifica se a conversa existe e pertence a uma empresa do usuário. + * Retorna { status: 404|403 } em caso de bloqueio, ou { ok:true, empresaId }. + */ + static async checarConversaEmpresa(alias, id, req) { + const r = await db.query(alias, + 'SELECT CON_EMPRESA_ID FROM CHATC2_CONVERSAS WHERE CON_CODIGO_ID = ?', [id]); + 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 }; + return { ok: true, empresaId: emp }; + } + + static async getMessages(req, res) { + try { + const { alias, id } = req.params; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 50; + const offset = (page - 1) * limit; + + const chk = await ChatController.checarConversaEmpresa(alias, id, req); + if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' }); + + const msgs = await db.query(alias, ` + SELECT * + FROM CHATC2_CONVERSAS_MENSAGENS + WHERE CME_CONVERSA_ID = ? AND CME_SITUACAO = 'A' + ORDER BY CME_DT_ENVIO DESC + OFFSET ${offset} ROWS FETCH NEXT ${limit} ROWS ONLY + `, [id]); + + var mapped = msgs.reverse().map(function(m) { return { + id: m.CME_CODIGO_ID, + conversaId: m.CME_CONVERSA_ID, + remetente: (m.CME_REMETENTE || '').trim(), + usuarioId: m.CME_USUARIO_ID, + mensagem: m.CME_TEXTO || m.CME_MENSAGEM || '', + tipo: (m.CME_TIPO || 'text').trim(), + dtEnvio: m.CME_DT_ENVIO, + privada: (m.CME_PRIVADA || 'N').trim(), + lida: (m.CME_LIDA || 'N').trim(), + midiaId: m.CME_MIDIA_ID, + };}); + + // Busca transcricao para mensagens com midia + for (var i = 0; i < mapped.length; i++) { + if (mapped[i].midiaId && (mapped[i].tipo === 'audio' || mapped[i].tipo === 'ptt')) { + try { + var trans = await db.query(alias, + 'SELECT MAT_TRANSCRICAO FROM CHATC2_MENSAGENS_ATENDIMENTOS WHERE MAT_CODIGO_ID = ? AND MAT_TRANSCRICAO IS NOT NULL', + [mapped[i].midiaId]); + if (trans.length > 0 && trans[0].MAT_TRANSCRICAO) { + var txt = trans[0].MAT_TRANSCRICAO; + if (typeof txt === 'object' && txt.toString) txt = txt.toString(); + mapped[i].transcricao = txt; + } + } catch(e) {} + } + } + + res.json({ + success: true, + data: mapped, + }); + } catch (err) { res.status(500).json({ success: false, error: err.message }); } + } + + /** + * Enviar mensagem + * POST /api/:alias/conversations/:id/messages + */ + static async sendMessage(req, res) { + try { + const { alias, id } = req.params; + const { mensagem, tipo, privada, nomeArquivo } = req.body; + let midiaBase64 = req.body.midiaBase64; + const usuarioId = req.user?.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[0].CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).includes(conv[0].CON_EMPRESA_ID)) + return res.status(403).json({ success: false, error: 'Sem permissão.' }); + + // Se a conversa estiver finalizada, reabre + if (conv[0].CON_STATUS === 'F') { + await db.execute(alias, + "UPDATE CHATC2_CONVERSAS SET CON_STATUS = 'A', CON_DT_FINAL = NULL WHERE CON_CODIGO_ID = ?", [id]); + } + + const maxId = await db.query(alias, 'SELECT MAX(CME_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS_MENSAGENS'); + const newId = (maxId[0]?.ID || 0) + 1; + let midiaId = null; + + // Salva mídia se houver + if (midiaBase64) { + const maxMidia = await db.query(alias, 'SELECT MAX(MAT_CODIGO_ID) AS ID FROM CHATC2_MENSAGENS_ATENDIMENTOS'); + midiaId = (maxMidia[0]?.ID || 0) + 1; + + // Para áudio: converte WebM → OGG antes de salvar no BD + // Assim o BD já tem o OGG e o envio para Evolution usa a mesma versão + var midiaFinal = midiaBase64; + var nomeFinal = nomeArquivo || 'arquivo'; + var mimeType = tipo || 'application/octet-stream'; + + if (tipo === 'audio') { + mimeType = 'audio/webm'; + try { + var inputBuf = Buffer.from(midiaBase64, 'base64'); + if (inputBuf.length > 100) { + var oggBuf = await convertWebmToOgg(inputBuf); + if (oggBuf && oggBuf.length > 100) { + midiaFinal = oggBuf.toString('base64'); + mimeType = 'audio/ogg'; + nomeFinal = 'audio.ogg'; + console.log('[SendMsg] Áudio convertido WebM→OGG antes de salvar:', inputBuf.length, '→', oggBuf.length); + } + } + } catch(convErr) { + console.error('[SendMsg] Conversão áudio falhou, usando WebM:', convErr.message.substring(0, 80)); + } + } else if (tipo === 'image') { + var ext = (nomeArquivo || '').split('.').pop().toLowerCase(); + if (ext === 'png') mimeType = 'image/png'; + else if (ext === 'gif') mimeType = 'image/gif'; + else if (ext === 'webp') mimeType = 'image/webp'; + else mimeType = 'image/jpeg'; + } + + await db.execute(alias, ` + INSERT INTO CHATC2_MENSAGENS_ATENDIMENTOS (MAT_CODIGO_ID, MAT_CONVERSA_ID, MAT_MENSAGEM_ID, MAT_NOME_ARQUIVO, MAT_TIPO_ARQUIVO, MAT_ARQUIVO) + VALUES (?, ?, ?, ?, ?, ?) + `, [midiaId, id, newId, nomeFinal, mimeType, midiaFinal]); + + // Usa a versão convertida para envio posterior à Evolution + midiaBase64 = midiaFinal; + } + + // Verifica config para enviar nome do usuário + let textoFinal = mensagem || ''; + try { + const config = await db.query(alias, + "SELECT CFE_ENVIAR_NOME_USUARIO FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?", + [conv[0].CON_EMPRESA_ID]); + if (config.length > 0 && config[0].CFE_ENVIAR_NOME_USUARIO === 'S') { + const user = await db.query(alias, + 'SELECT USU_NOME FROM USUARIOS WHERE USU_CODIGO_ID = ?', [usuarioId]); + if (user.length > 0) { + const nomeUser = (user[0].USU_NOME || '').trim(); + if (nomeUser && textoFinal) { + textoFinal = nomeUser + ': ' + textoFinal; + } + } + } + } catch(e) {} + + // 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, ?) + `, [newId, id, usuarioId, textoFinal, tipo || 'text', privada === 'S' ? 'S' : 'N', midiaId]); + + // Atualiza data da última mensagem + await db.execute(alias, + 'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?', [id]); + + // Se não for privada, envia via Evolution API + if (privada !== 'S' && conv[0].CON_NUMERO) { + try { + const instancia = await db.query(alias, + 'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [conv[0].CON_INSTANCIA_ID]); + if (instancia.length > 0) { + const ins = instancia[0]; + console.log('[SendMsg] Enviando mídia tipo:', tipo, 'tamanho:', (midiaBase64 || '').length, 'nome:', nomeArquivo, 'para:', conv[0].CON_NUMERO); + var evoResult = await sendEvolutionMessage(ins, conv[0].CON_NUMERO, textoFinal, tipo, midiaBase64, nomeArquivo); + console.log('[SendMsg] Evolution resposta:', (evoResult || '').substring(0, 200)); + } else { + console.log('[SendMsg] Instância não encontrada para ID:', conv[0].CON_INSTANCIA_ID); + } + } catch (evoErr) { + console.error('[SendMsg] Erro ao enviar via Evolution:', evoErr.message); + } + } + + res.json({ success: true, data: { id: newId } }); + } catch (err) { res.status(500).json({ success: false, error: err.message }); } + } + + /** + * Finalizar conversa + * POST /api/:alias/conversations/:id/finalize + */ + static async finalizeConversation(req, res) { + try { + const { alias, id } = req.params; + + 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.' }); + + const c = conv[0]; + if (c.CON_EMPRESA_ID && !((req.user && req.user.empresas) || []).includes(c.CON_EMPRESA_ID)) + return res.status(403).json({ success: false, error: 'Sem permissão.' }); + + // Busca nome do usuário que atendeu + let usuarioNome = null; + if (c.CON_USUARIO_ID) { + const user = await db.query(alias, + 'SELECT USU_NOME FROM USUARIOS WHERE USU_CODIGO_ID = ?', [c.CON_USUARIO_ID]); + if (user.length > 0) usuarioNome = (user[0].USU_NOME || '').trim(); + } + + // Busca nome da equipe + let equipeNome = null; + if (c.CON_EQUIPE_ID) { + const equipe = await db.query(alias, + 'SELECT EQU_NOME FROM CHATC2_EQUIPES WHERE EQU_CODIGO_ID = ?', [c.CON_EQUIPE_ID]); + if (equipe.length > 0) equipeNome = (equipe[0].EQU_NOME || '').trim(); + } + + // Busca etiquetas + const etiquetas = await db.query(alias, ` + SELECT ETI_NOME FROM CHATC2_ETIQUETAS e + INNER JOIN CHATC2_CONVERSAS_ETIQUETAS ce ON e.ETI_CODIGO_ID = ce.CET_ETIQUETA_ID + WHERE ce.CET_CONVERSA_ID = ? AND e.ETI_SITUACAO = 'A' + `, [id]); + const etiquetasDesc = etiquetas.map(function(e) { return (e.ETI_NOME || '').trim(); }).filter(Boolean).join(', '); + + await db.execute(alias, ` + UPDATE CHATC2_CONVERSAS SET + CON_STATUS = 'F', + CON_DT_FINAL = CURRENT_TIMESTAMP, + CON_USUARIO_NOME = ?, + CON_EQUIPE_NOME = ?, + CON_ETIQUETAS_DESC = ? + WHERE CON_CODIGO_ID = ? + `, [usuarioNome, equipeNome, etiquetasDesc, id]); + + // CSAT + const config = await db.query(alias, + 'SELECT CFE_CSAT_ATIVO, CFE_CSAT_MENSAGEM FROM CHATC2_CONFIGURACOES_EMPRESA WHERE CFE_EMPRESA_ID = ?', + [conv[0].CON_EMPRESA_ID]); + + if (config.length > 0 && config[0].CFE_CSAT_ATIVO === 'S') { + var csatEnviadoSucesso = false; + try { + const empresaId = conv[0].CON_EMPRESA_ID; + var baseUrl = process.env.EXTERNAL_URL || process.env.NGROK_URL || ('https://' + (req.headers.host || 'localhost:3000')); + var csatLink = baseUrl.replace(/^https?:\/\//, '') + '/app/' + alias + '/csat?alias=' + alias + '&conversa=' + id + '&empresa=' + empresaId; + const msgCsat = 'Ol\u00e1! Gostar\u00edamos de saber como foi seu atendimento.\n\nAcesse o link abaixo para avaliar:\n' + csatLink; + + const inst = await db.query(alias, + "SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ? AND INS_SITUACAO = 'A'", + [conv[0].CON_INSTANCIA_ID]); + if (inst.length > 0) { + var csatResult = await sendEvolutionMessage(inst[0], conv[0].CON_NUMERO, msgCsat, 'text', null); + console.log('[CSAT] Resultado envio:', JSON.stringify(csatResult)); + if (csatResult) { + try { + var parsed = typeof csatResult === 'string' ? JSON.parse(csatResult) : csatResult; + if (parsed && parsed.key && parsed.key.remoteJid) csatEnviadoSucesso = true; + } catch(e) { + if (csatResult.status || (typeof csatResult === 'string' && csatResult.length > 20)) csatEnviadoSucesso = true; + } + } + } else { + console.log('[CSAT] Instancia nao encontrada ou inativa para ID:', conv[0].CON_INSTANCIA_ID); + } + } catch(e) { console.error('[CSAT] Erro ao enviar:', e.message); } + + // So marca como enviado se realmente conseguiu enviar + if (csatEnviadoSucesso) { + await db.execute(alias, + "UPDATE CHATC2_CONVERSAS SET CON_CSAT_ENVIADO = 'S' WHERE CON_CODIGO_ID = ?", [id]); + console.log('[CSAT] Marcado como enviado para conversa', id); + } else { + console.log('[CSAT] NAO enviado - Evolution API pode estar offline'); + } + } + + res.json({ success: true, message: 'Conversa finalizada.' }); + } catch (err) { res.status(500).json({ success: false, error: err.message }); } + } + + /** + * Criar nova conversa e enviar primeira mensagem + * POST /api/:alias/conversations/create + * Body: { empresaId, numero, nomeContato, mensagem, instanciaId, clienteId } + */ + static async createConversation(req, res) { + try { + const { alias } = req.params; + const { empresaId, numero, nomeContato, mensagem, instanciaId, clienteId } = req.body; + + if (!empresaId || !numero || !mensagem) { + return res.status(400).json({ success: false, error: 'Campos obrigatórios: empresaId, numero, mensagem' }); + } + + // Verifica permissão + const userEmpresas = req.user?.empresas || []; + if (!userEmpresas.includes(parseInt(empresaId))) { + return res.status(403).json({ success: false, error: 'Sem permissão para esta empresa.' }); + } + + const numeroLimpo = numero.replace(/\D/g, '').substring(0, 15); + + // Verifica se já existe conversa ABERTA ou EM ESPERA para este número + // Tenta com o número exato, e também com os últimos 8 dígitos + let existente = await db.query(alias, ` + SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_NUMERO + FROM CHATC2_CONVERSAS + WHERE CON_NUMERO = ? AND CON_EMPRESA_ID = ? + AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' + ORDER BY CON_CODIGO_ID ASC + FETCH FIRST 1 ROWS ONLY + `, [numeroLimpo, empresaId]); + + // Se não achou pelo número exato, tenta pelos últimos 8 dígitos + if (existente.length === 0) { + const ultimos8 = numeroLimpo.slice(-8); + existente = await db.query(alias, ` + SELECT CON_CODIGO_ID, CON_STATUS, CON_CLIENTE_ID, CON_NUMERO + FROM CHATC2_CONVERSAS + WHERE CON_NUMERO LIKE '%' || ? || '%' AND CON_EMPRESA_ID = ? + AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A' + ORDER BY CON_CODIGO_ID ASC + FETCH FIRST 1 ROWS ONLY + `, [ultimos8, empresaId]); + } + + let conversaId; + if (existente.length > 0) { + // Reusa a conversa existente (prioriza a de menor ID) + conversaId = existente[0].CON_CODIGO_ID; + + // Se tem clienteId e a conversa não tinha, atualiza + if (clienteId && !existente[0].CON_CLIENTE_ID) { + await db.execute(alias, + 'UPDATE CHATC2_CONVERSAS SET CON_CLIENTE_ID = ?, CON_NOME_CONTATO = ? WHERE CON_CODIGO_ID = ?', + [clienteId, nomeContato || '', conversaId]); + } + + // Adiciona mensagem na conversa existente + 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) + `, [msgId, conversaId, req.user?.id, mensagem]); + + await db.execute(alias, + 'UPDATE CHATC2_CONVERSAS SET CON_DT_ULTIMA_MSG = CURRENT_TIMESTAMP WHERE CON_CODIGO_ID = ?', + [conversaId]); + + // Envia via Evolution + const resultado = await db.query(alias, + 'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [instanciaId || null]); + if (resultado.length > 0) { + sendEvolutionMessage(resultado[0], numeroLimpo, mensagem, 'text', null).catch(function(){}); + } + + return res.json({ success: true, data: { id: conversaId, reutilizada: true } }); + } + + // Gera ID da nova conversa + const maxId = await db.query(alias, 'SELECT MAX(CON_CODIGO_ID) AS ID FROM CHATC2_CONVERSAS'); + const newId = (maxId[0]?.ID || 0) + 1; + + // Se não informou instância, pega a primeira ativa + let instId = instanciaId; + if (!instId) { + const inst = await db.query(alias, + "SELECT INS_CODIGO_ID FROM CHATC2_INSTANCIAS WHERE INS_SITUACAO = 'A' FETCH FIRST 1 ROWS ONLY"); + if (inst.length > 0) instId = inst[0].INS_CODIGO_ID; + } + + // 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) + `, [newId, empresaId, instId, numeroLimpo, nomeContato || numeroLimpo, clienteId || null]); + + // Insere primeira mensagem + 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) + `, [msgId, newId, req.user?.id, mensagem]); + + // Envia via Evolution API + try { + const resultado = await db.query(alias, + 'SELECT * FROM CHATC2_INSTANCIAS WHERE INS_CODIGO_ID = ?', [instId]); + if (resultado.length > 0) { + await sendEvolutionMessage(resultado[0], numeroLimpo, mensagem, 'text', null); + } + } catch (evoErr) { + console.error('Erro ao enviar primeira mensagem via Evolution:', evoErr.message); + } + + res.json({ success: true, data: { id: newId, msgId } }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } + } + + /** + * Atribuir conversa a usuário + * POST /api/:alias/conversations/:id/assign + */ + static async assignConversation(req, res) { + try { + const { alias, id } = req.params; + const { usuarioId } = req.body; + + const chk = await ChatController.checarConversaEmpresa(alias, id, req); + if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' }); + + await db.execute(alias, + "UPDATE CHATC2_CONVERSAS SET CON_USUARIO_ID = ?, CON_STATUS = 'A' WHERE CON_CODIGO_ID = ?", + [usuarioId, id]); + + res.json({ success: true }); + } catch (err) { res.status(500).json({ success: false, error: err.message }); } + } + + /** + * Atribuir conversa a uma equipe + * POST /api/:alias/conversations/:id/assign-team + */ + static async assignTeam(req, res) { + try { + const { alias, id } = req.params; + const { equipeId } = req.body; + + const chk = await ChatController.checarConversaEmpresa(alias, id, req); + if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' }); + + await db.execute(alias, + 'UPDATE CHATC2_CONVERSAS SET CON_EQUIPE_ID = ? WHERE CON_CODIGO_ID = ?', + [equipeId || null, id]); + + res.json({ success: true }); + } catch (err) { res.status(500).json({ success: false, error: err.message }); } + } + + /** + * Adicionar/remover etiqueta da conversa + * POST /api/:alias/conversations/:id/labels + */ + static async toggleLabel(req, res) { + try { + const { alias, id } = req.params; + const { etiquetaId, ativar } = req.body; + + const chk = await ChatController.checarConversaEmpresa(alias, id, req); + if (chk.status) return res.status(chk.status).json({ success: false, error: chk.status === 404 ? 'Conversa não encontrada.' : 'Sem permissão.' }); + + if (ativar) { + await db.execute(alias, + 'INSERT INTO CHATC2_CONVERSAS_ETIQUETAS (CET_CONVERSA_ID, CET_ETIQUETA_ID) VALUES (?, ?)', + [id, etiquetaId]); + } else { + await db.execute(alias, + 'DELETE FROM CHATC2_CONVERSAS_ETIQUETAS WHERE CET_CONVERSA_ID = ? AND CET_ETIQUETA_ID = ?', + [id, etiquetaId]); + } + + res.json({ success: true }); + } catch (err) { res.status(500).json({ success: false, error: err.message }); } + } + + /** + * Buscar mídia (imagem/audio) pelo ID + * GET /api/:alias/media/:mediaId + */ + static async getMedia(req, res) { + const { alias, mediaId } = req.params; + + if (!alias || !mediaId) { + return res.status(400).json({ success: false, error: 'Parâmetros inválidos.' }); + } + + let responded = false; + function respond(status, data) { + if (responded) return; + responded = true; + res.status(status).json(data); + } + function respondBuffer(mimeType, nomeArquivo, buffer) { + if (responded) return; + responded = true; + const totalSize = buffer.length; + + // Suporte a byte-range para áudio (permite seek no player HTML5) + const range = res.req.headers.range; + if (range && mimeType.startsWith('audio/')) { + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + const end = parts[1] ? parseInt(parts[1], 10) : totalSize - 1; + const chunkSize = (end - start) + 1; + res.status(206); + res.set('Content-Range', 'bytes ' + start + '-' + end + '/' + totalSize); + res.set('Accept-Ranges', 'bytes'); + res.set('Content-Length', chunkSize); + res.set('Content-Type', mimeType); + res.set('Content-Disposition', 'inline; filename="' + encodeURIComponent(nomeArquivo) + '"'); + res.send(buffer.slice(start, end + 1)); + return; + } + + res.set('Content-Type', mimeType); + res.set('Accept-Ranges', 'bytes'); + res.set('Content-Length', totalSize); + res.set('Content-Disposition', 'inline; filename="' + encodeURIComponent(nomeArquivo) + '"'); + res.send(buffer); + } + + try { + // Autenticação: /