"""Conta Cloudflare dedicada por cliente — Spec 037 Fase D.""" from __future__ import annotations import json from datetime import datetime, timezone from pathlib import Path from typing import Any from app.services.cloudflare import CloudflareDNS, CloudflareError from app.services.dns_account_registry import pick_provision_account, provision_ligbox_zone _CF_CLIENT_DIR = Path("/var/lib/ligbox-wizard/cf_client_accounts") def _path(domain: str) -> Path: safe = domain.lower().strip().replace("/", "_") return _CF_CLIENT_DIR / f"{safe}.json" def save_record(domain: str, data: dict[str, Any]) -> dict[str, Any]: domain = domain.lower().strip().rstrip(".") _CF_CLIENT_DIR.mkdir(parents=True, exist_ok=True) existing = load_record(domain) or {} merged = {**existing, **data, "domain": domain} merged["updated_at"] = datetime.now(timezone.utc).isoformat() if not merged.get("created_at"): merged["created_at"] = merged["updated_at"] _path(domain).write_text(json.dumps(merged, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") return merged def load_record(domain: str) -> dict[str, Any] | None: p = _path(domain) if not p.is_file(): return None try: return json.loads(p.read_text(encoding="utf-8")) except json.JSONDecodeError: return None def provision_client_account( domain: str, *, ligbox_project_email: str | None = None, account_name: str | None = None, ) -> dict[str, Any]: """ Fase D: cria conta CF dedicada (Tenant) ou fallback zona na conta provision. Hoje: fallback = provision_ligbox_zone até Tenant activo. """ domain = domain.lower().strip().rstrip(".") acct = pick_provision_account() if not acct: raise CloudflareError("Nenhuma conta Ligbox configurada.") prov = provision_ligbox_zone(domain, account=acct) zone = prov["zone"] record = save_record( domain, { "ligbox_project_email": ligbox_project_email, "cloudflare_account_id": acct.cloudflare_account_id, "cloudflare_account_name": account_name or f"{domain}", "zone_id": zone.get("id"), "provision_mode": "shared_account_fallback", "handoff_status": "pending", "tenant_dedicated": False, }, ) return { "domain": domain, "zone": zone, "created": prov["created"], "account": acct.id, "record": record, "message": ( "Zona provisionada (modo fallback conta partilhada). " "Tenant dedicado pendente activação Cloudflare." ), } def handoff_manager(domain: str, manager_email: str) -> dict[str, Any]: """Fase D: convite gestor CF — stub até Tenant + members API.""" domain = domain.lower().strip().rstrip(".") manager_email = manager_email.lower().strip() rec = load_record(domain) or {} rec = save_record( domain, { **rec, "client_manager_email": manager_email, "handoff_status": "invited_stub", "handoff_note": "Implementar POST /accounts/{id}/members quando Tenant activo.", }, ) return { "domain": domain, "manager_email": manager_email, "handoff_status": rec["handoff_status"], "message": "Handoff registado (stub). Convite CF manual até Tenant API.", }