From e5564c15b96953bfa32c7b3471682a325b4fdec0 Mon Sep 17 00:00:00 2001 From: Ligbox Obsidian Vault Date: Wed, 1 Jul 2026 16:54:41 +0000 Subject: [PATCH] Obsidian: Spec 043 mail-bundle + FOSS ligbox-mail-business MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync contratos, runbooks VM112/VM123, código wizard e VM112.md com deploy validado. Co-authored-by: Cursor --- ligbox-ops-platform/_sidebar.md | 1 + .../deploy/vm112-wizard/MAIL-BUNDLE-DEPLOY.md | 56 +++++++ .../vm112-wizard/deploy-mail-bundle-vm112.py | 62 ++++++++ .../create-foss-mail-business.sh | 94 +++++++++++ ligbox-ops-platform/docs/vms/VM112.md | 78 ++++++++++ .../backend/app/routers/internal_provision.py | 75 +++++++++ .../backend/app/services/mail_bundle.py | 146 +++++++++++++++++ .../foss-products.md | 147 ++++++++++++++++++ .../tasks.md | 95 +++++++++++ .../contracts/activation-field-mapping.md | 3 +- .../contracts/mail-bundle-api.md | 101 ++++++++++++ .../043-desk-client-activation-sync/spec.md | 3 +- .../043-desk-client-activation-sync/tasks.md | 8 + 13 files changed, 867 insertions(+), 2 deletions(-) create mode 100644 ligbox-ops-platform/deploy/vm112-wizard/MAIL-BUNDLE-DEPLOY.md create mode 100644 ligbox-ops-platform/deploy/vm112-wizard/deploy-mail-bundle-vm112.py create mode 100755 ligbox-ops-platform/deploy/vm123-finance-stack/create-foss-mail-business.sh create mode 100644 ligbox-ops-platform/docs/vms/VM112.md create mode 100644 ligbox-ops-platform/projects/wizard/backend/app/routers/internal_provision.py create mode 100644 ligbox-ops-platform/projects/wizard/backend/app/services/mail_bundle.py create mode 100644 ligbox-ops-platform/specs/035-ligbox-mail-bundles-foss-openpanel/foss-products.md create mode 100644 ligbox-ops-platform/specs/035-ligbox-mail-bundles-foss-openpanel/tasks.md create mode 100644 ligbox-ops-platform/specs/043-desk-client-activation-sync/contracts/mail-bundle-api.md diff --git a/ligbox-ops-platform/_sidebar.md b/ligbox-ops-platform/_sidebar.md index 52e9473..ef5ab38 100644 --- a/ligbox-ops-platform/_sidebar.md +++ b/ligbox-ops-platform/_sidebar.md @@ -211,3 +211,4 @@ - **contracts/** - [activation-field-mapping.md](specs/043-desk-client-activation-sync/contracts/activation-field-mapping.md) — **fonte única** - [desk-activate-account-api.md](specs/043-desk-client-activation-sync/contracts/desk-activate-account-api.md) + - [mail-bundle-api.md](specs/043-desk-client-activation-sync/contracts/mail-bundle-api.md) diff --git a/ligbox-ops-platform/deploy/vm112-wizard/MAIL-BUNDLE-DEPLOY.md b/ligbox-ops-platform/deploy/vm112-wizard/MAIL-BUNDLE-DEPLOY.md new file mode 100644 index 0000000..12eca35 --- /dev/null +++ b/ligbox-ops-platform/deploy/vm112-wizard/MAIL-BUNDLE-DEPLOY.md @@ -0,0 +1,56 @@ +# Deploy — mail-bundle endpoint VM112 (Spec 035/043) + +**Roger · 2026-07-01** + +## Pré-requisitos + +- `OPS_INTERNAL_TOKEN` no `/opt/ligbox-deploy/env/.env` ou `.env` wizard (igual VM122 Desk) +- Clone `ligbox-ops-platform` na VM112 ou copiar ficheiros via `scp` + +## Passos (executar NA VM112) + +```bash +cd /opt/ligbox-spec-hub/repos/ligbox-ops-platform +git pull origin main + +python3 deploy/vm112-wizard/deploy-mail-bundle-vm112.py +systemctl restart ligbox-wizard +systemctl status ligbox-wizard --no-pager +``` + +## Teste local + +```bash +source /opt/ligbox-deploy/env/.env 2>/dev/null || true +curl -s -X POST http://127.0.0.1:8090/api/internal/provision/mail-bundle \ + -H "X-Ops-Internal-Token: ${OPS_INTERNAL_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "domain": "empresa-teste.ligbox.com.br", + "admin_email": "admin@empresa-teste.ligbox.com.br", + "admin_name": "Teste Bundle", + "seats": 25, + "mail_gb_per_seat": 30, + "files_gb_per_seat": 200 + }' | python3 -m json.tool +``` + +## Teste via Desk (após deploy VM122) + +Activar conta no Desk → passo `wizard` deve retornar `provisioned: true` (não `skipped`). + +## Rollback + +```bash +git checkout HEAD~1 -- projects/wizard/backend/app/services/mail_bundle.py +# ou remover router de main.py manualmente +systemctl restart ligbox-wizard +``` + +## Ficheiros instalados + +| Destino VM112 | Origem repo | +|---------------|-------------| +| `/opt/ligbox-wizard/backend/app/services/mail_bundle.py` | `projects/wizard/.../mail_bundle.py` | +| `/opt/ligbox-wizard/backend/app/routers/internal_provision.py` | `projects/wizard/.../internal_provision.py` | +| `main.py` (patch router) | `deploy-mail-bundle-vm112.py` | diff --git a/ligbox-ops-platform/deploy/vm112-wizard/deploy-mail-bundle-vm112.py b/ligbox-ops-platform/deploy/vm112-wizard/deploy-mail-bundle-vm112.py new file mode 100644 index 0000000..5eb112e --- /dev/null +++ b/ligbox-ops-platform/deploy/vm112-wizard/deploy-mail-bundle-vm112.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Deploy mail-bundle endpoint na VM112 — executar NO host (Spec 035/043). + +Uso (a partir do clone ligbox-ops-platform na VM112 ou CT130 com scp): + REPO=/opt/ligbox-spec-hub/repos/ligbox-ops-platform + python3 $REPO/deploy/vm112-wizard/deploy-mail-bundle-vm112.py + systemctl restart ligbox-wizard +""" +from __future__ import annotations + +import ast +import shutil +import sys +from pathlib import Path + +REPO = Path(sys.argv[1] if len(sys.argv) > 1 else "/opt/ligbox-spec-hub/repos/ligbox-ops-platform") +WIZARD = Path("/opt/ligbox-wizard") +SRC_SVC = REPO / "projects/wizard/backend/app/services/mail_bundle.py" +SRC_ROUTER = REPO / "projects/wizard/backend/app/routers/internal_provision.py" +DST_SVC = WIZARD / "backend/app/services/mail_bundle.py" +DST_ROUTER = WIZARD / "backend/app/routers/internal_provision.py" +MAIN = WIZARD / "backend/app/main.py" + +if not WIZARD.is_dir(): + print("ERRO: /opt/ligbox-wizard não encontrado — executar na VM112") + sys.exit(1) +if not SRC_SVC.is_file(): + print(f"ERRO: fonte não encontrada: {SRC_SVC}") + sys.exit(1) + +shutil.copy2(SRC_SVC, DST_SVC) +shutil.copy2(SRC_ROUTER, DST_ROUTER) +print("copied mail_bundle.py + internal_provision.py") + +text = MAIN.read_text(encoding="utf-8") + +if "internal_provision" not in text: + old = " telemetry,\n)" + new = " telemetry,\n internal_provision,\n)" + if old in text: + text = text.replace(old, new) + else: + text = text.replace( + "from app.routers import", + "from app.routers import internal_provision,", + 1, + ) + +if "internal_provision.router" not in text: + anchor = 'app.include_router(onboarding.router, prefix="/api")' + if anchor in text: + text = text.replace( + anchor, + anchor + '\napp.include_router(internal_provision.router, prefix="/api")', + ) + else: + text = text.rstrip() + '\napp.include_router(internal_provision.router, prefix="/api")\n' + +MAIN.write_text(text, encoding="utf-8") +ast.parse(text) +print("main.py patched") +print("OK — reiniciar: systemctl restart ligbox-wizard") diff --git a/ligbox-ops-platform/deploy/vm123-finance-stack/create-foss-mail-business.sh b/ligbox-ops-platform/deploy/vm123-finance-stack/create-foss-mail-business.sh new file mode 100755 index 0000000..b996e08 --- /dev/null +++ b/ligbox-ops-platform/deploy/vm123-finance-stack/create-foss-mail-business.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Spec 035/043 — Cria plano + produto FOSS ligbox-mail-business (VM123) +set -euo pipefail + +FOSS_URL="${FOSS_URL:-https://financeiro.ligbox.com.br}" +ADMIN_EMAIL="${FOSS_ADMIN_EMAIL:-admin@ligbox.com.br}" +ADMIN_PASS="${FOSS_ADMIN_PASS:-LbFossAdmin805353}" +PLAN_NAME="${PLAN_NAME:-ligbox-mail-business}" +PRODUCT_SLUG="${PRODUCT_SLUG:-ligbox-mail-business}" +PRODUCT_TITLE="${PRODUCT_TITLE:-Ligbox Mail Business}" +PRICE="${PRICE:-549.00}" +COOKIE_JAR="$(mktemp)" +trap 'rm -f "$COOKIE_JAR"' EXIT + +echo "=== FOSS: criar ${PRODUCT_SLUG} ===" + +curl -sk -c "$COOKIE_JAR" -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/guest/staff/login" \ + -d "email=${ADMIN_EMAIL}&password=${ADMIN_PASS}" | grep -q '"role":"admin"' || { echo "Login falhou"; exit 1; } +curl -sk -c "$COOKIE_JAR" -b "$COOKIE_JAR" "${FOSS_URL}/admin" >/dev/null +CSRF=$(awk '$6=="csrf_token" {print $7}' "$COOKIE_JAR" | tail -1) + +# Hosting plan OpenPanel +PLANS=$(curl -sk -b "$COOKIE_JAR" "${FOSS_URL}/api/admin/servicehosting/hp_get_list?CSRFToken=${CSRF}") +if ! echo "$PLANS" | grep -q "${PLAN_NAME}"; then + curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/servicehosting/hp_create" \ + -d "CSRFToken=${CSRF}&name=${PLAN_NAME}" >/dev/null + echo "Plano hosting ${PLAN_NAME} criado" +else + echo "Plano hosting ${PLAN_NAME} já existe" +fi + +HP_ID=$(echo "$PLANS" | python3 -c " +import sys,json,re +d=json.load(sys.stdin) +items=d.get('result',{}).get('list',[]) if isinstance(d.get('result'),dict) else d.get('list',[]) +name='${PLAN_NAME}' +for i in items: + if i.get('name')==name: + print(i.get('id')); break +" 2>/dev/null || true) +if [[ -z "$HP_ID" ]]; then + PLANS2=$(curl -sk -b "$COOKIE_JAR" "${FOSS_URL}/api/admin/servicehosting/hp_get_list?CSRFToken=${CSRF}") + HP_ID=$(echo "$PLANS2" | python3 -c " +import sys,json +d=json.load(sys.stdin) +items=d.get('result',{}).get('list',[]) if isinstance(d.get('result'),dict) else d.get('list',[]) +for i in items: + if i.get('name')=='${PLAN_NAME}': + print(i.get('id')); break +") +fi +echo "hp_id=${HP_ID}" + +SERVER_ID=$(curl -sk -b "$COOKIE_JAR" "${FOSS_URL}/api/admin/servicehosting/server_get_list?CSRFToken=${CSRF}" | python3 -c " +import sys,json +d=json.load(sys.stdin) +items=d.get('result',{}).get('list',[]) if isinstance(d.get('result'),dict) else d.get('list',[]) +print(items[0]['id'] if items else '1') +") +echo "server_id=${SERVER_ID}" + +LIST=$(curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/product/get_list" \ + -d "CSRFToken=${CSRF}&per_page=100") +if echo "$LIST" | grep -q "${PRODUCT_SLUG}"; then + PID=$(echo "$LIST" | python3 -c " +import sys,json +d=json.load(sys.stdin) +items=d.get('result',{}).get('list',[]) if isinstance(d.get('result'),dict) else d.get('list',[]) +for i in items: + if i.get('slug')=='${PRODUCT_SLUG}': + print(i.get('id')); break +") + echo "Produto ${PRODUCT_SLUG} já existe id=${PID}" + exit 0 +fi + +PID=$(curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/product/prepare" \ + -d "CSRFToken=${CSRF}&title=${PRODUCT_TITLE// /+}&type=hosting" | python3 -c "import sys,json; print(json.load(sys.stdin)['result'])") + +curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/product/update_config" \ + --data-urlencode "CSRFToken=${CSRF}" --data-urlencode "id=${PID}" \ + --data-urlencode "config[server_id]=${SERVER_ID}" \ + --data-urlencode "config[hosting_plan_id]=${HP_ID}" \ + --data-urlencode "config[reseller]=0" \ + --data-urlencode "config[allow_domain_own]=1" \ + --data-urlencode "config[allow_domain_register]=0" \ + --data-urlencode "config[allow_domain_transfer]=0" \ + --data-urlencode "config[allow_subdomain]=0" >/dev/null + +curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/product/update" \ + -d "CSRFToken=${CSRF}&id=${PID}&status=enabled&slug=${PRODUCT_SLUG}" >/dev/null + +echo "Produto criado: id=${PID} slug=${PRODUCT_SLUG} plan=${PLAN_NAME} price=R\$${PRICE}" +echo "Actualizar foss-products.md com product_id=${PID}" diff --git a/ligbox-ops-platform/docs/vms/VM112.md b/ligbox-ops-platform/docs/vms/VM112.md new file mode 100644 index 0000000..7e3fdb7 --- /dev/null +++ b/ligbox-ops-platform/docs/vms/VM112.md @@ -0,0 +1,78 @@ +# VM112 — Wizard Onboard + Carbonio Mail + +| Item | Valor | +|------|-------| +| **IP LAN** | `10.10.10.112` | +| **SSH WAN** | `95.216.14.146:2512` | +| **Hostname** | vm112-mail-ibytera | +| **URLs** | `onboard.ligbox.com.br` · API `:8090` | + +## Papel + +- Wizard onboarding clientes +- Carbonio mail tenants +- Webhooks → VM122 Desk +- Purge domínio / orquestração DNS (Spec 017, 026) + +## No repo Git (CT130) + +``` +deploy/vm112-spec022/ # Carbonio account scripts +deploy/vm112-wizard/ # Patches wizard (Spec 037 V4 DNS Viewer) · **mail-bundle Spec 043** +deploy/vm112-wizard-security/ # CSP, webhooks, audit +docs/EMAIL_LIGBOX_VM112.md +specs/001-webhook-vm112-integration/ +specs/017-vm112-domain-orchestration/ +specs/022-carbonio-account-exists-release/ +specs/025-wizard-onboarding-continuity/ +specs/026-purge-traefik-validation/ +specs/034-nextcloud-carbonio-vm112-integration/ +specs/037-dns-multi-cloudflare-orchestration/ # DNS Viewer V4 wizard +``` + +## Deploy na VM112 + +```bash +git clone https://git.spec.ligbox.com.br/ligbox/ligbox-ops-platform.git +# Copiar deploy/vm112-* para paths locais — ver README em cada pasta +``` + +### DNS Viewer V4 (Spec 037 — pendente) + +Painel read-only no passo DNS — **sem página nova**. Runbook: + +``` +deploy/vm112-wizard/DNS-VIEWER-V4.md +deploy/vm112-wizard/onboarding-dns-viewer-v4.patch.py +deploy/vm112-wizard/frontend-dns-viewer-v4.patch.py +``` + +Env obrigatório: `DESK_API_URL`, `OPS_INTERNAL_TOKEN` (igual Desk VM122). + +### Mail-bundle Spec 043 ✅ (deploy 2026-07-01) + +Endpoint interno para Desk activar domínio Carbonio + conta admin após FOSS order: + +``` +POST /api/internal/provision/mail-bundle +GET /api/internal/provision/mail-bundle/{domain} +``` + +Runbook: + +``` +deploy/vm112-wizard/MAIL-BUNDLE-DEPLOY.md +deploy/vm112-wizard/deploy-mail-bundle-vm112.py +specs/043-desk-client-activation-sync/contracts/mail-bundle-api.md +``` + +Auth: header `X-Ops-Internal-Token` (= `OPS_INTERNAL_TOKEN` Desk VM122, em `/opt/ligbox-deploy/env/.env`). + +**Teste validado:** domínio `bundle-novo-spec043.ligbox.com.br` → HTTP 200 `provisioned: true`. + +## Integração + +- **→ VM122:** webhooks `onboarding.*` · Assist/takeover API +- **→ CT114:** Traefik routers mail/onboard · `files.{dom}` (Spec 034) +- **→ VM124:** IMAP/SMTP (Nextcloud Mail app) · OCS provisioning (Spec 034) +- **← Desk:** purge, DNS revalidate, assist actions diff --git a/ligbox-ops-platform/projects/wizard/backend/app/routers/internal_provision.py b/ligbox-ops-platform/projects/wizard/backend/app/routers/internal_provision.py new file mode 100644 index 0000000..97918da --- /dev/null +++ b/ligbox-ops-platform/projects/wizard/backend/app/routers/internal_provision.py @@ -0,0 +1,75 @@ +"""Rotas internas VM112 — Desk/FOSS (Spec 035/043). Auth: X-Ops-Internal-Token.""" + +from __future__ import annotations + +import os + +from fastapi import APIRouter, Depends, Header, HTTPException +from pydantic import BaseModel, Field + +from app.services import activity_log, mail_bundle +from app.services.carbonio import CarbonioError +from app.services.domain_format import invalid_domain_http_detail, normalize_domain, validate_primary_domain + +router = APIRouter(prefix="/internal/provision", tags=["internal-provision"]) + +OPS_INTERNAL_TOKEN = os.getenv("OPS_INTERNAL_TOKEN", "") + + +def _require_internal(x_ops_internal_token: str | None = Header(default=None, alias="X-Ops-Internal-Token")): + if not OPS_INTERNAL_TOKEN: + raise HTTPException(503, "OPS_INTERNAL_TOKEN não configurado no wizard") + if x_ops_internal_token != OPS_INTERNAL_TOKEN: + raise HTTPException(401, "token interno inválido") + return True + + +class MailBundleRequest(BaseModel): + domain: str = Field(..., min_length=3, max_length=253) + admin_email: str = Field(..., min_length=5) + admin_name: str | None = Field(None, max_length=120) + seats: int = Field(25, ge=1, le=500) + mail_gb_per_seat: int = Field(30, ge=1, le=500) + files_gb_per_seat: int = Field(200, ge=1, le=2000) + foss_order_id: str | int | None = None + + +@router.post("/mail-bundle", dependencies=[Depends(_require_internal)]) +def post_mail_bundle(body: MailBundleRequest): + """ + Provisiona domínio + conta gerente Carbonio com quotas do bundle mail. + Chamado pelo Desk (Spec 043) após FOSS/OpenPanel/Odoo. + """ + domain = normalize_domain(body.domain) + try: + validate_primary_domain(domain) + except ValueError as e: + raise HTTPException(400, detail=invalid_domain_http_detail()) from e + + try: + result = mail_bundle.provision_mail_bundle( + domain=domain, + admin_email=body.admin_email.strip().lower(), + admin_name=body.admin_name, + seats=body.seats, + mail_gb_per_seat=body.mail_gb_per_seat, + files_gb_per_seat=body.files_gb_per_seat, + foss_order_id=body.foss_order_id, + ) + safe = {k: v for k, v in result.items() if k not in ("password",)} + return {"ok": True, **safe} + except ValueError as e: + raise HTTPException(400, str(e)) from e + except CarbonioError as e: + activity_log.error(f"mail-bundle falhou: {e}", source="internal") + raise HTTPException(502, str(e)) from e + + +@router.get("/mail-bundle/{domain}", dependencies=[Depends(_require_internal)]) +def get_mail_bundle(domain: str): + """Estado do bundle (sem senhas).""" + domain = normalize_domain(domain) + data = mail_bundle.load_bundle(domain) + if not data: + raise HTTPException(404, "bundle não provisionado") + return {"ok": True, **{k: v for k, v in data.items() if k != "password"}} diff --git a/ligbox-ops-platform/projects/wizard/backend/app/services/mail_bundle.py b/ligbox-ops-platform/projects/wizard/backend/app/services/mail_bundle.py new file mode 100644 index 0000000..f64100e --- /dev/null +++ b/ligbox-ops-platform/projects/wizard/backend/app/services/mail_bundle.py @@ -0,0 +1,146 @@ +""" +Spec 035/043 — Provisionamento mail bundle pós-activação Desk/FOSS. + +Garante domínio Carbonio, conta gerente (admin@domínio), quotas e metadata no registry. +Deploy: /opt/ligbox-wizard/backend/app/services/mail_bundle.py +""" + +from __future__ import annotations + +import json +import secrets +import string +from datetime import datetime, timezone +from typing import Any + +from app.services import activity_log, carbonio, domain_registry +from app.services.carbonio import CarbonioError + +_BUNDLE_VAULT = __import__("pathlib").Path("/var/lib/ligbox-wizard/mail_bundles") + + +def _now() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _generate_password(length: int = 16) -> str: + alphabet = string.ascii_letters + string.digits + "!@#$%&*" + while True: + pwd = "".join(secrets.choice(alphabet) for _ in range(length)) + if any(c.islower() for c in pwd) and any(c.isupper() for c in pwd) and any(c.isdigit() for c in pwd): + return pwd + + +def _bundle_path(domain: str) -> __import__("pathlib").Path: + safe = domain.lower().strip().replace("/", "_") + return _BUNDLE_VAULT / f"{safe}.json" + + +def load_bundle(domain: str) -> dict[str, Any] | None: + p = _bundle_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 save_bundle(domain: str, data: dict[str, Any]) -> None: + _BUNDLE_VAULT.mkdir(parents=True, exist_ok=True) + _bundle_path(domain).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + + +def provision_mail_bundle( + *, + domain: str, + admin_email: str, + admin_name: str | None = None, + seats: int = 25, + mail_gb_per_seat: int = 30, + files_gb_per_seat: int = 200, + foss_order_id: str | int | None = None, + admin_password: str | None = None, +) -> dict[str, Any]: + """ + Idempotente — reutiliza domínio/conta existentes; actualiza quotas e metadata. + """ + domain = domain.lower().strip().rstrip(".") + email = admin_email.lower().strip() + if not email.endswith(f"@{domain}"): + raise ValueError(f"admin_email deve ser @{domain}") + + existing = load_bundle(domain) + if existing and existing.get("provisioned"): + return { + **existing, + "already_provisioned": True, + "mail_host": existing.get("mail_host") or f"mail.{domain}", + } + + quota_mb = max(1, int(mail_gb_per_seat)) * 1024 + password = admin_password or _generate_password() + display = admin_name or email.split("@")[0] + + activity_log.info(f"mail-bundle: domínio {domain} seats={seats}", source="internal") + try: + if not carbonio.domain_exists(domain): + try: + carbonio.create_domain(domain) + except CarbonioError as exc: + if "DOMAIN_EXISTS" not in str(exc): + raise + carbonio.set_domain_public_hostname(domain) + else: + carbonio.set_domain_public_hostname(domain) + + _msg, reused_account = carbonio.ensure_onboarding_account( + email, password, display_name=display + ) + if hasattr(carbonio, "set_mail_quota"): + carbonio.set_mail_quota(email, quota_mb) + except CarbonioError as exc: + activity_log.error(f"mail-bundle Carbonio: {exc}", source="internal") + raise + + mail_host = f"mail.{domain}" + bundle = { + "domain": domain, + "admin_email": email, + "admin_name": display, + "seats": int(seats), + "mail_gb_per_seat": int(mail_gb_per_seat), + "files_gb_per_seat": int(files_gb_per_seat), + "foss_order_id": foss_order_id, + "mail_host": mail_host, + "webmail_url": f"https://{mail_host}/", + "files_url": f"https://files.{domain}/", + "provisioned": True, + "provisioned_at": _now(), + "account_reused": reused_account, + } + save_bundle(domain, bundle) + + rec = domain_registry.get_domain_record(domain) or {} + rec.update( + { + "domain": domain, + "mail_bundle": { + "seats": bundle["seats"], + "mail_gb_per_seat": bundle["mail_gb_per_seat"], + "files_gb_per_seat": bundle["files_gb_per_seat"], + "foss_order_id": foss_order_id, + "provisioned_at": bundle["provisioned_at"], + }, + "manager_email": email, + "manager_name": display, + } + ) + domain_registry.save_domain_record(domain, rec) + + activity_log.ok(f"mail-bundle OK {domain} → {mail_host}", source="internal") + return { + **bundle, + "already_provisioned": False, + "password_generated": not bool(admin_password), + } diff --git a/ligbox-ops-platform/specs/035-ligbox-mail-bundles-foss-openpanel/foss-products.md b/ligbox-ops-platform/specs/035-ligbox-mail-bundles-foss-openpanel/foss-products.md new file mode 100644 index 0000000..30084a0 --- /dev/null +++ b/ligbox-ops-platform/specs/035-ligbox-mail-bundles-foss-openpanel/foss-products.md @@ -0,0 +1,147 @@ +# Spec 035 — FOSSBilling Products (template) + +**Actualizar após criação manual no Admin FOSS** com IDs reais. + +Base URL Admin: `https://financeiro.ligbox.com.br/admin` + +--- + +## Hosting server + +| Campo | Valor | +|-------|-------| +| Nome | VM123 OpenPanel | +| Manager | OpenPanel | +| Hostname | `10.10.10.123:18087` | +| ID | 1 (existente) | + +--- + +## Produtos a criar + +### ligbox-mail-starter + +```yaml +title: Ligbox Mail Starter +slug: ligbox-mail-starter +type: hosting +category: Ligbox Mail +pricing: + type: recurrent + recurrent: monthly + price: 249.00 + setup: 0 +plugin: OpenPanel +plugin_config: + plan: ligbox-mail-starter +description: | + Até 10 contas email · 20 GB/caixa · 100 GB Nextcloud · EasyDMARC · subdomínio +custom_fields: + - name: domain + required: true + - name: manager_email + required: true +``` + +### ligbox-mail-business + +```yaml +title: Ligbox Mail Business +slug: ligbox-mail-business +pricing: + price: 549.00 +plugin_config: + plan: ligbox-mail-business +description: | + Até 25 contas · 30 GB/caixa · 200 GB Nextcloud · EasyDMARC · subdomínio +``` + +### ligbox-mail-enterprise + +```yaml +title: Ligbox Mail Enterprise +slug: ligbox-mail-enterprise +pricing: + price: 999.00 +plugin_config: + plan: ligbox-mail-enterprise +description: | + Até 50 contas · 50 GB/caixa · 300 GB Nextcloud · EasyDMARC · subdomínio +``` + +### ligbox-mail-custom + +```yaml +title: Ligbox Mail Personalizado +slug: ligbox-mail-custom +pricing: + type: recurrent + price: 99.00 # base fee +config_options: + - id: seats + name: Número de contas email + type: select + options: + - value: 10 + price: 0 + - value: 25 + price: 50 + - value: 30 + price: 80 + - value: 40 + price: 120 + - value: 50 + price: 180 + - id: mail_gb + name: GB email por conta + type: select + options: + - { value: 20, price: 0 } + - { value: 30, price: 15 } + - { value: 40, price: 30 } + - { value: 50, price: 45 } + - id: files_gb + name: GB Nextcloud por conta + type: select + options: + - { value: 100, price: 0 } + - { value: 200, price: 25 } + - { value: 300, price: 50 } + - { value: 400, price: 75 } + - { value: 500, price: 100 } +plugin_config: + plan: ligbox-mail-custom +``` + +--- + +## OpenPanel plans (OpenAdmin) + +| plan_name | max_domains | notes | +|-----------|-------------|-------| +| ligbox-mail-starter | 1 | hub gerente | +| ligbox-mail-business | 1 | hub gerente | +| ligbox-mail-enterprise | 1 | hub gerente | +| ligbox-mail-custom | 1 | metadata JSON quotas | + +Script provision: + +```bash +ssh root@10.10.10.123 +# Após criar plans no OpenAdmin, testar: +bash /opt/ligbox-ops-platform/projects/finance/deploy/vm123-finance-stack/test-foss-openpanel-order.sh +``` + +--- + +## IDs reais (preencher pós-criação) + +| Produto | FOSS product_id | OP plan (hp) | Slug | +|---------|-----------------|--------------|------| +| Starter | _TBD_ | _TBD_ | ligbox-mail-starter | +| **Business** | **3** | **2** | **ligbox-mail-business** | +| Enterprise | _TBD_ | _TBD_ | ligbox-mail-enterprise | +| Custom | _TBD_ | _TBD_ | ligbox-mail-custom | +| Site CMS | 2 | 1 | ligbox-site-cms-hosting | + +**Criado:** 2026-07-01 · VM123 · `deploy/vm123-finance-stack/create-foss-mail-business.sh` diff --git a/ligbox-ops-platform/specs/035-ligbox-mail-bundles-foss-openpanel/tasks.md b/ligbox-ops-platform/specs/035-ligbox-mail-bundles-foss-openpanel/tasks.md new file mode 100644 index 0000000..b99d86f --- /dev/null +++ b/ligbox-ops-platform/specs/035-ligbox-mail-bundles-foss-openpanel/tasks.md @@ -0,0 +1,95 @@ +# Spec 035 — Tasks + +**Status:** Draft · **Início:** 2026-06-21 + +--- + +## Fase 0 — Catálogo comercial (FOSS + OpenPanel) + +- [ ] Roger aprovar preços §1.1 — **via Admin Ligbox** (035-C), não FOSS directo +- [ ] Criar produtos FOSS: starter, business, enterprise, custom +- [ ] Criar planos OpenPanel espelhados (ligbox-mail-*) +- [ ] Config options FOSS para bundle custom (seats, mail_gb, files_gb) +- [ ] Publicar página vendas /signup com comparativo concorrência +- [ ] Documentar em `foss-products.md` IDs reais pós-criação + +## Fase 1 — Provisionamento + +- [ ] Tabela `bundle_entitlements` no wizard VM112 +- [ ] Endpoint `POST /api/internal/provision/mail-bundle` — **código pronto** · [mail-bundle-api.md](../043-desk-client-activation-sync/contracts/mail-bundle-api.md) · deploy: [MAIL-BUNDLE-DEPLOY.md](../../deploy/vm112-wizard/MAIL-BUNDLE-DEPLOY.md) +- [x] Produto FOSS `ligbox-mail-business` (id=3) — ver [foss-products.md](./foss-products.md) +- [ ] Webhook Desk `foss/order-activated` → wizard +- [ ] Bridge: metadata JSON no user OpenPanel hub +- [ ] Email template FOSS boas-vindas (links OP + Domain Admin) +- [ ] Teste E2E: order Business → admin@ + 1 conta teste + +## Fase 2 — Ligbox Console `/admin` gerente (PRIORIDADE UX — Roger 2026-06-21) + +Shell: [ligbox-console-shell.md](./ligbox-console-shell.md) · Detalhe: [domain-manager-console-ui.md](./domain-manager-console-ui.md) + +- [ ] UX-0: design tokens + AppLayout + login role → `console.ligbox.com.br` +- [ ] Traefik: `console.ligbox.com.br` VM123 + 301 onboard/admin +- [ ] UX-A1: `/admin` Início — cards resumo (tom banco digital BR) +- [ ] UX-A2: `/admin/contas` CRUD inline +- [ ] UX-A3: `/admin/files` quotas + toggle Mail NC +- [ ] UX-A4: `/admin/certificacao` + `/admin/dominio` **DNS Viewer** (037 dns-viewer.md) + - [x] `/admin/dominio` V3 deploy VM123 2026-06-25 (`AdminDominio.jsx`, `DnsViewerPanel.jsx`) + - [ ] `/admin/certificacao` EasyDMARC card +- [ ] UX-A4b: `/admin/plano` boleto/PIX +- [ ] SSO FOSS → `/admin?sso=TOKEN` +- [ ] API Desk `/api/v1/domain-console/*` + - [x] `GET /api/v1/domain-console/dns/viewer/{domain}` (VM122 2026-06-25) + +## Fase 2b — OpenPanel (backend only) + +- [ ] Bridge metadata JSON no user hub (sem UI gerente) + +## Fase C — Admin Ligbox staff (`/comercial` + `/ops`) — depois UX-A + +Ver: [ligbox-system-admin-ui.md](./ligbox-system-admin-ui.md) · Shell: [ligbox-console-shell.md](./ligbox-console-shell.md) + +- [ ] UX-B: `/comercial` — fila preços, catálogo, clientes, gateway +- [ ] UX-C: `/ops` — Spec 019 chamados no mesmo shell +- [ ] RBAC roles comercial/ops (Spec 027) +- [ ] Impersonate gerente → `/admin` + +## Fase 3 — Self-service utilizador email (Fase B — depois) + +Ver placeholder: [user-self-service-ui.md](./user-self-service-ui.md) + +- [ ] Estudo redirects, OOO, assinaturas, calendário por user +- [ ] Wireframe `/me` ou `mail.{dom}/settings` +- [ ] Decidir Carbonio nativo vs SPA Ligbox + +## Fase 3 — Nextcloud + EasyDMARC + +- [ ] VM116 provisionada (Spec 034 Fase 0) +- [ ] Auto-provision NC Files em cada createAccount +- [ ] Toggle Mail Nextcloud por domínio no `/admin` (Spec 035 §2.2.1) +- [ ] EasyDMARC register domain automático no wizard +- [ ] Agent A3 Desk: alertas DMARC fail + +## Fase 4 — Gateway pagamento + comercial avançado + +- [ ] Escolher gateway: ASAAS vs Iugu (Roger) +- [ ] Módulo FOSS: boleto bancário (PDF + linha digitável) +- [ ] Módulo FOSS: PIX QR Code + copia-e-cola +- [ ] Webhook pagamento confirmado → Desk → wizard provision +- [ ] Card «Pagamento» no `/admin` — status + link boleto/PIX (embed FOSS API) +- [ ] Upgrade/downgrade plano FOSS → update entitlements +- [ ] Add-ons: seat extra, storage mail/files +- [ ] Odoo subscription mirror (Spec 023 phase 2) + +## Documentação + +- [x] Spec 035 `spec.md` +- [x] `tasks.md` +- [x] `foss-products.md` (template) +- [x] `mockups/domain-manager-sandbox.html` — UI sandbox (Mock + Live create-only) +- [x] API Desk `domain_console_sandbox*.py` — create-only produção +- [ ] Deploy API sandbox no VM122 + teste cenário real +- [ ] Actualizar Spec 034 § comercial → link 035 +- [ ] Actualizar `docs/vms/README.md` +- [ ] Publicar Spec Hub Portal +- [x] Spec 037-DNS-VIEWER `dns-viewer.md` + §5.5 domain-manager actualizado +- [x] Implementar `GET /api/v1/dns/viewer/{domain}` (Desk VM122 + proxy Console VM123 2026-06-25) diff --git a/ligbox-ops-platform/specs/043-desk-client-activation-sync/contracts/activation-field-mapping.md b/ligbox-ops-platform/specs/043-desk-client-activation-sync/contracts/activation-field-mapping.md index a711465..50513f3 100644 --- a/ligbox-ops-platform/specs/043-desk-client-activation-sync/contracts/activation-field-mapping.md +++ b/ligbox-ops-platform/specs/043-desk-client-activation-sync/contracts/activation-field-mapping.md @@ -19,7 +19,7 @@ | G | Desk → Odoo | `res.partner` create/update | Odoo DB `ligbox` | | H | Desk → Wizard | `POST /api/internal/provision/mail-bundle` | Carbonio VM112 | -**Specs de origem:** [023](../../023-billing-recurrence-desk-visibility/spec.md) · [024 PROVISIONING_CLIENT_CARD](../../024-openpanel-fossbilling/PROVISIONING_CLIENT_CARD.md) · [035](../../035-ligbox-mail-bundles-foss-openpanel/spec.md) · [028 bridge](../../028-openpanel-ce-ligbox-reengineering/contracts/foss-bridge-api.md) +**Specs de origem:** [023](../../023-billing-recurrence-desk-visibility/spec.md) · [024 PROVISIONING_CLIENT_CARD](../../024-openpanel-fossbilling/PROVISIONING_CLIENT_CARD.md) · [035](../../035-ligbox-mail-bundles-foss-openpanel/spec.md) · [028 bridge](../../028-openpanel-ce-ligbox-reengineering/contracts/foss-bridge-api.md) · **[mail-bundle-api](./mail-bundle-api.md)** --- @@ -329,6 +329,7 @@ POST http://10.10.10.112/api/internal/provision/mail-bundle | Versão | Data | Autor | Alteração | |--------|------|-------|-----------| | 1.0 | 2026-07-01 | Roger / Cursor | Documento inicial Spec 043 | +| 1.1 | 2026-07-01 | Roger / Cursor | FOSS product_id=3 · mail-bundle-api | --- diff --git a/ligbox-ops-platform/specs/043-desk-client-activation-sync/contracts/mail-bundle-api.md b/ligbox-ops-platform/specs/043-desk-client-activation-sync/contracts/mail-bundle-api.md new file mode 100644 index 0000000..b6dcd2e --- /dev/null +++ b/ligbox-ops-platform/specs/043-desk-client-activation-sync/contracts/mail-bundle-api.md @@ -0,0 +1,101 @@ +# Contrato — API Wizard `POST /api/internal/provision/mail-bundle` + +**Spec:** [043](../spec.md) · [035](../../035-ligbox-mail-bundles-foss-openpanel/spec.md) +**VM:** 112 (`10.10.10.112:8090`) +**Status:** ✅ Implementado no repo · deploy VM112 pendente +**Auth:** `X-Ops-Internal-Token` (mesmo valor Desk `OPS_INTERNAL_TOKEN`) + +--- + +## POST `/api/internal/provision/mail-bundle` + +Provisiona domínio Carbonio + conta gerente com quotas do bundle mail. + +### Request + +```json +{ + "domain": "empresa.com.br", + "admin_email": "admin@empresa.com.br", + "admin_name": "João Silva", + "seats": 25, + "mail_gb_per_seat": 30, + "files_gb_per_seat": 200, + "foss_order_id": 55 +} +``` + +| Campo | Obrigatório | Descrição | +|-------|-------------|-----------| +| `domain` | sim | Domínio cliente | +| `admin_email` | sim | Conta gerente (`admin@{domain}`) | +| `admin_name` | não | Display name Carbonio | +| `seats` | não (default 25) | Licenças mail bundle | +| `mail_gb_per_seat` | não (default 30) | Quota GB/caixa gerente | +| `files_gb_per_seat` | não (default 200) | Nextcloud (fase 034) | +| `foss_order_id` | não | Correlação FOSS order | + +### Response 200 + +```json +{ + "ok": true, + "domain": "empresa.com.br", + "admin_email": "admin@empresa.com.br", + "mail_host": "mail.empresa.com.br", + "webmail_url": "https://mail.empresa.com.br/", + "files_url": "https://files.empresa.com.br/", + "seats": 25, + "provisioned": true, + "already_provisioned": false, + "account_reused": false +} +``` + +### Erros + +| HTTP | Condição | +|------|----------| +| 401 | Token interno inválido | +| 400 | Domínio/email inválido | +| 502 | Carbonio/zmprov falhou | +| 503 | `OPS_INTERNAL_TOKEN` ausente no wizard | + +--- + +## GET `/api/internal/provision/mail-bundle/{domain}` + +Estado do bundle (sem senhas). + +--- + +## Código + +| Artefacto | Path repo | +|-----------|-----------| +| Serviço | `projects/wizard/backend/app/services/mail_bundle.py` | +| Router | `projects/wizard/backend/app/routers/internal_provision.py` | +| Deploy VM112 | `deploy/vm112-wizard/deploy-mail-bundle-vm112.py` | +| Cliente Desk | `projects/ops-desk/api/app/vm123/wizard_client.py` | + +--- + +## Deploy VM112 + +```bash +# Na VM112 (SSH) +cd /opt/ligbox-spec-hub/repos/ligbox-ops-platform # ou clone Forgejo +git pull +python3 deploy/vm112-wizard/deploy-mail-bundle-vm112.py +systemctl restart ligbox-wizard + +# Teste +curl -s -X POST http://127.0.0.1:8090/api/internal/provision/mail-bundle \ + -H "X-Ops-Internal-Token: $OPS_INTERNAL_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"domain":"teste.ligbox.com.br","admin_email":"admin@teste.ligbox.com.br","seats":25}' +``` + +--- + +*Roger · 2026-07-01 · Spec 043 fecho ciclo mail-bundle* diff --git a/ligbox-ops-platform/specs/043-desk-client-activation-sync/spec.md b/ligbox-ops-platform/specs/043-desk-client-activation-sync/spec.md index 7e11acc..d891998 100644 --- a/ligbox-ops-platform/specs/043-desk-client-activation-sync/spec.md +++ b/ligbox-ops-platform/specs/043-desk-client-activation-sync/spec.md @@ -72,7 +72,8 @@ O Desk (VM122) é o **orquestrador**: recebe `company.validated`, persiste `bill | Artefacto | Path | |-----------|------| | **Mapa de campos** | [contracts/activation-field-mapping.md](./contracts/activation-field-mapping.md) | -| **API Desk (planeada)** | [contracts/desk-activate-account-api.md](./contracts/desk-activate-account-api.md) | +| **API Desk (activate)** | [contracts/desk-activate-account-api.md](./contracts/desk-activate-account-api.md) | +| **API Wizard mail-bundle** | [contracts/mail-bundle-api.md](./contracts/mail-bundle-api.md) | | **Tasks implementação** | [tasks.md](./tasks.md) | --- diff --git a/ligbox-ops-platform/specs/043-desk-client-activation-sync/tasks.md b/ligbox-ops-platform/specs/043-desk-client-activation-sync/tasks.md index b274b46..28ac07e 100644 --- a/ligbox-ops-platform/specs/043-desk-client-activation-sync/tasks.md +++ b/ligbox-ops-platform/specs/043-desk-client-activation-sync/tasks.md @@ -31,9 +31,17 @@ - [x] Deep-link Odoo na ficha cliente ## Fase 5 — Wizard mail-bundle +- [x] Contrato `contracts/mail-bundle-api.md` +- [x] Código `mail_bundle.py` + `internal_provision.py` (repo) - [x] `POST /billing/accounts/{id}/activate` com `provision_mail` - [x] Webhook `POST /billing/webhook/foss/order-activated` - [x] `wizard_client.provision_mail_bundle()` → VM112 +- [x] **Deploy VM112** — 2026-07-01 · `OPS_INTERNAL_TOKEN` + endpoint activo + +## FOSS — produto ligbox-mail-business (VM123) +- [x] Plano hosting hp_id=2 +- [x] Produto FOSS product_id=3 slug `ligbox-mail-business` +- [x] Script `deploy/vm123-finance-stack/create-foss-mail-business.sh` ## Fase 6 — Testes - [x] Unit: `test_activation_mapper_043.py` (3)