mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
A stack local entregava o comando da sessão de WhatsApp a qualquer máquina
da rede da VM, por duas portas independentes.
**A chave.** `WAHA_API_KEY=deskcomm-local-key` nascia literal em
`ubuntu-local-installer.sh` e em `scripts/local-env.sh` — isto é, publicada
neste repositório. É o mesmo argumento que já tornou a senha do dono
aleatória nesta fatia ("qualquer máquina da mesma rede alcançaria o CRM com
uma credencial que está no GitHub"), só que esta credencial comanda o
WhatsApp. Agora ela vem de `openssl rand -hex 24`.
Sem knob de propósito: nenhum terceiro precisa conhecê-la (o app manda o
plaintext, o WAHA compara o sha512 derivado dela), e um
`${WAHA_API_KEY:-...}` herdaria em silêncio a chave da nuvem de quem tiver
a variável exportada no shell — o `.env.local` deste repo é lido por 93
scripts. Em `local-env.sh` o valor é gerado UMA vez numa variável, porque o
heredoc é sem aspas e dois `$(openssl rand)` dariam chave e hash
divergentes, que rendem 401.
**A porta.** `docker-compose.local.yml` publicava `"3030:3000"` sem
endereço de bind, com `WAHA_DASHBOARD_ENABLED: "true"`. O dashboard do WAHA
tem autenticação própria, com o padrão do WAHA — a chave da API não o
protege —, então a chave aleatória sozinha não fecharia essa porta. Agora é
`"127.0.0.1:3030:3000"`.
Isto alinha o compose local com o que a casa já pratica e com o que o
próprio texto do PR já prometia: `docker-compose.prod.yml` desliga o
dashboard ("nunca expor o dashboard num template público", linha 156) e não
publica porta nenhuma para o WAHA (`networks: [internal]`); e o bloco novo
de `docs/SETUP.md` já dizia "o WAHA em `http://127.0.0.1:3030`". Nada
alcança essa porta de fora do compose — nenhum dos quatro scripts a
referencia —, e de outra máquina o túnel `ssh -L 3030:localhost:3030`
resolve, como `docs/vendaval-vps-deploy-comandos.md:64` já faz.
O app (3000) continua em 0.0.0.0: é o produto, e é o que a VM serve.
**A guarda.** Cinco verificações novas em `tests/shell/stack-local.test.sh`
(12 → 17). Três delas EXECUTAM as linhas em vez de as lerem, com um stub de
`sha512sum` (que é do GNU e não existe no macOS, onde o teste roda antes de
chegar ao Ubuntu do CI), porque o que se prova é a procedência do hash.
Sabotagem A — os três consertos desfeitos: 4 vermelhos, exatamente os
previstos, e a checagem de controle ("a sonda enxerga o bloco do WAHA")
seguiu verde, então os vermelhos são medição e não sonda morta.
Sabotagem B — chave e hash vindos de dois `openssl rand` diferentes: os
dois greps ingênuos PASSAM (`deskcomm-local-key` → 0 ocorrências; o grep de
aleatoriedade casa) e só a checagem executável fica vermelha. É o caso que
justifica ela existir.
Restaurado: `md5` dos três arquivos igual ao de antes da sabotagem, e o
conserto conferido por presença — `grep -c` devolve 1 em cada um dos quatro
pontos, e `deskcomm-local-key` devolve 0 no repositório inteiro.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RccZbdYe8URAiWCpyvZimQ
190 lines
7.7 KiB
Bash
Executable File
190 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# DeskcommCRM - Instalador Automático (Local Ubuntu VM)
|
|
# Este script prepara a VM, sobe o Supabase local (PostgreSQL + Auth +
|
|
# PostgREST + Storage + Realtime), inicializa os serviços da aplicação e cria
|
|
# o usuário Admin.
|
|
|
|
set -Eeuo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$ROOT_DIR"
|
|
|
|
paint() { printf '\033[%sm%s\033[0m\n' "$1" "$2"; }
|
|
step() { printf '\n'; paint 32 "▶ $1"; }
|
|
error() { printf '\n'; paint 31 "✖ $1"; exit 1; }
|
|
|
|
step "Verificando dependências do sistema..."
|
|
if ! sudo apt-get update; then
|
|
# Um repositório de terceiro quebrado não deve impedir a instalação quando
|
|
# os índices dos repositórios necessários já estão disponíveis localmente.
|
|
# Não usamos AllowInsecureRepositories nem ignoramos assinatura GPG.
|
|
paint 33 "⚠ Um repositório APT externo falhou; vou tentar instalar usando os índices válidos já disponíveis."
|
|
fi
|
|
sudo apt-get install -y curl git jq openssl iproute2
|
|
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
step "Instalando Docker..."
|
|
curl -fsSL https://get.docker.com | sudo sh
|
|
fi
|
|
|
|
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || true)"
|
|
if [[ "${NODE_MAJOR:-0}" != "22" ]]; then
|
|
step "Instalando Node.js (necessário para CLI do Supabase e scripts locais)..."
|
|
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
|
sudo apt-get install -y nodejs
|
|
fi
|
|
|
|
# Se não estivermos na raiz do projeto, clona e entra nele.
|
|
if [ ! -f "package.json" ]; then
|
|
step "Clonando o repositório DeskcommCRM..."
|
|
git clone https://github.com/melgarafael/DeskcommCRM.git
|
|
cd DeskcommCRM
|
|
fi
|
|
|
|
if ! command -v pnpm >/dev/null 2>&1; then
|
|
step "Instalando pnpm 9.15.9..."
|
|
sudo npm install --global pnpm@9.15.9
|
|
fi
|
|
|
|
step "Instalando dependências do projeto..."
|
|
pnpm install --frozen-lockfile
|
|
|
|
step "Inicializando Supabase Local (PostgreSQL e serviços oficiais)..."
|
|
# O helper inicia a infraestrutura sem a cadeia histórica quebrada e aplica o
|
|
# baseline, que é a fonte de verdade para uma instalação nova.
|
|
./scripts/local-supabase.sh start
|
|
|
|
step "Coletando credenciais do Supabase..."
|
|
# Pega as chaves da API geradas dinamicamente
|
|
SUPA_STATUS=$(./scripts/local-supabase.sh status)
|
|
ANON_KEY=$(printf '%s' "$SUPA_STATUS" | node -e 'let s=""; process.stdin.on("data", c => s += c).on("end", () => process.stdout.write(JSON.parse(s).ANON_KEY || ""))')
|
|
SERVICE_ROLE_KEY=$(printf '%s' "$SUPA_STATUS" | node -e 'let s=""; process.stdin.on("data", c => s += c).on("end", () => process.stdout.write(JSON.parse(s).SERVICE_ROLE_KEY || ""))')
|
|
DB_URL=$(printf '%s' "$SUPA_STATUS" | node -e 'let s=""; process.stdin.on("data", c => s += c).on("end", () => process.stdout.write(JSON.parse(s).DB_URL || ""))')
|
|
|
|
[[ -n "$ANON_KEY" && "$ANON_KEY" != "null" ]] || error "Supabase não retornou ANON_KEY"
|
|
[[ -n "$SERVICE_ROLE_KEY" && "$SERVICE_ROLE_KEY" != "null" ]] || error "Supabase não retornou SERVICE_ROLE_KEY"
|
|
[[ -n "$DB_URL" && "$DB_URL" != "null" ]] || error "Supabase não retornou DB_URL"
|
|
|
|
# Obtendo o IP local da máquina (VM)
|
|
VM_IP=$(hostname -I | awk '{print $1}')
|
|
if [ -z "$VM_IP" ]; then
|
|
VM_IP="127.0.0.1"
|
|
fi
|
|
|
|
# O CLI informa 127.0.0.1 porque é a perspectiva do host. Os contêineres da
|
|
# aplicação precisam alcançar a porta publicada pelo host usando o IP da VM.
|
|
DB_URL="${DB_URL/127.0.0.1/$VM_IP}"
|
|
|
|
# 3000 é o padrão, mas não deve impedir a instalação quando outra aplicação já
|
|
# a utiliza na VM. O operador pode fixar APP_PORT antes de chamar o script.
|
|
APP_PORT="${APP_PORT:-3000}"
|
|
if command -v ss >/dev/null 2>&1; then
|
|
while ss -ltn | awk '{print $4}' | grep -Eq "(^|:)${APP_PORT}$"; do
|
|
APP_PORT=$((APP_PORT + 1))
|
|
done
|
|
fi
|
|
|
|
# Reescrevendo as URLs para apontar para o IP local (para comunicação Host -> Container)
|
|
API_URL="http://$VM_IP:54321"
|
|
APP_URL="http://$VM_IP:$APP_PORT"
|
|
|
|
step "Gerando chaves de segurança..."
|
|
CHAVE_CPF=$(openssl rand -base64 32)
|
|
CHAVE_WAHA=$(openssl rand -base64 32)
|
|
CHAVE_AI=$(openssl rand -base64 32)
|
|
INTERNAL_SEC=$(openssl rand -hex 32)
|
|
IMPERSONATE_SEC=$(openssl rand -base64 32)
|
|
WAHA_HMAC=$(openssl rand -hex 32)
|
|
# A chave da API do WAHA NASCE ALEATÓRIA, pelo mesmo motivo da senha do dono:
|
|
# a que estava aqui era literal e está publicada neste repositório, e ela
|
|
# comanda a sessão de WhatsApp. Sem knob de propósito — nenhum terceiro
|
|
# precisa conhecê-la (o app manda o plaintext, o WAHA compara o sha512 abaixo),
|
|
# e um `${WAHA_API_KEY:-...}` herdaria em silêncio a chave da nuvem de quem
|
|
# tiver a variável exportada no shell.
|
|
WAHA_KEY=$(openssl rand -hex 24)
|
|
WAHA_KEY_HASH=$(echo -n "$WAHA_KEY" | sha512sum | awk '{print $1}')
|
|
|
|
step "Configurando .env.local (credenciais locais, não versionadas)..."
|
|
# O .env.local de quem já usa este clone aponta para OUTRO ambiente (a nuvem,
|
|
# um QA), e 93 scripts deste repositório o leem. Sobrescrever sem cópia apaga
|
|
# esse ambiente em silêncio — e este instalador roda DENTRO de um clone
|
|
# existente quando acha o package.json. `scripts/local-env.sh` já tinha esta
|
|
# guarda; aqui faltava.
|
|
if [[ -s .env.local ]] && ! grep -q '^DESKCOMM_ENV_MODE=local$' .env.local; then
|
|
BACKUP=".env.local.cloud-backup"
|
|
if [[ ! -e "$BACKUP" ]]; then
|
|
# `cp -p` e não `cp --preserve=mode`: a segunda é do GNU e falha no macOS,
|
|
# onde o teste desta guarda roda antes de chegar ao Ubuntu do CI.
|
|
cp -p .env.local "$BACKUP"
|
|
chmod 600 "$BACKUP"
|
|
paint 33 "⚠ O .env.local existente NÃO era local; copiei para $BACKUP antes de substituir."
|
|
fi
|
|
fi
|
|
umask 077
|
|
cat <<EOF > .env.local
|
|
DESKCOMM_ENV_MODE=local
|
|
NODE_ENV=production
|
|
NEXT_PUBLIC_SUPABASE_URL=$API_URL
|
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=$ANON_KEY
|
|
SUPABASE_SERVICE_ROLE_KEY=$SERVICE_ROLE_KEY
|
|
SUPABASE_DB_URL=$DB_URL
|
|
SUPABASE_DB_ADMIN_URL=$DB_URL
|
|
|
|
NEXT_PUBLIC_APP_URL=$APP_URL
|
|
NEXT_PUBLIC_ADMIN_URL=$APP_URL
|
|
WAHA_WEBHOOK_BASE_URL=$APP_URL
|
|
|
|
INTERNAL_SECRET=$INTERNAL_SEC
|
|
IMPERSONATE_COOKIE_SECRET=$IMPERSONATE_SEC
|
|
CPF_ENCRYPTION_KEY=$CHAVE_CPF
|
|
WAHA_BYO_ENCRYPTION_KEY=$CHAVE_WAHA
|
|
AI_CRED_AES_KEY=$CHAVE_AI
|
|
|
|
WAHA_API_BASE_URL=http://waha:3000
|
|
WAHA_API_KEY=$WAHA_KEY
|
|
WAHA_API_KEY_SHA512=$WAHA_KEY_HASH
|
|
WAHA_HMAC_SECRET=$WAHA_HMAC
|
|
WAHA_WEBHOOK_REQUIRE_SIGNATURE=false
|
|
WHATSAPP_RESTART_ALL_SESSIONS=True
|
|
|
|
UPSTASH_REDIS_REST_URL=http://srh:80
|
|
UPSTASH_REDIS_REST_TOKEN=deskcomm-local-redis
|
|
SRH_TOKEN=deskcomm-local-redis
|
|
INTERNAL_CRON_SECRET=$INTERNAL_SEC
|
|
LGPD_SIGNING_KEY=$INTERNAL_SEC
|
|
APP_PORT=$APP_PORT
|
|
|
|
SENTRY_DSN=
|
|
EOF
|
|
|
|
step "Subindo os serviços do DeskcommCRM (App, Worker, WAHA, Redis)..."
|
|
./scripts/local-stack.sh up
|
|
|
|
step "Criando usuário Administrador..."
|
|
export NEXT_PUBLIC_SUPABASE_URL="$API_URL"
|
|
export SUPABASE_SERVICE_ROLE_KEY="$SERVICE_ROLE_KEY"
|
|
export OWNER_EMAIL="${OWNER_EMAIL:-admin@admin.com}"
|
|
# A senha NASCE ALEATÓRIA e é impressa no fim. A fixa que estava aqui vinha
|
|
# publicada neste repositório, e o `.env.local` acima aponta a aplicação para
|
|
# o IP da VM na rede (não 127.0.0.1): qualquer máquina da mesma rede alcançaria
|
|
# o CRM com uma credencial que está no GitHub. Quem quiser escolher a senha
|
|
# exporta OWNER_PASSWORD antes de chamar o script.
|
|
export OWNER_PASSWORD="${OWNER_PASSWORD:-$(openssl rand -base64 18)}"
|
|
export OWNER_ORG_NAME="Deskcomm Local"
|
|
export APP_LOCALE="pt-BR"
|
|
|
|
# Instala a dependência para rodar o bootstrap e executa
|
|
pnpm exec tsx scripts/bootstrap-owner.ts
|
|
|
|
printf '\n\n\033[32m================================================================\033[0m\n'
|
|
paint 32 "✅ Instalação concluída com sucesso!"
|
|
echo ""
|
|
echo "🌐 Acesse o sistema em: $APP_URL"
|
|
echo "🧭 Status: ./scripts/local-stack.sh status"
|
|
echo "🛑 Parar: ./scripts/local-stack.sh down"
|
|
echo ""
|
|
echo "🔑 Credenciais do Administrador:"
|
|
echo " E-mail: $OWNER_EMAIL"
|
|
echo " Senha: $OWNER_PASSWORD"
|
|
printf '\033[32m================================================================\033[0m\n'
|