Inclui console handoff Desk↔Console (Spec 019), melhorias DNS Viewer (037), OpenPanel/Nextcloud/VM116 deploy notes, contracts stack e sidebar actualizado. Co-authored-by: Cursor <cursoragent@cursor.com>
308 lines
13 KiB
Markdown
308 lines
13 KiB
Markdown
# Anexo 004 — Cloudflare: inserir domínio na conta Ligbox (wizard VM112)
|
|
|
|
**Parent:** [spec.md](./spec.md) · Spec **004** onboard-funnel-events
|
|
**Criado:** 2026-06-22 · **Roger**
|
|
**VM:** **112** (`10.10.10.112`) · API wizard `:8090`
|
|
**URL pública:** `https://onboard.ligbox.com.br/api/onboarding/`
|
|
|
|
Este anexo documenta **onde vive o código**, **funções**, **endpoints**, **fluxo UI** e **specs relacionadas** para criar/inserir um domínio na **conta Cloudflare Ligbox/Ibytera** durante o onboarding (opção «Trazer DNS para o portal»).
|
|
|
|
---
|
|
|
|
## Resumo operacional
|
|
|
|
| Acção | Quem | Quando no wizard |
|
|
|-------|------|------------------|
|
|
| Validar domínio (formato + Carbonio) | `POST /validate-domain` | Passo 0 — antes do CF |
|
|
| **Criar zona na conta CF** | `POST /dns/cloudflare/provision-zone` | Passo DNS — «Trazer para o portal» |
|
|
| Aplicar MX/SPF/DKIM/… | `POST /dns/cloudflare/apply` | Após NS no registrador (ou auto sandbox) |
|
|
| Verificar propagação | `GET /dns/verify/{domain}` | Antes de criar conta |
|
|
| Emitir `dns.applied` (funil Desk) | `apply_cloudflare_dns` router | Após apply OK — ver [research.md](./research.md) |
|
|
|
|
**Nota:** `provision-zone` **cria a zona**; `apply` **escreve registos DNS** dentro da zona. São passos distintos.
|
|
|
|
**Spec 037 (2026-06-22):** três contas Ligbox (`ligit`, `itecnologys`, `ibytera`). `GET /dns/resolve/{domain}` descobre a conta; domínio fora das três → BYO (`connect-custom`) ou registrador. Ver [037-dns-multi-cloudflare-orchestration](../037-dns-multi-cloudflare-orchestration/spec.md).
|
|
|
|
---
|
|
|
|
## Onde vive o código (VM112)
|
|
|
|
| Caminho | Papel |
|
|
|---------|--------|
|
|
| `/opt/ligbox-wizard/backend/app/services/cloudflare.py` | Cliente API Cloudflare (`CloudflareDNS`) |
|
|
| `/opt/ligbox-wizard/backend/app/routers/onboarding.py` | Rotas HTTP onboarding + hooks webhook funil |
|
|
| `/opt/ligbox-wizard/backend/app/services/dns_verify.py` | Verificação MX/A após apply |
|
|
| `/opt/ligbox-wizard/backend/app/config.py` | `cloudflare_account_id`, `load_cloudflare_token_from_secrets()` |
|
|
| `/opt/ligbox-wizard/secrets/cloudflare.token` | Token API (não commitar) |
|
|
| `/opt/ligbox-wizard/secrets/README.txt` | Instruções token |
|
|
| Frontend: `frontend/src/App.jsx` (ou deploy equivalente) | `choosePortalDns()`, `applyPortalDnsRecords()` |
|
|
| Serviço: `systemctl status ligbox-wizard` | API `:8090` |
|
|
|
|
**Repo espelho (dev):** `ibytera-mail-portal` / `ligbox-wizard` no monorepo ou LAPTOP — deploy para VM112 via `scp` + restart.
|
|
|
|
**Desk VM122 (só leitura):** `projects/ops-desk/api/app/cloudflare_dns.py` — `GET /api/v1/dns/cloudflare/records` — **não cria zona**.
|
|
|
|
---
|
|
|
|
## Configuração e credenciais
|
|
|
|
| Variável / ficheiro | Descrição |
|
|
|---------------------|-----------|
|
|
| `secrets/cloudflare.token` | API Token Cloudflare (conta Ligbox/Ibytera) |
|
|
| `settings.cloudflare_account_id` | Account ID onde zonas são criadas (`POST /zones` body `account.id`) |
|
|
| `settings.mail_public_ip` | IP para registos A/MX em `mail_dns_records()` |
|
|
| `settings.onboard_sandbox` | Se true → modo sandbox (sem CF real) |
|
|
| Token prefix `sandbox_` | Também activa `is_cloudflare_sandbox()` |
|
|
|
|
### Constitution — segurança token
|
|
|
|
Fonte: [`.specify/memory/constitution.md`](../../.specify/memory/constitution.md)
|
|
|
|
> **Cloudflare API:** Tokens com scope mínimo (zone DNS, **não** account-wide).
|
|
|
|
**Scopes mínimos para provision-zone (produção):**
|
|
|
|
| Scope | Motivo |
|
|
|-------|--------|
|
|
| `Account - Zone - Edit` | Criar zona na conta |
|
|
| `Zone - Create` | `POST /zones` |
|
|
| `Zone - DNS - Edit` | `upsert_record` (MX, SPF, TXT, A) |
|
|
| `Zone - Read` | `get_zone_by_name`, list records |
|
|
|
|
Mensagem de erro no código se faltar permissão:
|
|
|
|
```text
|
|
Token sem permissão para criar zonas.
|
|
Adicione «Account - Zone - Edit» e «Zone - Create» ao API token Ibytera.
|
|
```
|
|
|
|
---
|
|
|
|
## Funções que criam/inserem a zona (conta CF)
|
|
|
|
Ficheiro: `backend/app/services/cloudflare.py`
|
|
|
|
### Helpers
|
|
|
|
| Função | Descrição |
|
|
|--------|-----------|
|
|
| `is_cloudflare_sandbox(token?)` | `True` se `onboard_sandbox` ou token `sandbox_*` |
|
|
| `sandbox_zone(domain)` | Zona fictícia para testes (sem API CF) |
|
|
| `wizard_nameservers(zone)` | Extrai NS da zona para UI |
|
|
| `mail_dns_records(domain, mail_ip, aliases?)` | Lista dicts MX/SPF/DKIM/A para apply |
|
|
|
|
### Classe `CloudflareDNS`
|
|
|
|
| Método | API CF | Descrição |
|
|
|--------|--------|-----------|
|
|
| `verify_token()` | `GET /user/tokens/verify` ou equivalente | Valida token antes de operações |
|
|
| `get_zone_by_name(domain)` | `GET /zones?name=` | Procura zona na conta |
|
|
| **`create_zone(domain, account_id?)`** | **`POST /zones`** | **Cria zona `type: full` na conta Ibytera**; se já existe (1061/1093) devolve existente |
|
|
| **`ensure_zone(domain)`** | composto | **`get_zone_by_name` → se null → `create_zone`** — **função principal de inserção** |
|
|
| `list_zone_names()` | `GET /zones` paginado | Lista domínios na conta |
|
|
| `list_records(zone_id)` | `GET /zones/{id}/dns_records` | Registos actuais |
|
|
| `upsert_record(zone_id, type, name, content, …)` | POST/PUT dns_records | Idempotente (81058 = já existe) |
|
|
| `fqdn(name, zone_name)` | — | Normaliza `@` e subdomínios |
|
|
|
|
### Corpo `create_zone` (produção)
|
|
|
|
```json
|
|
{
|
|
"name": "exemplo.com.br",
|
|
"account": { "id": "<cloudflare_account_id>" },
|
|
"type": "full",
|
|
"jump_start": false
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Endpoints API (wizard — passo DNS onboard)
|
|
|
|
Base: `/api/onboarding` · Header obrigatório: `X-Onboarding-Session: <session_id>`
|
|
|
|
| Método | Path | Handler | Função |
|
|
|--------|------|---------|--------|
|
|
| POST | `/validate-domain` | `validate_domain` | Valida domínio; emite `onboarding.started` + `domain.validated` |
|
|
| GET | `/dns/cloudflare/status/{domain}` | `cloudflare_zone_status` | Estado zona (configurado?, NS, pending) |
|
|
| **POST** | **`/dns/cloudflare/provision-zone`** | **`provision_cloudflare_zone`** | **`ensure_zone(domain)` — insere na conta CF** |
|
|
| **POST** | **`/dns/cloudflare/apply`** | **`apply_cloudflare_dns`** | Aplica registos mail; emite **`dns.applied`** |
|
|
| GET | `/dns/verify/{domain}` | `verify_dns` | Verifica propagação DNS pública |
|
|
| GET | `/dns/portal-onboarding/{domain}` | `portal_dns_onboarding` | Payload UI passos NS |
|
|
| GET | `/dns/instructions/{domain}` | `dns_instructions` | Instruções alternativas DNS |
|
|
| POST | `/account/create` | `create_account` | Conta Carbonio + infra (após DNS OK) |
|
|
|
|
### `provision-zone` — request/response
|
|
|
|
**Request:**
|
|
|
|
```json
|
|
{ "domain": "exemplo.com.br" }
|
|
```
|
|
|
|
**Response (campos principais):**
|
|
|
|
```json
|
|
{
|
|
"domain": "exemplo.com.br",
|
|
"zone_id": "…",
|
|
"nameservers": ["…ns.cloudflare.com", "…"],
|
|
"message": "Domínio … criado na Cloudflare Ligbox",
|
|
"sandbox": false,
|
|
"status": { "nameservers": ["…"] },
|
|
"steps": [ … ],
|
|
"verification": { … }
|
|
}
|
|
```
|
|
|
|
Em **sandbox**, `provision-zone` pode também aplicar registos simulados e preencher `verification`.
|
|
|
|
### `apply` — request
|
|
|
|
```json
|
|
{
|
|
"domain": "exemplo.com.br",
|
|
"zone_id": "opcional-se-ja-conhecido",
|
|
"mail_aliases": ["suporte", "vendas"]
|
|
}
|
|
```
|
|
|
|
Se zona não existir na conta → HTTP **422** `zone_not_in_account` + `use_portal_onboarding: true`.
|
|
|
|
---
|
|
|
|
## Fluxo no wizard (UI)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant U as Cliente
|
|
participant UI as Wizard App.jsx
|
|
participant API as VM112 :8090
|
|
participant CF as Cloudflare API
|
|
participant OPS as Desk VM122
|
|
|
|
U->>UI: Passo 0 — domínio
|
|
UI->>API: POST /validate-domain
|
|
API-->>OPS: webhook domain.validated (+ started 1x)
|
|
|
|
U->>UI: Escolhe «DNS no portal Ligbox»
|
|
UI->>API: POST /dns/cloudflare/provision-zone
|
|
API->>CF: ensure_zone (create se necessário)
|
|
CF-->>API: zone_id + nameservers
|
|
API-->>UI: nameservers + passos
|
|
|
|
U->>U: Altera NS no registrador
|
|
UI->>API: POST /dns/cloudflare/apply
|
|
API->>CF: upsert_record (MX, SPF, …)
|
|
API-->>OPS: webhook dns.applied
|
|
UI->>API: GET /dns/verify/{domain}
|
|
|
|
U->>UI: Passo conta admin
|
|
UI->>API: POST /account/create
|
|
```
|
|
|
|
### Funções frontend (referência)
|
|
|
|
| Função JS | Endpoint |
|
|
|-----------|----------|
|
|
| `choosePortalDns()` | `POST /onboarding/dns/cloudflare/provision-zone` |
|
|
| `applyPortalDnsRecords()` / `applyPortalDns()` | `POST /onboarding/dns/cloudflare/apply` |
|
|
| `refreshCfStatus()` | `GET /onboarding/dns/cloudflare/status/{domain}` |
|
|
|
|
Ficheiro típico: `frontend/src/App.jsx` (passo DNS do onboard).
|
|
|
|
---
|
|
|
|
## Integração funil Desk (Spec 004)
|
|
|
|
| Marco wizard | Evento webhook | Hook em `onboarding.py` |
|
|
|--------------|----------------|------------------------|
|
|
| Primeiro validate-domain OK | `onboarding.started` | `validate_domain` |
|
|
| validate-domain OK | `domain.validated` | `validate_domain` |
|
|
| **apply CF OK** | **`dns.applied`** | **`apply_cloudflare_dns`** (~L592) |
|
|
| Infra pós-conta | `infra.synced` | `create_account` |
|
|
| Fim OK | `onboarding.completed` | `create_account` |
|
|
| Erro crítico | `onboarding.failed` | `create_account` except |
|
|
|
|
Fase funil: `domain_validated` → **`dns_applied`** → `account_created` → …
|
|
|
|
Ver [research.md](./research.md) tabela hook points.
|
|
|
|
**Gap conhecido:** `provision-zone` **não** emite evento próprio — só cria zona. O funil avança para `dns_applied` no **apply**.
|
|
|
|
---
|
|
|
|
## Specs e documentos relacionados
|
|
|
|
| # | Documento | Relação com Cloudflare onboard |
|
|
|---|-----------|-------------------------------|
|
|
| **004** | [spec.md](./spec.md) | Funil + `dns.applied` |
|
|
| **004** | [research.md](./research.md) | Hook `apply_cloudflare_dns` |
|
|
| **004** | [contracts/webhook-funnel-events.md](./contracts/webhook-funnel-events.md) | Payload eventos funil |
|
|
| **001** | [specs/001-webhook-vm112-integration/spec.md](../001-webhook-vm112-integration/spec.md) | Webhook base + `domain.validated` |
|
|
| **012** | [specs/012-abandoned-onboarding-lead/spec.md](../012-abandoned-onboarding-lead/spec.md) | `onboarding.started` em `account/create` (não validate) |
|
|
| **016** | [specs/016-onboard-self-service-prefill/spec.md](../016-onboard-self-service-prefill/spec.md) | Pré-preenche domínio passo 0 |
|
|
| **025** | [specs/025-wizard-onboarding-continuity/spec.md](../025-wizard-onboarding-continuity/spec.md) | Fluxo domínio/DNS idempotente |
|
|
| **017** | [specs/017-vm112-domain-orchestration/spec.md](../017-vm112-domain-orchestration/spec.md) | **Purge** — apaga zona CF (passo 5) |
|
|
| **010** | [specs/010-desk-assist-takeover/spec.md](../010-desk-assist-takeover/spec.md) | `dns.revalidate` / assist passo DNS |
|
|
| **010** | [specs/010-admin-domain-validation/spec.md](../010-admin-domain-validation/spec.md) | Checks infra (não cria zona) |
|
|
| **019** | [specs/019-email-migration-vm122-execution/spec.md](../019-email-migration-vm122-execution/spec.md) | MX CF bloqueado até gate DNS |
|
|
| **035** | [specs/035-ligbox-mail-bundles-foss-openpanel/spec.md](../035-ligbox-mail-bundles-foss-openpanel/spec.md) | Wizard: MX, SPF, DMARC records |
|
|
| **027** | [specs/027-desk-rbac-function-matrix/spec.md](../027-desk-rbac-function-matrix/spec.md) | RBAC `cloudflare_dns.read` Desk |
|
|
| — | [constitution.md](../../.specify/memory/constitution.md) | Token CF scope mínimo |
|
|
| — | `chat-bruto/CHAT_BRUTO_WIZARD_VM112_20260619.txt` | E2E `provision-zone`, sandbox, fixes |
|
|
|
|
---
|
|
|
|
## Testes rápidos (curl)
|
|
|
|
```bash
|
|
SESSION="test-$(date +%s)"
|
|
BASE="https://onboard.ligbox.com.br/api/onboarding"
|
|
HDR=(-H "X-Onboarding-Session: $SESSION" -H "Content-Type: application/json")
|
|
|
|
# 1. Validar domínio
|
|
curl -s -X POST "$BASE/validate-domain" "${HDR[@]}" \
|
|
-d '{"domain":"exemplo.com.br"}' | jq .
|
|
|
|
# 2. Estado CF
|
|
curl -s "$BASE/dns/cloudflare/status/exemplo.com.br" "${HDR[@]}" | jq .
|
|
|
|
# 3. Inserir zona na conta Ligbox
|
|
curl -s -X POST "$BASE/dns/cloudflare/provision-zone" "${HDR[@]}" \
|
|
-d '{"domain":"exemplo.com.br"}' | jq .
|
|
|
|
# 4. Aplicar registos mail (após NS)
|
|
curl -s -X POST "$BASE/dns/cloudflare/apply" "${HDR[@]}" \
|
|
-d '{"domain":"exemplo.com.br"}' | jq .
|
|
|
|
# 5. Verificar DNS
|
|
curl -s "$BASE/dns/verify/exemplo.com.br" "${HDR[@]}" | jq .
|
|
```
|
|
|
|
LAN directo: substituir host por `http://10.10.10.112:8090/api/onboarding`.
|
|
|
|
---
|
|
|
|
## Erros comuns
|
|
|
|
| Sintoma | Causa provável | Acção |
|
|
|---------|----------------|-------|
|
|
| `Token Cloudflare não configurado` | Falta `secrets/cloudflare.token` | Criar token + restart wizard |
|
|
| `Token sem permissão para criar zonas` | Scopes insuficientes | Account Zone Edit + Zone Create |
|
|
| `zone_not_in_account` no apply | `provision-zone` não foi corrido | Correr provision primeiro |
|
|
| Zona noutra conta CF | Domínio já em CF cliente | Opção «manter no provedor» ou transfer |
|
|
| Sandbox sem CF real | Token `sandbox_` ou `onboard_sandbox` | Esperado em dev |
|
|
|
|
---
|
|
|
|
## Manutenção deste anexo
|
|
|
|
Actualizar quando:
|
|
|
|
- Alterar `cloudflare.py` ou rotas em `onboarding.py`
|
|
- Novos eventos funil ligados a CF
|
|
- Mudança de conta Cloudflare ou scopes token
|
|
- Refactor frontend passo DNS
|
|
|
|
Após editar: `refresh-spec-driver.sh` no CT130 (regra `spec-driver-sync`).
|