feat: Fase 9/10 — métricas, scripts operacionais, callback/wrap-up e aceite final
Fase 9 (segurança e produção): - GET /api/metrics: endpoint Prometheus com métricas reais (chamadas, agentes, filas, CPS por campanha), protegido por permissão - scripts/backup.sh, restore.sh, healthcheck.sh, install.sh, update.sh — testados contra o ambiente real (backup.sh e healthcheck.sh rodados de verdade; install.sh/update.sh validados por inspeção, ambiente atual já provisionado) - POST /api/agent-console/dispose: aplica disposição de chamada de verdade (lacuna deixada aberta desde a Fase 6), com ações CALLBACK (agenda retorno) e DO_NOT_CALL (suprime automaticamente) - apps/dialer-worker/src/callback-sweep.ts: reativa leads com callback vencido; GET /api/callbacks para consulta - apps/dialer-worker/src/wrap-up-sweep.ts: transição automática WRAP_UP -> AVAILABLE + despausa real na fila do Asterisk. Exigiu corrigir main.ts para conectar ao AMI mesmo em DIALER_SIMULATION=true (DIALER_SIMULATION deve impedir só originação de chamada, não ações administrativas de fila) - nftables revisado (sem alterações necessárias) - Documentação completa: INSTALL, OPERATIONS, BACKUP_RESTORE, SECURITY, DATABASE, API, ASTERISK, OPENSIPS (não implementado, motivo documentado), TROUBLESHOOTING - README.md e CHANGELOG.md reescritos Fase 10 (testes e aceite): - Quality gate completo executado: build/typecheck (7 workspaces), lint, 46 testes unitários, docker compose config/ps, healthcheck — tudo verde - Aceite de segurança (seção 92): 13 itens verificados ao vivo contra o sistema real, não só por inspeção de código - Aceite Asterisk (seção 93): os 5 comandos executados e documentados, comunicação API->AMI->Asterisk validada - docs/RELATORIO_FINAL.md: relatório final no formato da seção 96 Todos os fixtures de teste desta fase foram removidos/desativados ao final. Credenciais de acesso entregues separadamente em CREDENCIAIS.txt (fora do git, nunca versionado).
This commit is contained in:
72
docs/API.md
Normal file
72
docs/API.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# B2BCall — Referência da API
|
||||
|
||||
Base URL (via Nginx): `http://<host>/api`
|
||||
Documentação interativa (Swagger, quando `SWAGGER_ENABLED=true`): `http://<host>/api/docs`
|
||||
|
||||
## Autenticação
|
||||
|
||||
Todas as rotas exigem um `access_token` válido (cookie HttpOnly, definido pelo
|
||||
login) **exceto** as marcadas `@Public()`:
|
||||
`/api/auth/login`, `/api/auth/refresh`, `/api/auth/forgot-password`,
|
||||
`/api/auth/reset-password`, `/api/health`, `/api/health/live`,
|
||||
`/api/health/ready`, `GET /api`.
|
||||
|
||||
Cada rota protegida também é validada contra o RBAC do usuário
|
||||
(`@RequirePermissions(...)`) — ver `packages/shared/src/permissions.ts` para
|
||||
o catálogo completo de chaves de permissão.
|
||||
|
||||
## Mapa de rotas
|
||||
|
||||
| Módulo | Rotas |
|
||||
|---|---|
|
||||
| `auth` | `POST /auth/login`, `/refresh`, `/logout`, `/change-password`, `/forgot-password`, `/reset-password`, `GET /auth/me` |
|
||||
| `users` | `GET /users`, `GET /users/:id`, `POST /users`, `PATCH /users/:id` |
|
||||
| `roles` | `GET /roles/permissions` (catálogo), `GET/POST/PATCH/DELETE /roles` |
|
||||
| `audit` | `GET /audit` (filtros: userId, action, entityType, from, to, paginação) |
|
||||
| `trunks` | `GET/POST/PATCH/DELETE /trunks`, `GET /trunks/:id/status` |
|
||||
| `extensions` | `GET/POST/PATCH/DELETE /extensions`, `POST /extensions/:id/reset-password` |
|
||||
| `dialplans` | `GET/POST/PATCH/DELETE /dialplans`, `GET /dialplans/versions`, `POST /dialplans/publish`, `POST /dialplans/versions/:id/rollback` |
|
||||
| `asterisk-admin` | `GET /asterisk/status`, `GET /asterisk/modules`, `GET /asterisk/diagnostic/allowed-commands`, `POST /asterisk/diagnostic`, `POST /asterisk/reload` |
|
||||
| `queues` | `GET/POST/PATCH/DELETE /queues`, `POST /queues/:id/members`, `DELETE /queues/:id/members/:agentId` |
|
||||
| `agents` | `GET/POST/PATCH/DELETE /agents` |
|
||||
| `agent-console` | `GET /agent-console/me`, `POST /login`, `/available`, `/pause`, `/unpause`, `/logout`, `/dispose` |
|
||||
| `pause-reasons` | `GET/POST/PATCH/DELETE /pause-reasons` |
|
||||
| `dispositions` | `GET/POST/PATCH/DELETE /dispositions` |
|
||||
| `callbacks` | `GET /callbacks?campaignId=` (somente leitura — agendamento via `agent-console/dispose`) |
|
||||
| `campaigns` | `GET/POST/PATCH/DELETE /campaigns`, `POST /:id/start`, `/pause`, `/stop`, `/drain` |
|
||||
| leads (sob campanhas) | `GET /campaigns/:campaignId/leads`, `GET .../imports`, `GET .../imports/:importId/rejected.csv`, `POST .../import` |
|
||||
| `suppression` | `GET/POST /suppression`, `POST /suppression/import`, `DELETE /suppression/:id` |
|
||||
| `reports` | `GET /reports/calls`, `/calls/export` (CSV), `/metrics`, `/agents/:agentId` |
|
||||
| `dashboard` | `GET /dashboard`, `/calls-by-hour`, `/campaigns/:id` |
|
||||
| `compliance` | `GET/PATCH /compliance/settings`, `GET /compliance/indicators` |
|
||||
| `monitoring` | `GET /monitoring/extensions`, `/queues`, `/agents` |
|
||||
| `metrics` | `GET /metrics` (Prometheus, texto plano) |
|
||||
| `health` | `GET /health`, `/health/live`, `/health/ready` |
|
||||
|
||||
## Convenções
|
||||
|
||||
- Paginação server-side em endpoints que retornam listas potencialmente
|
||||
grandes (`audit`, `reports/calls`, `suppression`): `page`, `pageSize`,
|
||||
resposta com `{ items, total, page, pageSize }`.
|
||||
- Erros nunca vazam detalhes internos: toda resposta de erro inclui
|
||||
`requestId` para correlação com o log estruturado do servidor.
|
||||
- Datas em ISO 8601 UTC; conversão de timezone de campanha
|
||||
(`America/Sao_Paulo` por padrão) acontece no backend, nunca no cliente.
|
||||
- Segredos (senha de ramal, senha de tronco) nunca retornam em `GET`/`PATCH`
|
||||
— só no `POST` de criação (senha de ramal) ou nunca em texto puro (senha
|
||||
de tronco, sempre `secretEncrypted` omitido da resposta).
|
||||
|
||||
## Exemplos rápidos
|
||||
|
||||
```bash
|
||||
# Login
|
||||
curl -c cookies.txt -X POST http://<host>/api/auth/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"admin@b2bcall.local","password":"..."}'
|
||||
|
||||
# Listar troncos (autenticado)
|
||||
curl -b cookies.txt http://<host>/api/trunks
|
||||
|
||||
# Métricas Prometheus
|
||||
curl -b cookies.txt http://<host>/api/metrics
|
||||
```
|
||||
94
docs/ASTERISK.md
Normal file
94
docs/ASTERISK.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# B2BCall — Asterisk
|
||||
|
||||
## Versão e módulos
|
||||
|
||||
Asterisk **22.10.1**, compilado do fonte (Debian trixie não empacota
|
||||
`asterisk`) — `infrastructure/docker/asterisk.Dockerfile`. Módulos
|
||||
habilitados: `chan_pjsip` (nunca `chan_sip`, removido/depreciado),
|
||||
`res_odbc`/`res_config_odbc` (Realtime), `app_queue`, `cdr_adaptive_odbc`,
|
||||
`cel_odbc`, AMI, ARI.
|
||||
|
||||
## Realtime (PJSIP via Postgres)
|
||||
|
||||
Objetos PJSIP (`ps_endpoints`, `ps_auths`, `ps_aors`, `ps_contacts`,
|
||||
`ps_endpoint_id_ips`, `ps_registrations`) vivem no schema `asterisk` do
|
||||
mesmo Postgres da aplicação — nunca em arquivo estático. Escritos
|
||||
exclusivamente por `apps/api/src/telephony/pjsip-realtime.service.ts`
|
||||
quando um Tronco/Ramal é criado/editado pela API.
|
||||
|
||||
- DSN ODBC: `infrastructure/asterisk/config/odbc.ini.tpl` → conexão nomeada
|
||||
`asterisk`, `ConnSettings = SET search_path TO asterisk, public;`.
|
||||
- `sorcery.conf`/`extconfig.conf` apontam os tipos PJSIP para essa conexão.
|
||||
- Depois de criar/editar um objeto, a API dispara reload seletivo via AMI
|
||||
(`pjsip reload`), nunca `core restart`.
|
||||
|
||||
## Dialplan e filas (gerados, versionados)
|
||||
|
||||
- `infrastructure/asterisk/config/extensions.conf` inclui
|
||||
`/etc/asterisk-generated/b2bcall-dialplan.conf` — gerado por
|
||||
`DialplanService` a partir de `DialplanEntry`/`DialplanVersion`
|
||||
(Postgres). Toda alteração é uma nova `DialplanVersion` com validação
|
||||
(`dialplan reload` + checagem de erro) e rollback automático se falhar.
|
||||
- `infrastructure/asterisk/config/queues.conf` inclui
|
||||
`/etc/asterisk-generated/b2bcall-queues.conf` — gerado a partir de
|
||||
`Queue` (Postgres). **Membros nunca são estáticos** — adicionados/
|
||||
removidos via AMI `QueueAdd`/`QueueRemove` no login/logout do agente
|
||||
(`AgentConsoleService`), para nunca ter duas fontes de verdade
|
||||
divergentes sobre quem está em qual fila.
|
||||
- Ambos os arquivos gerados vivem no volume Docker `dialplan-generated`,
|
||||
compartilhado entre `api` (escreve) e `asterisk` (lê + recarrega).
|
||||
- **Nunca editar esses dois arquivos gerados manualmente** — qualquer
|
||||
edição é sobrescrita na próxima publicação pela aplicação.
|
||||
|
||||
## AMI / ARI
|
||||
|
||||
- AMI: client próprio em `packages/telephony` (TCP raw, sem biblioteca de
|
||||
terceiros — o protocolo de wire do Asterisk 22 para a action `Command`
|
||||
usa headers `Output:` repetidos, não o formato legado
|
||||
`Response: Follows`/`--END COMMAND--` documentado em versões antigas).
|
||||
- Usado para: Originate, Hangup, QueueAdd/Remove, QueuePause,
|
||||
QueueStatus, ExtensionState/DeviceState, PJSIP show endpoints/contacts,
|
||||
reloads seletivos.
|
||||
- ARI: reservado para controle fino de canais/bridges quando necessário —
|
||||
não usado extensivamente nesta fase (a maior parte das operações
|
||||
administrativas é feita via AMI).
|
||||
- **Nunca expostos além de `127.0.0.1`/rede interna** — bloqueados da LAN
|
||||
por nftables mesmo estando em `network_mode: host` (ver
|
||||
`docs/ARCHITECTURE.md` §3.1 e `infrastructure/nftables/`).
|
||||
|
||||
## Comandos úteis de diagnóstico
|
||||
|
||||
```bash
|
||||
docker exec b2bcall-asterisk asterisk -rx "core show uptime"
|
||||
docker exec b2bcall-asterisk asterisk -rx "pjsip show endpoints"
|
||||
docker exec b2bcall-asterisk asterisk -rx "pjsip show registrations"
|
||||
docker exec b2bcall-asterisk asterisk -rx "queue show"
|
||||
docker exec b2bcall-asterisk asterisk -rx "dialplan show b2bcall-healthcheck"
|
||||
docker exec b2bcall-asterisk asterisk -rx "module show like odbc"
|
||||
docker exec b2bcall-asterisk asterisk -rx "odbc show all"
|
||||
```
|
||||
|
||||
Um subconjunto seguro e auditado desses comandos também é exposto via
|
||||
`GET /api/asterisk/diagnostic/allowed-commands` +
|
||||
`POST /api/asterisk/diagnostic` (allowlist explícita — nunca shell livre,
|
||||
seção 18) para uso pela interface web sem precisar SSH no servidor.
|
||||
|
||||
## Rede
|
||||
|
||||
`network_mode: host` (necessário para a faixa de portas RTP — mapear porta
|
||||
a porta em bridge Docker é inviável). Implicações e mitigação em
|
||||
`docs/ARCHITECTURE.md` §3.1.
|
||||
|
||||
## Limitações conhecidas nesta fase
|
||||
|
||||
- `res_pjsip_outbound_registration` pode logar erro no boot se nenhum
|
||||
registration estiver configurado ainda (nenhum tronco cadastrado) — não é
|
||||
um erro real, some ao cadastrar o primeiro tronco com `registration`.
|
||||
- `res_config_ldap` loga ERROR no boot (LDAP não configurado/não usado
|
||||
neste projeto) — módulo carregado por padrão pelo build, inofensivo.
|
||||
- `cdr_pgsql`/`cel_pgsql` (variante não-ODBC) não compilam nesta imagem por
|
||||
falta de `libpq-dev` — não é um problema porque `cdr_adaptive_odbc`/
|
||||
`cel_odbc` (a variante realmente usada) funcionam normalmente.
|
||||
- Nenhuma correlação automática CDR↔`DialAttempt` para chamadas **reais**
|
||||
(não simuladas) além do timeout de segurança da reconciliação — ver
|
||||
`docs/PREDICTIVE_DIALER.md`.
|
||||
79
docs/BACKUP_RESTORE.md
Normal file
79
docs/BACKUP_RESTORE.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# B2BCall — Backup e Restore
|
||||
|
||||
## O que é backupado
|
||||
|
||||
Um único `pg_dump -Fc` do banco `b2bcall` cobre **tudo que precisa
|
||||
persistir**:
|
||||
|
||||
- Schema `public` — usuários, campanhas, leads, chamadas, configurações.
|
||||
- Schema `asterisk` — troncos/ramais provisionados (ps_endpoints, ps_auths,
|
||||
ps_aors, ...), CDR, CEL.
|
||||
|
||||
Os arquivos gerados em `Telefonia → Dialplan` e `Call Center → Filas`
|
||||
(`/etc/asterisk-generated/*.conf` dentro do container do Asterisk) **não**
|
||||
precisam de backup separado: são derivados de `DialplanVersion`/`Queue`
|
||||
(Postgres) e recriados automaticamente na próxima publicação, caso o volume
|
||||
Docker seja perdido (agente.md seção 97: "Asterisk não é banco de negócio").
|
||||
|
||||
O `.env` é backupado à parte (contém segredos que não estão no banco:
|
||||
`JWT_*_SECRET`, `SECRETS_MASTER_KEY`, credenciais AMI/ARI/SMTP).
|
||||
|
||||
## Backup
|
||||
|
||||
```bash
|
||||
sudo ./scripts/backup.sh
|
||||
```
|
||||
|
||||
Gera em `backups/` (fora do git, `chmod 600`):
|
||||
|
||||
- `postgres-<timestamp>.dump` — dump `pg_dump -Fc` (formato custom, comprime
|
||||
e permite restore seletivo).
|
||||
- `env-<timestamp>.bak` — cópia do `.env`.
|
||||
- `FIRST_LOGIN-<timestamp>.txt` — se o arquivo ainda existir no servidor.
|
||||
|
||||
Retenção: remove automaticamente backups com mais de 30 dias
|
||||
(`BACKUP_RETENTION_DAYS` no ambiente para ajustar).
|
||||
|
||||
**Agendamento recomendado** (cron, fora do escopo deste repositório):
|
||||
|
||||
```cron
|
||||
0 3 * * * cd /opt/b2bcall && ./scripts/backup.sh >> /var/log/b2bcall-backup.log 2>&1
|
||||
```
|
||||
|
||||
Copie os backups para fora do servidor (outro host, storage externo) — um
|
||||
backup que só existe no mesmo disco do banco não protege contra falha de
|
||||
disco.
|
||||
|
||||
## Restore
|
||||
|
||||
```bash
|
||||
sudo ./scripts/restore.sh backups/postgres-20260101-030000.dump
|
||||
```
|
||||
|
||||
**Isso é destrutivo**: substitui completamente o conteúdo atual do banco
|
||||
(`pg_restore --clean --if-exists`). O script pede confirmação explícita
|
||||
(digitar "restaurar") antes de agir, para acidentes de digitação.
|
||||
|
||||
O que o script faz:
|
||||
1. Para os serviços de aplicação (`api`, `asterisk-events`, `dialer-worker`,
|
||||
`frontend`, `nginx`) — mantém `postgres`/`redis`/`asterisk` no ar.
|
||||
2. Executa `pg_restore` contra o Postgres em funcionamento.
|
||||
3. Reinicia os serviços de aplicação.
|
||||
|
||||
Após restaurar, rode `scripts/healthcheck.sh` e confira:
|
||||
- `pjsip show endpoints` no Asterisk reflete os troncos/ramais do backup.
|
||||
- Login funciona com um usuário que existia no momento do backup.
|
||||
|
||||
## Restaurando o `.env`
|
||||
|
||||
Se o `.env` também precisar ser restaurado (ex.: perda total do servidor):
|
||||
|
||||
```bash
|
||||
cp backups/env-<timestamp>.bak .env
|
||||
chmod 600 .env
|
||||
```
|
||||
|
||||
Isso restaura os segredos (JWT, master key de criptografia) — sem eles, as
|
||||
senhas de tronco/ramal cifradas no banco restaurado **não podem ser
|
||||
decifradas**. Por isso o `.env` deve ser guardado com a mesma prioridade
|
||||
que o dump do banco, não apenas como acessório.
|
||||
69
docs/DATABASE.md
Normal file
69
docs/DATABASE.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# B2BCall — Banco de Dados
|
||||
|
||||
PostgreSQL 17, um único cluster/instância com **dois schemas** que nunca se
|
||||
misturam (ver `docs/ARCHITECTURE.md` §3.3):
|
||||
|
||||
- **`public`** — domínio da aplicação (este documento). Gerenciado 100% por
|
||||
Prisma (`packages/database/prisma/schema.prisma` + `prisma/migrations/`).
|
||||
- **`asterisk`** — objetos de Realtime do Asterisk (`ps_endpoints`,
|
||||
`ps_auths`, `ps_aors`, `ps_contacts`, `ps_endpoint_id_ips`,
|
||||
`ps_registrations`, `cdr`, `cel`). Criado por
|
||||
`infrastructure/postgres/init/002-asterisk-realtime.sql` e escrito
|
||||
exclusivamente por `apps/api/src/telephony/pjsip-realtime.service.ts`
|
||||
(nunca por SQL solto em outro lugar da aplicação).
|
||||
|
||||
## Modelos (schema `public`)
|
||||
|
||||
| Modelo | Propósito |
|
||||
|---|---|
|
||||
| `User`, `Session`, `PasswordResetToken` | Autenticação (Fase 3) |
|
||||
| `Role`, `Permission`, `UserRole`, `RolePermission` | RBAC |
|
||||
| `AuditLog` | Auditoria de todas as ações sensíveis |
|
||||
| `Trunk`, `Extension`, `ExtensionState` | Telefonia (Fase 4) — `Trunk`/`Extension` são o CRUD da aplicação; os objetos PJSIP reais ficam no schema `asterisk` |
|
||||
| `DialplanEntry`, `DialplanVersion` | Dialplan estruturado versionado |
|
||||
| `Queue`, `QueueMember`, `PauseReason` | Call Center (Fase 5) |
|
||||
| `Agent`, `AgentSession`, `AgentStateEvent`, `AgentPauseEvent` | Máquina de estados do agente — sempre exatamente um `AgentStateEvent` aberto (`ended_at IS NULL`) por agente |
|
||||
| `Campaign`, `LeadImport`, `Lead`, `DialAttempt` | Discador preditivo (Fase 6). `DialAttempt` é a "chamada" como state machine (seção 36) — `id` próprio, nunca o `UNIQUEID` do Asterisk como PK de negócio |
|
||||
| `CallDisposition`, `Callback` | Disposição de chamada e agendamento de retorno |
|
||||
| `SuppressionEntry` | Lista de bloqueio (DNC) |
|
||||
| `ComplianceSettings` | Parâmetros de compliance (singleton) |
|
||||
|
||||
## Decisões importantes
|
||||
|
||||
- **Prisma como ORM** (não Drizzle/TypeORM) — ver justificativa em
|
||||
`docs/ARCHITECTURE.md` §3.6.1.
|
||||
- **Índices de alto volume**: `Lead(campaignId, status, nextAttemptAt)`,
|
||||
`DialAttempt` por `campaignId`/`state`/`asteriskUniqueId`,
|
||||
`AuditLog(createdAt)`, `AuditLog(entityType, entityId)`. Consultas de
|
||||
relatório usam paginação server-side sempre (nunca `SELECT *` sem filtro
|
||||
— seção 54).
|
||||
- **Reserva concorrente de leads**: `SELECT ... FOR UPDATE SKIP LOCKED` via
|
||||
`$queryRaw` em `apps/dialer-worker/src/lead-repository.ts` — não modelado
|
||||
via Prisma de alto nível porque a transição atômica READY→RESERVED
|
||||
precisa de controle fino de lock que o ORM não expõe com segurança
|
||||
suficiente sob concorrência real.
|
||||
- **Particionamento**: avaliado, não implementado (volume atual não
|
||||
justifica). `DialAttempt`/`AuditLog` são os candidatos naturais quando o
|
||||
volume crescer — desenhados para permitir particionamento por
|
||||
`started_at`/`created_at` sem migração destrutiva.
|
||||
- **Segredos**: `Trunk.secretEncrypted` e `Extension.sipPasswordEncrypted`
|
||||
são cifrados com AES-256-GCM (`packages/shared/src/secret-crypto.ts`),
|
||||
chave mestra em `SECRETS_MASTER_KEY` (.env, nunca no banco).
|
||||
|
||||
## Migrations
|
||||
|
||||
```bash
|
||||
cd packages/database
|
||||
pnpm exec prisma migrate dev --name <descrição> # desenvolvimento
|
||||
pnpm exec prisma migrate deploy # produção (usado por scripts/update.sh)
|
||||
pnpm run seed # permissões, perfis, bootstrap super_admin
|
||||
```
|
||||
|
||||
Nenhuma alteração de schema é feita manualmente em produção — sempre via
|
||||
migration versionada e commitada (seção 85).
|
||||
|
||||
## Backup / Restore
|
||||
|
||||
Ver `docs/BACKUP_RESTORE.md`. Resumo: `pg_dump -Fc` do banco inteiro cobre
|
||||
`public` **e** `asterisk` num único arquivo, já que ambos vivem na mesma
|
||||
instância Postgres.
|
||||
92
docs/INSTALL.md
Normal file
92
docs/INSTALL.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# B2BCall — Instalação
|
||||
|
||||
## Requisitos
|
||||
|
||||
- Debian 13 (trixie) limpo, acesso root.
|
||||
- **Mínimo para dev/testes/simulação**: 2 vCPU, 2 GiB RAM, 20 GiB disco.
|
||||
- **Recomendado para produção real com volume de discagem**: 4 vCPU, 8 GiB
|
||||
RAM (ver `docs/ARCHITECTURE.md` §1 — o ambiente original de
|
||||
desenvolvimento deste projeto tinha só 1.9 GiB e isso limitou decisões de
|
||||
arquitetura, como resource limits conservadores por container).
|
||||
- Rede: um IP privado para o servidor; troncos SIP/OpenSIPS acessíveis por
|
||||
IP privado (o Asterisk nunca tem IP público — seção 5).
|
||||
|
||||
## Instalação automatizada
|
||||
|
||||
```bash
|
||||
git clone <url-do-repositorio> /opt/b2bcall
|
||||
cd /opt/b2bcall
|
||||
sudo ./scripts/install.sh
|
||||
```
|
||||
|
||||
O script (`scripts/install.sh`) faz, em ordem:
|
||||
|
||||
1. Verifica que está rodando como root.
|
||||
2. Instala dependências de sistema (git, curl, openssl, Node.js 24 LTS via
|
||||
NodeSource, pnpm via corepack).
|
||||
3. Instala Docker Engine + Compose plugin (repositório oficial Docker,
|
||||
detecta o codename da distro automaticamente).
|
||||
4. Gera `.env` a partir de `.env.example` com segredos aleatórios fortes
|
||||
(`scripts/generate-secrets.sh`) — só na primeira instalação, nunca
|
||||
sobrescreve um `.env` existente.
|
||||
5. Instala as regras de firewall (`infrastructure/nftables/`) — protege
|
||||
AMI/ARI/SIP contra a LAN, nunca bloqueia SSH.
|
||||
6. Aplica `vm.overcommit_memory=1` (recomendado pelo Redis).
|
||||
7. `pnpm install` no monorepo.
|
||||
8. `docker compose build` (compila Asterisk do fonte — a etapa mais
|
||||
demorada, ~5-10 minutos dependendo do hardware).
|
||||
9. Sobe Postgres e Redis, aguarda ficarem saudáveis.
|
||||
10. Aplica migrations do Prisma e roda o seed (permissões, perfis,
|
||||
bootstrap do `super_admin`).
|
||||
11. Sobe Asterisk e os demais serviços (`docker compose up -d`).
|
||||
12. Roda `scripts/healthcheck.sh`.
|
||||
13. Imprime a URL de acesso e o caminho do `FIRST_LOGIN.txt`.
|
||||
|
||||
## Primeiro acesso
|
||||
|
||||
```bash
|
||||
cat FIRST_LOGIN.txt
|
||||
```
|
||||
|
||||
Contém e-mail e senha do `super_admin`, gerados aleatoriamente
|
||||
(agente.md seção 70) — nunca uma senha padrão. O primeiro login **exige**
|
||||
troca de senha (`mustChangePassword: true`).
|
||||
|
||||
**Remova o arquivo do servidor depois do primeiro acesso**:
|
||||
|
||||
```bash
|
||||
shred -u FIRST_LOGIN.txt # ou: rm -f FIRST_LOGIN.txt
|
||||
```
|
||||
|
||||
## Acesso
|
||||
|
||||
- Interface web: `http://<ip-do-servidor>/`
|
||||
- API: `http://<ip-do-servidor>/api` (Swagger em `/api/docs` se
|
||||
`SWAGGER_ENABLED=true`)
|
||||
|
||||
Nenhuma outra porta deve estar acessível pela LAN além de 80 (e 443 quando
|
||||
HTTPS for configurado — ver `docs/OPERATIONS.md`).
|
||||
|
||||
## Instalação manual (passo a passo, se preferir não usar o script)
|
||||
|
||||
Ver o conteúdo de `scripts/install.sh` — cada etapa pode ser executada
|
||||
isoladamente. Pontos que merecem atenção especial se for fazer manualmente:
|
||||
|
||||
- **Ordem de subida**: Postgres/Redis saudáveis → migrations → Asterisk →
|
||||
demais serviços. Subir tudo de uma vez com `docker compose up -d` também
|
||||
funciona (o `depends_on: condition: service_healthy` do
|
||||
`docker-compose.yml` já orquestra isso), mas as migrations/seed do Prisma
|
||||
precisam rodar manualmente contra o Postgres antes da API funcionar de
|
||||
verdade (a API não roda migrations automaticamente no boot — decisão
|
||||
deliberada para nunca alterar schema de produção sem um passo explícito).
|
||||
- **DATABASE_URL para comandos rodados do host** (fora dos containers):
|
||||
use `127.0.0.1` como host, não `postgres` — ver
|
||||
`docs/ARCHITECTURE.md` §3.2.1.
|
||||
|
||||
## Atualizando uma instalação existente
|
||||
|
||||
```bash
|
||||
sudo ./scripts/update.sh
|
||||
```
|
||||
|
||||
Ver `docs/OPERATIONS.md` para detalhes.
|
||||
51
docs/OPENSIPS.md
Normal file
51
docs/OPENSIPS.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# B2BCall — OpenSIPS (não implementado nesta fase)
|
||||
|
||||
A arquitetura alvo do projeto (agente.md seção 5) prevê OpenSIPS na frente
|
||||
do Asterisk:
|
||||
|
||||
```text
|
||||
INTERNET → [OpenSIPS] → rede SIP privada → [Asterisk] → [B2BCall]
|
||||
```
|
||||
|
||||
**Este componente não foi implantado neste ciclo.** O Asterisk hoje recebe
|
||||
tráfego SIP diretamente (protegido por nftables, sem IP público — ver
|
||||
`docs/ARCHITECTURE.md` §3.2). Isso é suficiente para o ambiente de
|
||||
laboratório atual (um servidor, troncos configurados diretamente no
|
||||
Asterisk), mas não entrega os benefícios que OpenSIPS traria:
|
||||
|
||||
- Balanceamento entre múltiplas instâncias de Asterisk.
|
||||
- Roteamento/normalização de múltiplos troncos antes de chegar ao core de
|
||||
telefonia.
|
||||
- Uma camada adicional de proteção SIP (rate limiting, topology hiding)
|
||||
antes do Asterisk.
|
||||
|
||||
## Por que não foi feito agora
|
||||
|
||||
Motivo honesto: com um único servidor e um único Asterisk, OpenSIPS não
|
||||
resolve nenhum problema real deste ambiente — adicionaria complexidade
|
||||
operacional (mais um serviço com seu próprio config/estado/observabilidade)
|
||||
sem benefício imediato. A decisão de projeto (agente.md seções 71/72) é não
|
||||
construir infraestrutura para um cenário hipotético que ainda não existe.
|
||||
|
||||
## Quando reconsiderar
|
||||
|
||||
Implantar OpenSIPS quando pelo menos uma destas condições existir:
|
||||
|
||||
1. Mais de uma instância de Asterisk precisar compartilhar o mesmo conjunto
|
||||
de troncos/roteamento.
|
||||
2. Necessidade de expor SIP a múltiplas operadoras com normalização de
|
||||
roteamento antes do Asterisk.
|
||||
3. Volume que justifique uma camada de proteção SIP dedicada, separada do
|
||||
Asterisk.
|
||||
|
||||
## Esboço de integração futura
|
||||
|
||||
- OpenSIPS ficaria na mesma rede privada do Asterisk (nunca IP público
|
||||
direto no Asterisk — isso não muda).
|
||||
- Trunks/roteamento no domínio da aplicação (`Trunk` no Postgres) já
|
||||
modelam host/porta/credenciais de forma genérica — o registro de
|
||||
domínios `Trunk.host` como sendo o OpenSIPS em vez do Asterisk-tronco
|
||||
direto é, na prática, uma mudança de configuração, não de schema.
|
||||
- Regras de firewall (`infrastructure/nftables/`) precisariam abrir a porta
|
||||
SIP do OpenSIPS para a origem confiável (operadora/OpenSIPS peer),
|
||||
mantendo o Asterisk inacessível de fora da rede interna.
|
||||
78
docs/OPERATIONS.md
Normal file
78
docs/OPERATIONS.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# B2BCall — Operação
|
||||
|
||||
## Comandos do dia a dia
|
||||
|
||||
| Tarefa | Comando |
|
||||
|---|---|
|
||||
| Ver status de tudo | `bash scripts/healthcheck.sh` |
|
||||
| Ver logs de um serviço | `docker compose logs -f api` (troque `api` pelo serviço) |
|
||||
| Reiniciar um serviço | `docker compose restart api` |
|
||||
| Aplicar atualização de código | `sudo ./scripts/update.sh` |
|
||||
| Backup manual | `sudo ./scripts/backup.sh` |
|
||||
| Restaurar backup | `sudo ./scripts/restore.sh backups/postgres-<ts>.dump` |
|
||||
| Console do Asterisk | `docker exec -it b2bcall-asterisk asterisk -rvvv` |
|
||||
| Ver regras de firewall ativas | `nft list table inet b2bcall_fw` |
|
||||
|
||||
## Atualização (`scripts/update.sh`)
|
||||
|
||||
1. `git pull --ff-only` (se for um clone git).
|
||||
2. `pnpm install --frozen-lockfile`.
|
||||
3. `prisma migrate deploy` contra o Postgres (via `127.0.0.1:5432`, fora dos
|
||||
containers).
|
||||
4. `docker compose build` — rebuilda só as imagens cujo contexto mudou
|
||||
(cache do Docker layer a layer).
|
||||
5. `docker compose up -d` — recria containers com imagem nova.
|
||||
6. `scripts/healthcheck.sh`.
|
||||
|
||||
Não há downtime zero: durante o rebuild/restart de `api`/`frontend`/`nginx`
|
||||
há uma janela curta de indisponibilidade (segundos). O Asterisk e as
|
||||
chamadas em andamento não são afetados a menos que a migration exija
|
||||
mudança incompatível em tabelas usadas pelo Realtime (raro — revisar a
|
||||
migration antes de rodar em produção com campanhas ativas).
|
||||
|
||||
## Rotina recomendada de produção
|
||||
|
||||
- **Backup diário** via cron (`docs/BACKUP_RESTORE.md`).
|
||||
- **Monitoramento**: apontar Prometheus para `http://api:3000/api/metrics`
|
||||
de dentro da rede Docker (`b2bcall-net`) — o endpoint exige uma sessão
|
||||
autenticada (permissão `monitoring.view`) e não é exposto via Nginx, então
|
||||
um scraper externo precisaria de uma integração dedicada (fora do escopo
|
||||
atual; hoje o uso pretendido é observabilidade interna/manual).
|
||||
- **Alertas mínimos recomendados**: `b2bcall_calls_abandoned_total` /
|
||||
`b2bcall_calls_total` acima do limiar de compliance (ver
|
||||
`GET /api/compliance/indicators`, que já calcula isso), containers não
|
||||
saudáveis (`docker compose ps`), espaço em disco (dumps + logs do
|
||||
Asterisk).
|
||||
- **Rotação de logs do Asterisk**: `/var/log/asterisk` dentro do volume
|
||||
`asterisk-log` cresce indefinidamente sem `logrotate` configurado — não
|
||||
incluído nesta fase (ver `TODO.md`, limitação conhecida).
|
||||
|
||||
## Escalando além do laboratório atual
|
||||
|
||||
O ambiente original (1.9 GiB RAM / 2 vCPU) é suficiente para desenvolvimento
|
||||
e para o modo `DIALER_SIMULATION=true`, não para volume real de discagem
|
||||
(`docs/ARCHITECTURE.md` §1). Para produção real:
|
||||
|
||||
1. Aumentar para pelo menos 4 vCPU / 8 GiB RAM.
|
||||
2. Revisar `mem_limit` de cada serviço em `docker-compose.yml` (hoje
|
||||
conservador para caber em 1.9 GiB).
|
||||
3. Considerar mover Postgres/Redis para fora do mesmo host do Asterisk se o
|
||||
volume de CDR/CEL crescer muito (arquitetura já separa os dois
|
||||
logicamente — schema `asterisk` dedicado — o que facilita migrar depois).
|
||||
4. Colocar OpenSIPS na frente do Asterisk para múltiplos troncos/roteamento
|
||||
avançado (não implementado nesta fase — ver `docs/OPENSIPS.md`).
|
||||
|
||||
## HTTPS
|
||||
|
||||
Não configurado nesta fase (a rede é privada, sem IP público — seção 5).
|
||||
Para expor com TLS:
|
||||
|
||||
1. Obter certificado (Let's Encrypt via DNS challenge, já que não há IP
|
||||
público para HTTP-01, ou certificado interno/self-signed para uso só na
|
||||
LAN).
|
||||
2. Editar `infrastructure/nginx/nginx.conf`: adicionar bloco
|
||||
`listen 443 ssl;` com `ssl_certificate`/`ssl_certificate_key`, e um
|
||||
bloco `listen 80` que só redireciona para 443.
|
||||
3. Publicar a porta 443 em `docker-compose.yml` (serviço `nginx`).
|
||||
4. Atualizar `infrastructure/nftables/` se necessário (443 já não é
|
||||
bloqueado por padrão, só os serviços administrativos do Asterisk são).
|
||||
157
docs/RELATORIO_FINAL.md
Normal file
157
docs/RELATORIO_FINAL.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# B2BCall — Relatório Final da Implementação
|
||||
|
||||
Formato conforme agente.md seção 96. Gerado em 2026-08-27 contra o ambiente
|
||||
real em execução (`Lab-FSCore-API`, 10.10.32.142).
|
||||
|
||||
```text
|
||||
B2BCall version: 0.1.0
|
||||
Asterisk version: 22.10.1
|
||||
Node version: v24.20.0
|
||||
PostgreSQL version: 17 (postgres:17-alpine)
|
||||
Docker version: 29.7.2 (Compose v5.5.0)
|
||||
```
|
||||
|
||||
## Containers
|
||||
|
||||
| Container | Status |
|
||||
|---|---|
|
||||
| b2bcall-postgres | healthy |
|
||||
| b2bcall-redis | healthy |
|
||||
| b2bcall-asterisk | healthy |
|
||||
| b2bcall-api | healthy |
|
||||
| b2bcall-frontend | healthy |
|
||||
| b2bcall-nginx | healthy |
|
||||
| b2bcall-asterisk-events | up (sem healthcheck HTTP formal — worker sem porta exposta) |
|
||||
| b2bcall-dialer-worker | up (idem) |
|
||||
|
||||
## URLs
|
||||
|
||||
- Interface web: `http://10.10.32.142/`
|
||||
- API: `http://10.10.32.142/api`
|
||||
- Health: `http://10.10.32.142/api/health`
|
||||
- Métricas (Prometheus, autenticado, uso interno): `http://10.10.32.142/api/metrics`
|
||||
|
||||
## Super admin
|
||||
|
||||
Ver arquivo local `CREDENCIAIS.txt` (fora do git, entregue separadamente —
|
||||
nunca colocamos senha em texto puro em um arquivo versionado, mesmo em um
|
||||
Git privado). Contém: usuário/senha do `super_admin` web e as credenciais
|
||||
do PostgreSQL/Redis.
|
||||
|
||||
## Health
|
||||
|
||||
```text
|
||||
$ bash scripts/healthcheck.sh
|
||||
docker compose ps: 8/8 containers up
|
||||
/api/health: {"status":"ok","postgres":"up","redis":"up","asterisk":"up"}
|
||||
Asterisk core show uptime: OK
|
||||
pjsip show endpoints: OK (comando responde; sem endpoints — fixtures de teste removidos)
|
||||
nftables (b2bcall_fw): ativo
|
||||
```
|
||||
|
||||
### Aceite Asterisk (seção 93)
|
||||
|
||||
```text
|
||||
$ asterisk -rx "core show version"
|
||||
Asterisk 22.10.1 built by root @ buildkitsandbox on a x86_64 running Linux on 2026-08-27 13:32:29 UTC
|
||||
|
||||
$ asterisk -rx "core show uptime"
|
||||
System uptime: 5 hours, 43 minutes, 19 seconds
|
||||
Last reload: 6 minutes, 54 seconds
|
||||
|
||||
$ asterisk -rx "pjsip show endpoints"
|
||||
No objects found.
|
||||
(esperado: todos os troncos/ramais de teste foram removidos ao final de cada fase)
|
||||
|
||||
$ asterisk -rx "pjsip show contacts"
|
||||
No objects found.
|
||||
|
||||
$ asterisk -rx "queue show"
|
||||
No queues.
|
||||
```
|
||||
|
||||
**Comunicação API → AMI → Asterisk**: validada via
|
||||
`GET /api/asterisk/status` →
|
||||
`{"amiControlConnection":"up","asteriskEventsHeartbeat":"up",...}`.
|
||||
|
||||
## Tests
|
||||
|
||||
```text
|
||||
Build/typecheck (pnpm -r build, 7 workspaces): PASS
|
||||
Lint (api, frontend): PASS
|
||||
Unit tests:
|
||||
apps/api 13/13 PASS
|
||||
apps/dialer-worker 33/33 PASS
|
||||
docker compose config: válida
|
||||
docker compose ps: 8/8 up
|
||||
scripts/healthcheck.sh: PASS
|
||||
```
|
||||
|
||||
### Aceite de segurança (seção 92) — verificado ao vivo, não só por inspeção
|
||||
|
||||
| Item | Resultado |
|
||||
|---|---|
|
||||
| Nenhuma senha no Git | ✅ (`git log --all -- .env` vazio) |
|
||||
| Nenhum `.env` commitado | ✅ |
|
||||
| PostgreSQL não público | ✅ (`127.0.0.1:5432` apenas) |
|
||||
| Redis não público | ✅ (nenhuma porta publicada) |
|
||||
| AMI não público | ✅ (nftables bloqueia `ens18:5038`) |
|
||||
| ARI não público | ✅ (nftables bloqueia `ens18:8088/8089`) |
|
||||
| Rate limit funcionando | ✅ (7ª tentativa de login seguida → HTTP 429) |
|
||||
| RBAC funcionando no backend | ✅ (papel `agent` → 403 em `/asterisk/status`, `/trunks`, `/users`) |
|
||||
| Agent não acessa configuração do Asterisk | ✅ |
|
||||
| Agent não eleva a própria permissão | ✅ |
|
||||
| SQL injection protegida | ✅ (Prisma parametrizado em toda parte) |
|
||||
| Path traversal protegido | ✅ (nenhum path de arquivo construído a partir de input do usuário) |
|
||||
| Logs sem credenciais | ✅ (redação do pino confirmada, varredura real sem vazamento) |
|
||||
|
||||
### Prova do dialer (seção 91)
|
||||
|
||||
- CPS configurado nunca excedido em nenhuma janela de 1s: ✅ (`simulation-harness.spec.ts`)
|
||||
- Concorrência máxima da campanha nunca excedida: ✅ (idem)
|
||||
- Pacing reage/reduz quando abandono sobe: ✅ (idem + bug real corrigido durante o desenvolvimento, ver `docs/PREDICTIVE_DIALER.md`)
|
||||
- Reprodutibilidade (mesma seed → mesmo resultado): ✅
|
||||
- Reserva de lead atômica (sem discagem dupla): ✅ (`FOR UPDATE SKIP LOCKED`, testado na Fase 6 contra Postgres real)
|
||||
- Campanha pausada não gera chamada nova: ✅ (testado na Fase 6)
|
||||
- Lead suprimido não é discado: ✅ (`isSuppressed` pré-originação, testado na Fase 6)
|
||||
- Campanha fora do horário não discou: ✅ (`schedule.spec.ts`, 6 testes)
|
||||
- Worker reiniciado recupera operação: ⚠️ parcial — `reconciliation.ts` tem teste unitário e roda em produção a cada 60s, mas o cenário "matar o processo de verdade no meio de uma chamada" não foi exercitado nesta sessão (ver Pending).
|
||||
|
||||
## Pending
|
||||
|
||||
Itens genuinamente pendentes, nunca escondidos:
|
||||
|
||||
1. **Integration tests automatizados** (Postgres/Redis/AMI reais via
|
||||
testcontainers ou equivalente) não existem como suíte formal — toda a
|
||||
verificação de integração foi feita manualmente contra containers reais
|
||||
a cada fase (real, mas não repetível em CI sem intervenção).
|
||||
2. **Recuperação após kill real do worker** não foi testada nesta sessão
|
||||
(só via teste unitário da lógica de reconciliação).
|
||||
3. **`scripts/install.sh`/`update.sh`** não foram testados de ponta a ponta
|
||||
num servidor limpo (o servidor atual já estava provisionado) — validados
|
||||
por inspeção linha a linha contra os comandos manuais já executados.
|
||||
4. **Verificação visual em navegador real** não foi feita (ambiente
|
||||
headless) — validado via `curl` reproduzindo as chamadas do navegador.
|
||||
5. **Tela de Callbacks** no frontend não existe — o agendamento e a
|
||||
reativação automática funcionam via API/worker (`POST /agent-console/
|
||||
dispose`, `GET /api/callbacks`, `callback-sweep.ts`), mas não há uma
|
||||
tela dedicada para visualizar a fila de callbacks pendentes.
|
||||
6. **AMD** (detecção de secretária eletrônica) — campo existe no schema,
|
||||
detecção real via app AMD do Asterisk não integrada à originação.
|
||||
7. **Correlação automática CDR↔DialAttempt** para chamadas reais (não
|
||||
simuladas) além do timeout de segurança de reconciliação.
|
||||
8. **WebSocket real** — painéis de monitoramento usam polling
|
||||
(5-15s), não um gateway WebSocket dedicado.
|
||||
9. **OpenSIPS** não implantado — decisão documentada em `docs/OPENSIPS.md`
|
||||
(não resolve nenhum problema real do ambiente atual de instância única).
|
||||
10. **HTTPS** não configurado (rede privada, sem IP público) — passo a
|
||||
passo em `docs/OPERATIONS.md`.
|
||||
11. `asterisk-events`/`dialer-worker` não têm healthcheck Docker formal
|
||||
(workers em background sem porta HTTP própria).
|
||||
12. Rotação de `SECRETS_MASTER_KEY` não é automatizada — exigiria
|
||||
redescriptografar todos os segredos de tronco/ramal manualmente.
|
||||
|
||||
Nenhum destes itens bloqueia o uso do sistema para o que foi pedido
|
||||
(discador preditivo funcional, call center completo, frontend completo,
|
||||
segurança e backup); são lacunas de robustez/automação a considerar antes
|
||||
de operar com volume real de produção.
|
||||
112
docs/SECURITY.md
Normal file
112
docs/SECURITY.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# B2BCall — Segurança
|
||||
|
||||
## Autenticação
|
||||
|
||||
- Senhas com **Argon2id** (`argon2` lib), nunca bcrypt/md5/sha.
|
||||
- JWT de acesso (curta duração, `JWT_ACCESS_TTL`) + refresh token opaco
|
||||
(hash SHA-256 armazenado, nunca o token em texto puro) com **rotação**: a
|
||||
cada uso, o refresh antigo é revogado e um novo par é emitido.
|
||||
- Cookies `HttpOnly`, `SameSite=Lax`, `Secure` quando HTTPS estiver ativo.
|
||||
`SameSite=Lax` é a mitigação primária de CSRF (o navegador não envia o
|
||||
cookie em requisições cross-site que não sejam navegação de topo) — não
|
||||
há token CSRF de double-submit adicional. Suficiente para o modelo de
|
||||
ameaça atual (rede privada, sem terceiros hospedando conteúdo que chame a
|
||||
API); reavaliar se o frontend algum dia rodar em uma origem
|
||||
publicamente diferente da API.
|
||||
- Rate limiting progressivo de login por IP via Redis
|
||||
(`LoginThrottleService`): backoff exponencial a cada bloco de tentativas
|
||||
falhas, nunca bloqueio permanente sem reset.
|
||||
- Resposta de "esqueci minha senha" sempre genérica, independente de o
|
||||
e-mail existir (evita enumeração de usuários).
|
||||
- Troca de senha revoga todas as sessões ativas do usuário.
|
||||
|
||||
## Autorização (RBAC)
|
||||
|
||||
- `User` → `UserRole` → `Role` → `RolePermission` → `Permission`.
|
||||
- Catálogo de permissões centralizado (`packages/shared/src/permissions.ts`)
|
||||
— nenhuma checagem de permissão usa string solta espalhada pelo código.
|
||||
- `PermissionsGuard` global, aplicado via `@RequirePermissions(...)` em
|
||||
cada rota que precisa (rotas sem decorator exigem apenas autenticação).
|
||||
- **Ninguém pode alterar os próprios perfis de acesso**
|
||||
(`users.service.ts#update`), mesmo tendo permissão de gerenciar usuários
|
||||
— bloqueia auto-elevação mesmo por um super_admin comprometido/mal
|
||||
configurado.
|
||||
|
||||
## Criptografia de segredos
|
||||
|
||||
- `Trunk.secretEncrypted` / `Extension.sipPasswordEncrypted`: AES-256-GCM
|
||||
(`packages/shared/src/secret-crypto.ts`), chave mestra
|
||||
`SECRETS_MASTER_KEY` (`.env`, nunca no banco, nunca versionada).
|
||||
- Nenhum segredo (senha de tronco, senha de ramal, tokens) é retornado em
|
||||
`GET`/`PATCH` — só uma vez, no momento da criação, quando aplicável.
|
||||
|
||||
## Auditoria
|
||||
|
||||
- `AuditService` registra toda ação sensível (login, criação/edição/exclusão
|
||||
de entidades, disposição de chamada, mudanças de compliance, etc.) com
|
||||
`userId`, `ipAddress`, `userAgent`, estado antes/depois.
|
||||
- Redação automática de campos sensíveis (`SENSITIVE_KEYS`) antes de
|
||||
persistir `before`/`after` — senha, secret, token nunca aparecem em texto
|
||||
puro no log de auditoria.
|
||||
- Logs estruturados JSON (`nestjs-pino`/`pino`) com `request_id` de
|
||||
correlação — mesma política de redação se aplica.
|
||||
|
||||
## Rede
|
||||
|
||||
- Único ponto exposto à LAN: Nginx, porta 80 (443 quando HTTPS configurado
|
||||
— `docs/OPERATIONS.md`).
|
||||
- Postgres/Redis nunca publicados fora de `127.0.0.1`/rede Docker interna.
|
||||
- AMI (5038), ARI (8088/8089), SIP (5060/5061), RTP (10000-20000) bloqueados
|
||||
da interface LAN por nftables (`infrastructure/nftables/b2bcall.nft`),
|
||||
mesmo o Asterisk rodando em `network_mode: host`. SSH nunca bloqueado.
|
||||
- CORS: `origin` restrito a uma allowlist configurável (`ALLOWED_ORIGINS`),
|
||||
`credentials: true` só para essas origens.
|
||||
- Helmet (`@fastify/helmet`) ativo: `X-Content-Type-Options`,
|
||||
`X-Frame-Options`, `Strict-Transport-Security`,
|
||||
`Cross-Origin-Opener-Policy`, etc.
|
||||
|
||||
## Validação de entrada
|
||||
|
||||
- Todo DTO usa `class-validator` (`ValidationPipe` global,
|
||||
`whitelist: true, forbidNonWhitelisted: true`) — payload com campo não
|
||||
esperado é rejeitado, não ignorado silenciosamente.
|
||||
- Toda query ao banco via Prisma (parametrizado) — as únicas duas exceções
|
||||
(`$queryRaw` em `lead-repository.ts` e `agent-call-binding.ts`) usam
|
||||
apenas parâmetros tipados interpolados pelo próprio Prisma
|
||||
(`$queryRaw\`...${valor}...\``), nunca concatenação de string.
|
||||
- Comandos de diagnóstico do Asterisk expostos via API usam **allowlist
|
||||
explícita** de comandos (`GET /api/asterisk/diagnostic/allowed-commands`)
|
||||
— nunca shell livre nem `asterisk -rx "<input do usuário>"` sem filtro.
|
||||
|
||||
## Checklist de aceite de segurança (agente.md seção 92)
|
||||
|
||||
| Item | Status |
|
||||
|---|---|
|
||||
| Senhas com hash forte (Argon2id) | ✅ |
|
||||
| Nenhuma senha padrão (bootstrap sempre aleatório) | ✅ |
|
||||
| RBAC aplicado no backend (não só escondido na UI) | ✅ |
|
||||
| Agente não eleva a própria permissão | ✅ |
|
||||
| Segredos de tronco/ramal cifrados em repouso | ✅ |
|
||||
| Postgres/Redis não expostos à LAN | ✅ |
|
||||
| AMI/ARI/SIP não expostos à LAN | ✅ (nftables) |
|
||||
| Rate limiting de login | ✅ |
|
||||
| Auditoria de ações sensíveis, sem vazar segredos | ✅ |
|
||||
| Logs estruturados sem credenciais | ✅ |
|
||||
| `.env`/segredos nunca versionados no git | ✅ |
|
||||
| Validação de entrada em todos os DTOs | ✅ |
|
||||
| Sem SQL injection (Prisma parametrizado em toda parte) | ✅ |
|
||||
| Sem shell injection (allowlist de comandos Asterisk) | ✅ |
|
||||
| CSRF | ⚠️ Mitigado via `SameSite=Lax`, sem token dedicado (ver acima) |
|
||||
| HTTPS | ⚠️ Não configurado nesta fase (rede privada) — ver `docs/OPERATIONS.md` |
|
||||
|
||||
## Como reportar/tratar um incidente
|
||||
|
||||
1. Revogar sessões: `UPDATE sessions SET revoked_at = now() WHERE revoked_at IS NULL;`
|
||||
(ou endpoint de logout em massa, se necessário adicionar).
|
||||
2. Rotacionar segredos: gerar novos valores com
|
||||
`scripts/generate-secrets.sh`, atualizar `.env`, reiniciar `api`.
|
||||
Rotacionar `SECRETS_MASTER_KEY` exige redescriptografar e recriptografar
|
||||
todos os `Trunk.secretEncrypted`/`Extension.sipPasswordEncrypted` (não
|
||||
automatizado nesta fase — script futuro se necessário).
|
||||
3. Consultar `AuditLog` filtrando por `userId`/`entityType`/janela de tempo
|
||||
para reconstituir o que aconteceu.
|
||||
108
docs/TROUBLESHOOTING.md
Normal file
108
docs/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# B2BCall — Troubleshooting
|
||||
|
||||
Problemas reais encontrados durante o desenvolvimento deste projeto e como
|
||||
foram diagnosticados/resolvidos — mantido como referência para o futuro.
|
||||
|
||||
## "Argument calledNumber is missing" ao originar chamada
|
||||
|
||||
**Sintoma**: `PrismaClientValidationError` ao reservar um lead, mesmo a
|
||||
query SQL parecendo correta.
|
||||
|
||||
**Causa**: `$queryRaw` do Prisma **não aplica** o mapeamento camelCase
|
||||
automático que `findMany`/`update` aplicam — colunas retornadas por SQL cru
|
||||
mantêm o nome exato da coluna (`phone_normalized`, não `phoneNormalized`).
|
||||
|
||||
**Fix**: sempre usar alias explícito no SQL: `phone_normalized AS
|
||||
"phoneNormalized"`. Ver `apps/dialer-worker/src/lead-repository.ts`.
|
||||
|
||||
## Pacing do discador oscilando/nunca parando de subir mesmo sem atividade
|
||||
|
||||
**Sintoma**: no cenário de teste da seção 66 (20 agentes/10 CPS/30% taxa de
|
||||
atendimento), o `pacingFactor` continuava subindo mesmo em períodos ociosos,
|
||||
gerando concorrência muito acima do esperado.
|
||||
|
||||
**Causas** (três, compostas):
|
||||
1. O cálculo de concorrência total não incluía chamadas já conectadas ao
|
||||
agente (`agentConnectedCalls`), subestimando quanto já estava "em uso".
|
||||
2. O ajuste de pacing subia mesmo sem nenhuma atividade de discagem no
|
||||
ciclo (nada "deu errado", então ele interpretava como "pode subir mais").
|
||||
3. Uma única iteração fechava 100% da diferença entre o alvo e o atual,
|
||||
causando overshoot.
|
||||
|
||||
**Fix**: `agentConnectedCalls` entra no total de concorrência; pacing nunca
|
||||
sobe durante ociosidade (`hasActivity` gate); fechamento de gap limitado a
|
||||
50% por ciclo (`RAMP_FRACTION`); e dampening por raiz quadrada do próprio
|
||||
`pacingFactor`. Ver `apps/dialer-worker/src/predictive-engine.ts` e
|
||||
`docs/PREDICTIVE_DIALER.md`.
|
||||
|
||||
## AMI: comando `Command` não retorna o output esperado
|
||||
|
||||
**Sintoma**: parsing manual do protocolo AMI para a action `Command`
|
||||
(usada por diagnóstico) não batia com a documentação legada
|
||||
(`Response: Follows` / `--END COMMAND--`).
|
||||
|
||||
**Causa**: o Asterisk 22 usa um formato diferente para `Command`: múltiplos
|
||||
headers `Output:` repetidos, um por linha de saída — não o bloco
|
||||
`Follows`/`END COMMAND` de versões antigas.
|
||||
|
||||
**Fix**: reverse-engineering via socket TCP cru (script de debug dedicado)
|
||||
para confirmar o formato real antes de implementar o parser em
|
||||
`packages/telephony`.
|
||||
|
||||
## Erro de sintaxe no `schema.prisma`: `@default(1_000_000)`
|
||||
|
||||
**Sintoma**: `prisma generate` falha com erro de parsing.
|
||||
|
||||
**Causa**: separador de milhar com underscore (`1_000_000`) não é suportado
|
||||
na sintaxe do `.prisma` — é um recurso de JS/TS, não do DSL do Prisma.
|
||||
|
||||
**Fix**: escrever o número sem separadores: `1000000`.
|
||||
|
||||
## `Prisma.InputJsonValue` rejeita instância de DTO
|
||||
|
||||
**Sintoma**: `Index signature for type 'string' is missing` ao passar um
|
||||
objeto DTO diretamente para um campo `Json` do Prisma (ex.: `AuditLog.after`).
|
||||
|
||||
**Causa**: uma instância de classe (mesmo com os campos certos) não
|
||||
satisfaz a assinatura de índice que `InputJsonValue` exige — só um objeto
|
||||
literal simples satisfaz.
|
||||
|
||||
**Fix**: espalhar em um objeto literal: `after: { ...dto }` em vez de
|
||||
`after: dto`.
|
||||
|
||||
## Redis: agentes travados em `IN_CALL` para sempre
|
||||
|
||||
**Sintoma**: depois de uma chamada, o agente nunca voltava a `AVAILABLE`.
|
||||
|
||||
**Causa**: o "claim" de agente disponível só marcava `IN_CALL` na conexão —
|
||||
não havia nenhum mecanismo simétrico de liberação após o fim da chamada.
|
||||
|
||||
**Fix**: `agent-call-binding.ts#releaseAgentAfterCall`, chamado
|
||||
explicitamente quando a chamada termina, com suporte a `WRAP_UP`
|
||||
intermediário antes de `AVAILABLE`.
|
||||
|
||||
## Corrida entre duas campanhas reivindicando o mesmo agente
|
||||
|
||||
**Sintoma** (encontrado em revisão de código, não em produção):
|
||||
implementação inicial de `claimAvailableAgent` usava `findFirst` + update
|
||||
separado — janela de corrida entre duas campanhas concorrentes.
|
||||
|
||||
**Fix**: reescrito com `UPDATE ... WHERE id = (SELECT ... FOR UPDATE SKIP
|
||||
LOCKED LIMIT 1) RETURNING ...`, mesmo padrão atômico já usado para reserva
|
||||
de leads.
|
||||
|
||||
## Diagnóstico geral
|
||||
|
||||
Para qualquer problema não listado acima:
|
||||
|
||||
1. `bash scripts/healthcheck.sh` — descarta problema de infraestrutura
|
||||
básica primeiro.
|
||||
2. `docker compose logs -f <serviço>` — logs estruturados JSON, procurar
|
||||
por `"level":50` (error) ou `"level":40` (warn).
|
||||
3. `docker exec b2bcall-asterisk asterisk -rx "..."` — ver `docs/ASTERISK.md`
|
||||
para comandos úteis.
|
||||
4. Consultar `AuditLog` (`GET /api/audit`) se o problema envolve uma ação
|
||||
específica de um usuário.
|
||||
5. Verificar `docker compose ps` — um container "unhealthy" quase sempre
|
||||
aponta a causa raiz de problemas em cascata nos serviços que dependem
|
||||
dele.
|
||||
Reference in New Issue
Block a user