ligbox-ops-platform/specs/004-onboard-funnel-events/cloudflare-zone-provision.md
Ligbox Spec Hub c1881f58e6 chore: sync Console SSO, DNS viewer, specs e infra docs pendentes
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>
2026-06-25 20:10:17 +00:00

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`).