#!/usr/bin/env python3 """Deploy Spec 037 construção — Fase A + stubs D na VM112.""" from pathlib import Path BASE = Path("/opt/ligbox-spec-hub/repos/ligbox-ops-platform") WIZARD = Path("/opt/ligbox-wizard") REPO = BASE / "projects/wizard/backend/app" FILES = [ (REPO / "services/project_identity.py", WIZARD / "backend/app/services/project_identity.py"), (REPO / "services/cf_client_accounts.py", WIZARD / "backend/app/services/cf_client_accounts.py"), (REPO / "routers/project.py", WIZARD / "backend/app/routers/project.py"), ] for src, dst in FILES: dst.parent.mkdir(parents=True, exist_ok=True) dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8") print(f"copied {dst.name}") # main.py — register project router main = WIZARD / "backend/app/main.py" text = main.read_text(encoding="utf-8") if "project" not in text.split("from app.routers import")[1].split("\n")[0]: text = text.replace( "from app.routers import admin_accounts, admin_domains, assist, corporate, domain_admin, onboarding, portal_auth, telemetry", "from app.routers import admin_accounts, admin_domains, assist, corporate, domain_admin, onboarding, portal_auth, project, telemetry", ) if "project.router" not in text: text = text.replace( "app.include_router(onboarding.router, prefix=\"/api\")", "app.include_router(onboarding.router, prefix=\"/api\")\napp.include_router(project.router, prefix=\"/api\")", ) main.write_text(text, encoding="utf-8") print("main.py patched") # onboarding.py — phase D endpoints + webhook ligbox_project_email onb = WIZARD / "backend/app/routers/onboarding.py" ot = onb.read_text(encoding="utf-8") if "cf_client_accounts" not in ot: ot = ot.replace( "from app.services import dns_byo_vault", "from app.services import dns_byo_vault, cf_client_accounts, project_identity", ) PHASE_D = ''' class ProvisionClientAccountRequest(BaseModel): domain: str = Field(..., min_length=3, max_length=253) display_name: str | None = None class HandoffManagerRequest(BaseModel): domain: str manager_email: str = Field(..., min_length=5) def _project_email_for_domain(domain: str) -> str | None: ident = project_identity.get_project_identity(domain.lower().strip()) return ident.get("ligbox_project_email") if ident else None @router.post("/dns/cloudflare/provision-client-account") def provision_client_cf_account(body: ProvisionClientAccountRequest, request: Request): """Fase D — conta CF dedicada (fallback zona partilhada até Tenant).""" get_session_from_request(request) domain = normalize_domain(body.domain) try: validate_primary_domain(domain) except ValueError as e: raise HTTPException(status_code=400, detail=invalid_domain_http_detail()) from e try: ligbox_email = _project_email_for_domain(domain) result = cf_client_accounts.provision_client_account( domain, ligbox_project_email=ligbox_email, account_name=body.display_name or domain, ) ops_webhook.emit_event( "onboard.cf_account.provisioned", domain=domain, session_id=get_session_from_request(request), data={ "ligbox_project_email": ligbox_email, "zone_id": result.get("zone", {}).get("id"), "provision_mode": result.get("record", {}).get("provision_mode"), }, ) return result except CloudflareError as e: raise HTTPException(502, str(e)) from e @router.post("/dns/cloudflare/handoff-manager") def handoff_cf_manager(body: HandoffManagerRequest, request: Request): """Fase D — convite gestor domínio na conta CF.""" get_session_from_request(request) domain = normalize_domain(body.domain) result = cf_client_accounts.handoff_manager(domain, body.manager_email) ops_webhook.emit_event( "onboard.cf_handoff.requested", domain=domain, session_id=get_session_from_request(request), data=result, ) return result ''' if "/dns/cloudflare/provision-client-account" not in ot: anchor = "@router.get(\"/session/password-status\")" ot = ot.replace(anchor, PHASE_D + anchor) # apply webhook if "onboard.dns.applied" not in ot: ot = ot.replace( ' verification = dns_verify.verify_mail_dns(domain, settings.mail_public_ip)\n return {\n "domain": domain,', ' verification = dns_verify.verify_mail_dns(domain, settings.mail_public_ip)\n ops_webhook.emit_event(\n "onboard.dns.applied",\n domain=domain,\n session_id=sid,\n data={\n "dns_mode": dns_mode,\n "ligbox_project_email": _project_email_for_domain(domain),\n "ligbox_account_id": ligbox_acct,\n "zone_id": zone_id,\n "verification_ready": verification.get("ready"),\n },\n )\n return {\n "domain": domain,', ) # account create webhook extension if "onboard.account.created" not in ot: ot = ot.replace( ' activity_log.ok("Processo de onboarding finalizado", source="portal")', ' ops_webhook.emit_event(\n "onboard.account.created",\n domain=domain,\n session_id=sid,\n data={\n "email": email,\n "dns_mode": dns_mode,\n "ligbox_project_email": _project_email_for_domain(domain),\n "verified": verified,\n },\n )\n activity_log.ok("Processo de onboarding finalizado", source="portal")', ) onb.write_text(ot, encoding="utf-8") print("onboarding.py patched") import ast ast.parse(main.read_text()) ast.parse(onb.read_text()) print("syntax OK")