""" Spec 037 — Resolve qual conta Cloudflare Ligbox gere um domínio. Deploy: /opt/ligbox-wizard/backend/app/services/dns_account_registry.py Config: /opt/ligbox-wizard/dns-accounts.yaml """ from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Any import yaml from app.config import _PROJECT_ROOT, _parse_token_line from app.services.cloudflare import CloudflareDNS, CloudflareError _DEFAULT_CONFIG = _PROJECT_ROOT / "dns-accounts.yaml" @dataclass(frozen=True) class LigboxCfAccount: id: str label: str admin_email: str cloudflare_account_id: str token_file: str def load_token(self) -> str: path = _PROJECT_ROOT / self.token_file if not path.is_file(): return "" for line in path.read_text(encoding="utf-8").splitlines(): token = _parse_token_line(line) if token: return token return "" def client(self) -> CloudflareDNS: return CloudflareDNS(token=self.load_token()) @dataclass class ZoneMatch: domain: str account: LigboxCfAccount zone: dict[str, Any] zone_id: str @property def dns_mode(self) -> str: return f"ligbox_cf_{self.account.id}" def load_accounts(config_path: Path | None = None) -> tuple[list[LigboxCfAccount], list[str]]: path = config_path or _DEFAULT_CONFIG if not path.is_file(): return [], [] data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} order = list(data.get("probe_order") or []) accounts: list[LigboxCfAccount] = [] for raw in data.get("accounts") or []: accounts.append( LigboxCfAccount( id=str(raw["id"]), label=str(raw.get("label") or raw["id"]), admin_email=str(raw.get("admin_email") or ""), cloudflare_account_id=str(raw.get("cloudflare_account_id") or ""), token_file=str(raw.get("token_file") or ""), ) ) by_id = {a.id: a for a in accounts} ordered = [by_id[i] for i in order if i in by_id] # append any not listed in probe_order seen = {a.id for a in ordered} for a in accounts: if a.id not in seen: ordered.append(a) return ordered, order def resolve_ligbox_zone( domain: str, config_path: Path | None = None, ) -> ZoneMatch | None: """Procura zona nas contas Ligbox por ordem do catálogo.""" domain = domain.lower().strip().rstrip(".") accounts, _ = load_accounts(config_path) errors: list[str] = [] for account in accounts: token = account.load_token() if not token: errors.append(f"{account.id}: token ausente") continue try: cf = account.client() zone = cf.get_zone_by_name(domain) if zone and zone.get("id"): return ZoneMatch( domain=domain, account=account, zone=zone, zone_id=str(zone["id"]), ) except CloudflareError as e: errors.append(f"{account.id}: {e}") return None def _load_config_raw(config_path: Path | None = None) -> dict[str, Any]: path = config_path or _DEFAULT_CONFIG if not path.is_file(): return {} return yaml.safe_load(path.read_text(encoding="utf-8")) or {} def pick_provision_account(config_path: Path | None = None) -> LigboxCfAccount | None: """Conta Ligbox onde o wizard cria zona nova do cliente.""" data = _load_config_raw(config_path) default_id = str(data.get("default_provision_account") or "ibytera") accounts, _ = load_accounts(config_path) by_id = {a.id: a for a in accounts} preferred = by_id.get(default_id) if preferred and preferred.load_token(): return preferred for account in accounts: if account.load_token(): return account return None def provision_ligbox_zone( domain: str, account: LigboxCfAccount | None = None, config_path: Path | None = None, ) -> dict[str, Any]: """Garante zona do cliente na conta CF Ligbox indicada (cria se necessário).""" domain = domain.lower().strip().rstrip(".") account = account or pick_provision_account(config_path) if not account: raise CloudflareError("Nenhuma conta Ligbox com token configurado para provisionar zona.") cf = account.client() existing = cf.get_zone_by_name(domain) if existing: return {"zone": existing, "created": False, "account": account} acc_id = account.cloudflare_account_id or None if acc_id and acc_id.startswith("PREENCHER"): acc_id = None zone = cf.create_zone(domain, account_id=acc_id) return {"zone": zone, "created": True, "account": account} def resolve_payload(domain: str, config_path: Path | None = None) -> dict[str, Any]: match = resolve_ligbox_zone(domain, config_path) provision_acct = pick_provision_account(config_path) base = { "domain": domain.lower().strip().rstrip("."), "paths_available": ["provision_ligbox", "byo", "external"], } if match: return { **base, "matched": True, "account_id": match.account.id, "account_label": f"{match.account.label} ({match.account.admin_email})", "zone_id": match.zone_id, "zone_status": match.zone.get("status"), "dns_mode": match.dns_mode, "can_provision_ligbox": True, "provision_account_id": match.account.id, "paths_available": ["apply_ligbox", "provision_ligbox", "byo", "external"], "message": ( f"Domínio encontrado na conta Cloudflare {match.account.label}. " "Pode aplicar apontamentos Ligbox automaticamente." ), } prov_id = provision_acct.id if provision_acct else None return { **base, "matched": False, "dns_mode": f"ligbox_cf_{prov_id}" if prov_id else None, "can_provision_ligbox": provision_acct is not None, "provision_account_id": prov_id, "message": ( "Domínio novo — o wizard pode criar a zona na Cloudflare gerenciada pela Ligbox " f"({provision_acct.label}) ou usar BYO Cloudflare / registrador." if provision_acct else "Configure tokens Ligbox para provisionar zona, ou use BYO/registrador." ), }