Files
DeskcommCRM/ubuntu-local-installer.sh
melgarafaelandClaude Opus 5 9da23dea56 fix(dev): a chave do WAHA nasce aleatória, e o painel dele não atende a rede
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
2026-09-20 23:51:05 -03:00

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'