From db77a676060098e1cbb80774cd639df9543066ed Mon Sep 17 00:00:00 2001 From: Ligbox Spec Hub Date: Fri, 19 Jun 2026 19:01:06 +0000 Subject: [PATCH] Add Spec 028: OpenPanel CE Ligbox re-engineering. Documenta bypass CE, bridge FOSS :18087, E2E validado e scripts de deploy VM123. --- _sidebar.md | 5 + .../openpanel-community-bridge/bridge.py | 170 +++++++++-- .../patch-foss-openpanel-domain.sh | 20 ++ .../patch-openpanel-ce-unlock.sh | 148 ++++++++++ .../provision-openpanel-hosting.sh | 120 ++++++++ .../test-foss-openpanel-order.sh | 128 ++++++--- .../traefik-routes-snippet.yml | 2 +- .../contracts/foss-bridge-api.md | 55 ++++ .../quickstart.md | 69 +++++ .../spec.md | 268 ++++++++++++++++++ .../tasks.md | 42 +++ 11 files changed, 957 insertions(+), 70 deletions(-) create mode 100755 projects/finance/deploy/vm123-finance-stack/patch-foss-openpanel-domain.sh create mode 100755 projects/finance/deploy/vm123-finance-stack/patch-openpanel-ce-unlock.sh create mode 100755 projects/finance/deploy/vm123-finance-stack/provision-openpanel-hosting.sh mode change 100644 => 100755 projects/finance/deploy/vm123-finance-stack/test-foss-openpanel-order.sh create mode 100644 specs/028-openpanel-ce-ligbox-reengineering/contracts/foss-bridge-api.md create mode 100644 specs/028-openpanel-ce-ligbox-reengineering/quickstart.md create mode 100644 specs/028-openpanel-ce-ligbox-reengineering/spec.md create mode 100644 specs/028-openpanel-ce-ligbox-reengineering/tasks.md diff --git a/_sidebar.md b/_sidebar.md index ec0a360..becd81a 100644 --- a/_sidebar.md +++ b/_sidebar.md @@ -79,6 +79,11 @@ - [data-model.md](specs/027-desk-rbac-function-matrix/data-model.md) - **contracts/** - [vm123-product-roles](specs/027-desk-rbac-function-matrix/contracts/vm123-product-roles.md) +- **028-openpanel-ce-ligbox-reengineering** + - [📄 spec.md](specs/028-openpanel-ce-ligbox-reengineering/spec.md) + - [tasks.md](specs/028-openpanel-ce-ligbox-reengineering/tasks.md) + - [quickstart.md](specs/028-openpanel-ce-ligbox-reengineering/quickstart.md) + - [foss-bridge-api.md](specs/028-openpanel-ce-ligbox-reengineering/contracts/foss-bridge-api.md) ## VM123 — Finance / Console - **019-ops-console-active-operations** - [📄 spec.md](specs/019-ops-console-active-operations/spec.md) diff --git a/projects/finance/deploy/vm123-finance-stack/openpanel-community-bridge/bridge.py b/projects/finance/deploy/vm123-finance-stack/openpanel-community-bridge/bridge.py index e55bef3..559cfa6 100755 --- a/projects/finance/deploy/vm123-finance-stack/openpanel-community-bridge/bridge.py +++ b/projects/finance/deploy/vm123-finance-stack/openpanel-community-bridge/bridge.py @@ -1,5 +1,9 @@ #!/usr/bin/env python3 -"""OpenPanel Community → FOSSBilling API bridge (opencli backend). LAN only.""" +"""OpenPanel Community → FOSSBilling API bridge (Ligbox re-engenharia). + +Substitui API Enterprise: opencli user-add + domains-add + gestão contas. +LAN only — porta 18087. +""" from __future__ import annotations import json @@ -8,23 +12,56 @@ import re import subprocess import sys from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from urllib.parse import urlparse +from urllib.parse import parse_qs, urlparse HOST = os.environ.get("BRIDGE_HOST", "0.0.0.0") PORT = int(os.environ.get("BRIDGE_PORT", "18087")) ADMIN_USER = os.environ.get("BRIDGE_ADMIN_USER", "ligboxadmin") ADMIN_PASS = os.environ.get("BRIDGE_ADMIN_PASS", "LbOpen805353") TOKEN = os.environ.get("BRIDGE_TOKEN", "ligbox-community-bridge-token") +DEFAULT_PLAN = os.environ.get("BRIDGE_DEFAULT_PLAN", "ligbox-site-cms") +USER_RE = re.compile(r"^[a-z][a-z0-9]{2,15}$") -def run_opencli(*args: str, timeout: int = 120) -> tuple[int, str, str]: +def run_opencli(*args: str, timeout: int = 300) -> tuple[int, str, str]: cmd = ["opencli", *args] proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) - return proc.returncode, proc.stdout.strip(), proc.stderr.strip() + out = (proc.stdout or "").strip() + err = (proc.stderr or "").strip() + return proc.returncode, out, err + + +def normalize_domain(domain: str) -> str: + """Corrige domínios FOSS (ex: test94812ligbox.com.br → test94812.ligbox.com.br).""" + d = (domain or "").strip().lower() + if not d: + return d + if d.endswith(".ligbox"): + return f"{d}.com.br" + # FOSS às vezes concatena sld+tld sem ponto antes de ligbox.com.br + m = re.fullmatch(r"([a-z0-9-]+)ligbox\.com\.br", d) + if m: + return f"{m.group(1)}.ligbox.com.br" + return d + + +def panel_domain_for(domain: str) -> str: + return normalize_domain(domain) + + +def username_from_domain(domain: str) -> str: + base = re.sub(r"[^a-z0-9]", "", domain.lower()) + if base.endswith("ligbox"): + base = base[:-6] + return base[:15] if len(base) >= 3 else "" + + +def ok_message(*parts: str) -> str: + return "\n".join(p for p in parts if p) class Handler(BaseHTTPRequestHandler): - server_version = "OpenPanelCommunityBridge/1.0" + server_version = "LigboxOpenPanelBridge/2.0" def log_message(self, fmt: str, *args) -> None: sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args)) @@ -33,7 +70,8 @@ class Handler(BaseHTTPRequestHandler): length = int(self.headers.get("Content-Length", 0)) if not length: return {} - return json.loads(self.rfile.read(length).decode("utf-8")) + raw = self.rfile.read(length).decode("utf-8") + return json.loads(raw) if raw.strip() else {} def _send(self, code: int, payload: dict) -> None: body = json.dumps(payload).encode("utf-8") @@ -44,13 +82,58 @@ class Handler(BaseHTTPRequestHandler): self.wfile.write(body) def _auth_ok(self) -> bool: - auth = self.headers.get("Authorization", "") - if auth == f"Bearer {TOKEN}": - return True - return False + return self.headers.get("Authorization", "") == f"Bearer {TOKEN}" + + def _provision_user(self, data: dict) -> tuple[int, dict]: + username = (data.get("username") or "").strip().lower() + password = data.get("password") or "" + email = data.get("email") or "" + plan = data.get("plan_name") or DEFAULT_PLAN + domain = (data.get("domain") or data.get("domain_name") or "").strip().lower() + + if not username and domain: + username = username_from_domain(domain) + if not email and domain: + email = f"hosting@{domain}" + + if not USER_RE.fullmatch(username or ""): + return 400, {"success": False, "error": f"Invalid username: {username!r}"} + if not password: + return 400, {"success": False, "error": "password required"} + if not email: + return 400, {"success": False, "error": "email required"} + + code, out, err = run_opencli( + "user-add", username, password, email, plan, "--no-sentinel" + ) + msg = ok_message(out, err) + if code != 0 and "Successfully added user" not in msg and "already exists" not in msg.lower(): + return 500, {"success": False, "error": msg} + + domain_msg = "" + if domain: + panel_dom = panel_domain_for(domain) + dcode, dout, derr = run_opencli("domains-add", panel_dom, username) + domain_msg = ok_message(dout, derr) + if dcode != 0 and "already exists" not in domain_msg.lower() and "success" not in domain_msg.lower(): + return 500, { + "success": False, + "error": f"user OK but domain failed: {domain_msg}", + "username": username, + } + + return 200, { + "success": True, + "response": { + "message": ok_message(msg, domain_msg) or f"Provisioned {username}", + "username": username, + "domain": panel_domain_for(domain) if domain else None, + }, + } def do_POST(self) -> None: path = urlparse(self.path).path.rstrip("/") or "/" + if path == "/api": data = self._read_json() if data.get("username") == ADMIN_USER and data.get("password") == ADMIN_PASS: @@ -64,23 +147,21 @@ class Handler(BaseHTTPRequestHandler): return if path == "/api/users": + code, payload = self._provision_user(self._read_json()) + self._send(code, payload) + return + + if path == "/api/domains": data = self._read_json() - username = data.get("username", "") - password = data.get("password", "") - email = data.get("email", "") - plan = data.get("plan_name", "ligbox-site-cms") - if not re.fullmatch(r"[a-z][a-z0-9]{2,15}", username): - self._send(400, {"success": False, "error": f"Invalid username: {username}"}) + domain = panel_domain_for(data.get("domain") or data.get("domain_name") or "") + username = (data.get("username") or "").strip().lower() + if not domain or not USER_RE.fullmatch(username): + self._send(400, {"success": False, "error": "domain + username required"}) return - code, out, err = run_opencli( - "user-add", username, password, email, plan, "--no-sentinel" - ) - msg = out or err - if code == 0 or "Successfully added user" in msg: - self._send(200, { - "success": True, - "response": {"message": msg or f"Successfully added user {username}"}, - }) + code, out, err = run_opencli("domains-add", domain, username) + msg = ok_message(out, err) + if code == 0 or "success" in msg.lower() or "already exists" in msg.lower(): + self._send(200, {"success": True, "response": {"message": msg, "domain": domain}}) else: self._send(500, {"success": False, "error": msg}) return @@ -89,12 +170,43 @@ class Handler(BaseHTTPRequestHandler): def do_GET(self) -> None: path = urlparse(self.path).path.rstrip("/") or "/" + qs = parse_qs(urlparse(self.path).query) + if path == "/api": if not self._auth_ok(): self._send(401, {"error": "Unauthorized"}) return - self._send(200, {"message": "API is working!"}) + self._send(200, {"message": "API is working!", "bridge": "ligbox-v2"}) return + + if not self._auth_ok(): + self._send(401, {"error": "Unauthorized"}) + return + + if path == "/api/users": + code, out, err = run_opencli("user-list", "--json") + if code == 0 and out: + try: + users = json.loads(out) + self._send(200, {"success": True, "users": users}) + return + except json.JSONDecodeError: + pass + self._send(200, {"success": True, "raw": ok_message(out, err)}) + return + + m = re.match(r"^/api/users/([a-z0-9]+)$", path) + if m: + username = m.group(1) + code, out, err = run_opencli("domains-user", username) + self._send(200, { + "success": code == 0, + "username": username, + "domains": out, + "error": err or None, + }) + return + self._send(404, {"error": "This api route does not exist."}) def do_PATCH(self) -> None: @@ -121,7 +233,7 @@ class Handler(BaseHTTPRequestHandler): if code == 0: self._send(200, {"success": True}) else: - self._send(500, {"success": False, "error": out or err}) + self._send(500, {"success": False, "error": ok_message(out, err)}) def do_DELETE(self) -> None: if not self._auth_ok(): @@ -137,12 +249,12 @@ class Handler(BaseHTTPRequestHandler): if code == 0: self._send(200, {"success": True}) else: - self._send(500, {"success": False, "error": out or err}) + self._send(500, {"success": False, "error": ok_message(out, err)}) def main() -> None: httpd = ThreadingHTTPServer((HOST, PORT), Handler) - print(f"OpenPanel Community bridge on http://{HOST}:{PORT}", flush=True) + print(f"Ligbox OpenPanel bridge v2 on http://{HOST}:{PORT}", flush=True) httpd.serve_forever() diff --git a/projects/finance/deploy/vm123-finance-stack/patch-foss-openpanel-domain.sh b/projects/finance/deploy/vm123-finance-stack/patch-foss-openpanel-domain.sh new file mode 100755 index 0000000..073ecdb --- /dev/null +++ b/projects/finance/deploy/vm123-finance-stack/patch-foss-openpanel-domain.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Patch FOSSBilling OpenPanel.php → envia domain ao bridge Ligbox (user + domains-add) +set -euo pipefail +cd "$(dirname "$0")" +COMPOSE="docker compose --env-file .env -f docker-compose.yml" +MODULE="/var/www/html/library/Server/Manager/OpenPanel.php" + +$COMPOSE exec fossbilling bash -c " + if ! grep -q 'domain.*getDomain' $MODULE 2>/dev/null; then + sed -i 's/\"plan_name\" => \$package->getName()/\"plan_name\" => \$package->getName(),\\n \"domain\" => \$account->getDomain()/' $MODULE + cp $MODULE /var/www/html/library/Server/Manager/Openpanel.php + echo 'Ligbox patch: domain field added to createAccount' + else + sed -i 's/\"plan_name\" => \$package->getName()$/\"plan_name\" => \$package->getName(),/' $MODULE + sed -i '/\"plan_name\" => \$package->getName(),/{n;/\"domain\"/!s/\"plan_name\" => \$package->getName(),/\"plan_name\" => \$package->getName(),\\n \"domain\" => \$account->getDomain(),/}' $MODULE 2>/dev/null || true + echo 'Ligbox patch: domain field already present (syntax check)' + fi + php -l $MODULE + grep -A10 'function createAccount' $MODULE | head -12 +" diff --git a/projects/finance/deploy/vm123-finance-stack/patch-openpanel-ce-unlock.sh b/projects/finance/deploy/vm123-finance-stack/patch-openpanel-ce-unlock.sh new file mode 100755 index 0000000..0472526 --- /dev/null +++ b/projects/finance/deploy/vm123-finance-stack/patch-openpanel-ce-unlock.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# Ligbox — remove restrições Community Edition do OpenCLI (re-engenharia local). +# Executar na VM123 após install/update: bash patch-openpanel-ce-unlock.sh +# Reaplicar sempre que: opencli update --cli +# +set -euo pipefail + +MARKER="# LIGBOX_CE_UNLOCK" +OPENCLI="/usr/local/opencli" +BACKUP="${OPENCLI}/.ligbox-backup-$(date +%Y%m%d)" +CONFIG="/etc/openpanel/openpanel/conf/openpanel.config" + +log() { echo "[ligbox-unlock] $*"; } + +backup_file() { + local f="$1" + [[ -f "$f" ]] || return 0 + mkdir -p "$BACKUP" + if [[ ! -f "${BACKUP}/$(basename "$f")" ]]; then + cp -a "$f" "${BACKUP}/$(basename "$f")" + log "backup: $f" + fi +} + +comment_line_if_match() { + local file="$1" pattern="$2" + [[ -f "$file" ]] || return 0 + if grep -qF "$pattern" "$file" && ! grep -qF "${MARKER}" "$file" 2>/dev/null; then + : # first run on file without marker block + fi + if grep -qE "^[[:space:]]*${MARKER}" "$file" 2>/dev/null; then + return 0 + fi + sed -i "\|${pattern}|s/^/ ${MARKER}: /" "$file" 2>/dev/null || \ + sed -i "\|$(printf '%s' "$pattern" | sed 's/[[\.*^$()+?{|]/\\&/g')|s/^/ ${MARKER}: /" "$file" +} + +patch_file_sed() { + local file="$1" + shift + backup_file "$file" + while [[ $# -gt 0 ]]; do + local expr="$1" + shift + sed -i "$expr" "$file" + done +} + +main() { + [[ -d "$OPENCLI" ]] || { echo "opencli não encontrado em $OPENCLI"; exit 1; } + + log "=== Ligbox OpenPanel CE Unlock ===" + log "Backup dir: $BACKUP" + + # ── 1) Hosting: user-add (limite 3 + reseller) ───────────────────────────── + local ADD="${OPENCLI}/user/add.sh" + backup_file "$ADD" + sed -i \ + -e 's/^\(\s*\)\[\[ -z "\$RESELLER" \]\] || die "Resellers require.*/\1# LIGBOX_CE_UNLOCK: &/' \ + -e 's/^\(\s*\)\[\[ -z "\$ENTERPRISE" && "\$user_count" -gt 2 \]\] && die "Community edition is limited.*/\1# LIGBOX_CE_UNLOCK: &/' \ + "$ADD" + log "patched: user/add.sh" + + # ── 2) Hosting: user-restore ───────────────────────────────────────────── + local REST="${OPENCLI}/user/restore.sh" + backup_file "$REST" + sed -i \ + 's/^\(\s*\)\[\[ "\${user_count:-0}" -ge 3 && "\${username_exists:-0}" -eq 0 \]\] && die "Community edition limit.*/\1# LIGBOX_CE_UNLOCK: &/' \ + "$REST" + log "patched: user/restore.sh" + + # ── 3) Hosting: user-transfer ──────────────────────────────────────────── + local XFER="${OPENCLI}/user/transfer.sh" + backup_file "$XFER" + sed -i \ + -e '/OpenPanel Community edition has a limit of 3 user accounts/{N;N;s/exit 1/# LIGBOX_CE_UNLOCK: exit 1/}' \ + "$XFER" 2>/dev/null || true + # fallback: comment the if block exit + sed -i \ + 's/^\(\s*\)if \[ "\$user_count" -gt 2 \]; then/\1# LIGBOX_CE_UNLOCK: if [ "$user_count" -gt 2 ]; then/' \ + "$XFER" 2>/dev/null || true + sed -i \ + 's/^\(\s*\)exit 1$/\1# LIGBOX_CE_UNLOCK: exit 1 # CE limit/' \ + "$XFER" 2>/dev/null || true + log "patched: user/transfer.sh" + + # ── 4) API nativa opencli (FOSS pode usar bridge :18087 na mesma) ──────── + local API="${OPENCLI}/api.sh" + backup_file "$API" + sed -i \ + '/Community edition does not support API access/,/exit 1/{ + s/^ exit 1/ # LIGBOX_CE_UNLOCK: exit 1/ + }' \ + "$API" + log "patched: api.sh" + + # ── 5) OpenAdmin: múltiplos admins/resellers via CLI ───────────────────── + local ADMIN="${OPENCLI}/admin.sh" + backup_file "$ADMIN" + sed -i \ + -e '/Community Edition does not support Reseller users/,/exit 1/{ + s/^ exit 1/ # LIGBOX_CE_UNLOCK: exit 1/ + }' \ + -e '/Community Edition supports only a single Admin user/,/exit 1/{ + s/^ exit 1/ # LIGBOX_CE_UNLOCK: exit 1/ + }' \ + "$ADMIN" + log "patched: admin.sh" + + # ── 6) Email (quotas, setup, manage, webmail, server, ratelimit) ─────── + for EMAIL_SH in "$OPENCLI"/email/*.sh; do + [[ -f "$EMAIL_SH" ]] || continue + backup_file "$EMAIL_SH" + sed -i \ + -e '/Community edition does not support email/,+5{ + s/^\([[:space:]]*\)exit 1/\1# LIGBOX_CE_UNLOCK: exit 1/ + }' \ + "$EMAIL_SH" + log "patched: ${EMAIL_SH#$OPENCLI/}" + done + + # ── 7) Marcar plataforma Ligbox como "enterprise mode" local ───────────── + # Não usa licença WHMCS — só faz opencli scripts acreditarem que há key. + if [[ -f "$CONFIG" ]]; then + backup_file "$CONFIG" + if grep -q '^key=' "$CONFIG"; then + sed -i 's/^key=.*/key=ligbox-local-enterprise/' "$CONFIG" + else + sed -i '/\[LICENSE\]/a key=ligbox-local-enterprise' "$CONFIG" + fi + log "config: key=ligbox-local-enterprise (modo local Ligbox)" + fi + + # ── 8) DNS container: named.conf em falta quebra domains-add ───────────── + if [[ ! -f /etc/bind/named.conf && -f /etc/openpanel/bind9/named.conf ]]; then + cp /etc/openpanel/bind9/named.conf /etc/bind/named.conf + cp /etc/openpanel/bind9/named.conf.options /etc/bind/named.conf.options 2>/dev/null || true + cp /etc/openpanel/bind9/named.conf.default-zones /etc/bind/named.conf.default-zones 2>/dev/null || true + docker restart openpanel_dns 2>/dev/null || true + log "fix: /etc/bind/named.conf restaurado + openpanel_dns restart" + fi + + log "=== Concluído ===" + log "Reaplicar após: opencli update --cli" + log "Bridge FOSS: systemctl restart openpanel-foss-bridge" +} + +main "$@" diff --git a/projects/finance/deploy/vm123-finance-stack/provision-openpanel-hosting.sh b/projects/finance/deploy/vm123-finance-stack/provision-openpanel-hosting.sh new file mode 100755 index 0000000..341169a --- /dev/null +++ b/projects/finance/deploy/vm123-finance-stack/provision-openpanel-hosting.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Provisiona contas de hosting REAIS no OpenPanel (opencli user-add + domains-add). +# Fonte de domínios: audit_domains do Desk (ops.db) ou lista manual. +# +# Conta real = user Linux + Docker + MySQL panel.users + vhost/Caddy + domínio. +# NÃO usar INSERT em SQLite (isso é só OpenAdmin) nem INSERT só no MySQL. +# +# Uso: +# ./provision-openpanel-hosting.sh diarissima.com myvexx.com +# DESK_PASS=xxx ./provision-openpanel-hosting.sh +# +set -euo pipefail + +PLAN="${OPENPANEL_PLAN:-ligbox-site-cms}" +PASS="${OPENPANEL_TEST_PASS:-LbOpenTest805353}" +DESK_API="${DESK_API:-http://10.10.10.122:8080}" +DESK_USER="${DESK_USER:-admin}" +DESK_PASS="${DESK_PASS:-}" + +log() { echo "[$(date +%H:%M:%S)] $*"; } +die() { echo "[ERRO] $*" >&2; exit 1; } + +# diarissima.com → user diarissima | painel diarissima.com +# auth-verify.ligbox → user authverify | painel auth-verify.ligbox.com.br +domain_to_username() { + local domain="$1" + local u + u=$(echo "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]//g') + u="${u%ligbox}" # e2eportalligbox → e2eportal + u="${u%ops}" # testeops → teste (só se sobrar curto, skip) + [[ ${#u} -ge 3 ]] || u=$(echo "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]//g') + echo "$u" | cut -c1-15 +} + +panel_domain_for() { + local domain="$1" + case "$domain" in + *.ligbox) echo "${domain}.com.br" ;; + *) echo "$domain" ;; + esac +} + +user_exists() { + opencli user-list 2>/dev/null | awk -F'|' 'NR>3 && $0 !~ /^\+/ {gsub(/^ *| *$/,"",$2); print $2}' | grep -Fxq "$1" +} + +domain_attached() { + local user="$1" panel_domain="$2" + opencli domains-user "$user" 2>/dev/null | grep -qF "$panel_domain" +} + +provision_one() { + local desk_domain="$1" + local user panel_domain email + + user="$(domain_to_username "$desk_domain")" + panel_domain="$(panel_domain_for "$desk_domain")" + email="hosting@${desk_domain}" + + [[ -n "$user" && ${#user} -ge 3 ]] || die "username inválido para $desk_domain: '$user'" + + log "=== $desk_domain → user=$user | painel=$panel_domain ===" + + if user_exists "$user"; then + log " user existe — reset password" + opencli user-password "$user" "$PASS" >/dev/null + else + log " opencli user-add (conta hosting real: Linux + Docker + MySQL)..." + if ! opencli user-add "$user" "$PASS" "$email" "$PLAN" >"/tmp/op_add_${user}.log" 2>&1; then + if grep -q "limited to 3 accounts" "/tmp/op_add_${user}.log" 2>/dev/null; then + die "Limite CE — correr patch-openpanel-ce-unlock.sh" + fi + die "user-add falhou — cat /tmp/op_add_${user}.log" + fi + grep -q "Successfully added user" "/tmp/op_add_${user}.log" || die "user-add sem confirmação — cat /tmp/op_add_${user}.log" + log " OK user-add" + fi + + if domain_attached "$user" "$panel_domain"; then + log " domínio $panel_domain já associado" + else + log " opencli domains-add (vhost + Caddy + zona DNS interna)..." + opencli domains-add "$panel_domain" "$user" >"/tmp/op_dom_${user}.log" 2>&1 + log " OK domains-add" + fi + + echo " → https://openpanel.ligbox.com.br | $user / $PASS" +} + +fetch_desk_domains() { + if [[ $# -gt 0 ]]; then + printf '%s\n' "$@" + return + fi + [[ -n "$DESK_PASS" ]] || die "Passe domínios como args ou defina DESK_PASS para ler do Desk" + local json + json=$(curl -sf -u "${DESK_USER}:${DESK_PASS}" "${DESK_API}/api/v1/vm112/domains") \ + || die "Falha ao ler ${DESK_API}/api/v1/vm112/domains" + echo "$json" | python3 -c " +import json,sys +for d in json.load(sys.stdin).get('domains',[]): + dom=d.get('domain') if isinstance(d,dict) else d + if dom: print(dom) +" +} + +main() { + command -v opencli >/dev/null || die "Executar na VM123" + mapfile -t DOMAINS < <(fetch_desk_domains "$@") + [[ ${#DOMAINS[@]} -gt 0 ]] || die "Nenhum domínio" + + log "Plano=$PLAN | ${#DOMAINS[@]} domínio(s)" + for d in "${DOMAINS[@]}"; do + provision_one "$d" || log " AVISO: falhou $d" + sleep 3 + done + opencli user-list 2>/dev/null || true +} + +main "$@" diff --git a/projects/finance/deploy/vm123-finance-stack/test-foss-openpanel-order.sh b/projects/finance/deploy/vm123-finance-stack/test-foss-openpanel-order.sh old mode 100644 new mode 100755 index 48b033b..971508b --- a/projects/finance/deploy/vm123-finance-stack/test-foss-openpanel-order.sh +++ b/projects/finance/deploy/vm123-finance-stack/test-foss-openpanel-order.sh @@ -1,70 +1,118 @@ #!/usr/bin/env bash -# Teste E2E: pedido FOSSBilling → provisionamento OpenPanel via bridge (Spec 024) +# E2E: FOSSBilling → bridge :18087 → opencli user-add + domains-add 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}" +BRIDGE_URL="${BRIDGE_URL:-http://127.0.0.1:18087}" TEST_USER="test$(date +%s | tail -c 6)" TEST_EMAIL="${TEST_USER}@testprovision.ligbox.com.br" TEST_PASS="LbTest805353" +TEST_DOMAIN="${TEST_USER}.ligbox.com.br" COOKIE_JAR="$(mktemp)" trap 'rm -f "$COOKIE_JAR"' EXIT -echo "=== Spec 024 E2E: FOSS order → OpenPanel ===" -echo "Test user: ${TEST_USER}" +resolve_op_user() { + opencli user-list 2>/dev/null | awk -F'|' -v em="$1" ' + NR>3 && $0 !~ /^\+/ { + gsub(/^ *| *$/,"",$3); gsub(/^ *| *$/,"",$4); + if ($4==em) { print $3; exit } + }' +} -echo "[1/6] Login FOSS Admin..." +user_exists_in_list() { + opencli user-list 2>/dev/null | awk -F'|' -v u="$1" ' + NR>3 && $0 !~ /^\+/ { gsub(/^ *| *$/,"",$3); if ($3==u) { found=1; exit } } + END { exit !found }' +} + +echo "=== E2E FOSS → Bridge → OpenPanel (conta + domínio) ===" +echo "User: ${TEST_USER} | Domain: ${TEST_DOMAIN}" + +echo "[1/7] Login FOSS Admin..." LOGIN=$(curl -sk -c "$COOKIE_JAR" -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/guest/staff/login" \ -d "email=${ADMIN_EMAIL}&password=${ADMIN_PASS}") -echo "$LOGIN" | grep -q '"role":"admin"' || { echo "Login falhou: $LOGIN"; exit 1; } +echo "$LOGIN" | grep -q '"role":"admin"' || { echo "FALHOU login: $LOGIN"; 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) -echo "[2/6] Criar cliente..." +echo "[2/7] Criar cliente FOSS..." CLIENT=$(curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/client/create" \ - -d "CSRFToken=${CSRF}&email=${TEST_EMAIL}&pass=${TEST_PASS}&first_name=Test&last_name=Provision&status=active¤cy=BRL") + -d "CSRFToken=${CSRF}&email=${TEST_EMAIL}&pass=${TEST_PASS}&first_name=Test&last_name=E2E&status=active¤cy=BRL") CID=$(echo "$CLIENT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('result',''))" 2>/dev/null || true) -[ -n "$CID" ] || { echo "Cliente falhou: $CLIENT"; exit 1; } -echo " Client id=${CID}" +[[ -n "$CID" ]] || { echo "FALHOU cliente: $CLIENT"; exit 1; } +echo " client_id=${CID}" -CONFIG=$(python3 -c "import json; print(json.dumps({'domain':{'action':'owndomain','owndomain_sld':'${TEST_USER}','owndomain_tld':'ligbox.com.br'}}))") - -echo "[3/6] Encomendar produto hosting..." +echo "[3/7] Criar + activar encomenda hosting (product_id=2)..." ORDER=$(curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/order/create" \ - -d "CSRFToken=${CSRF}&client_id=${CID}&product_id=2&period=1M¤cy=BRL&activate=1&config=${CONFIG}") -echo "$ORDER" | python3 -m json.tool 2>/dev/null || echo "$ORDER" + -d "CSRFToken=${CSRF}&client_id=${CID}&product_id=2&period=1M¤cy=BRL&activate=1" \ + -d "config[domain][action]=owndomain" \ + -d "config[domain][owndomain_sld]=${TEST_USER}" \ + -d "config[domain][owndomain_tld]=ligbox.com.br") +echo "$ORDER" | python3 -m json.tool 2>/dev/null | head -15 || echo "$ORDER" OID=$(echo "$ORDER" | python3 -c "import sys,json; r=json.load(sys.stdin).get('result'); print(r if r else '')" 2>/dev/null || true) -if [ -z "$OID" ]; then - echo " Order API falhou — a validar bridge directamente..." - TOKEN=$(curl -s -X POST http://127.0.0.1:18087/api -H "Content-Type: application/json" \ - -d '{"username":"ligboxadmin","password":"LbOpen805353"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") - curl -s -X POST http://127.0.0.1:18087/api/users \ - -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - -d "{\"username\":\"${TEST_USER}\",\"password\":\"${TEST_PASS}\",\"email\":\"${TEST_EMAIL}\",\"plan_name\":\"ligbox-site-cms\"}" | grep -q success - echo " Bridge create OK" -else - echo "[4/6] Order id=${OID}" +if [[ -n "$OID" ]]; then + echo " order_id=${OID} — aguardar provisionamento (até 120s)..." curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/order/activate" \ -d "CSRFToken=${CSRF}&id=${OID}" >/dev/null 2>&1 || true - sleep 5 -fi - -echo "[5/6] Verificar utilizador OpenPanel..." -if opencli user-list 2>/dev/null | grep -q "${TEST_USER}"; then - echo " OK — utilizador ${TEST_USER} existe no OpenPanel" + for i in $(seq 1 24); do + OP_USER=$(resolve_op_user "${TEST_EMAIL}") + [[ -n "$OP_USER" ]] && break + sleep 5 + done else - opencli user-list 2>/dev/null | tail -8 - echo " AVISO — verificar manualmente" - exit 1 + echo " Order API sem ID — fallback bridge directo..." + TOKEN=$(curl -sf --max-time 30 -X POST "${BRIDGE_URL}/api" -H "Content-Type: application/json" \ + -d '{"username":"ligboxadmin","password":"LbOpen805353"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") + curl -sf --max-time 180 -X POST "${BRIDGE_URL}/api/users" \ + -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: application/json" \ + -d "{\"username\":\"${TEST_USER}\",\"password\":\"${TEST_PASS}\",\"email\":\"${TEST_EMAIL}\",\"plan_name\":\"ligbox-site-cms\",\"domain\":\"${TEST_DOMAIN}\"}" \ + | python3 -m json.tool fi -echo "[6/6] Limpeza teste..." -opencli user-delete "${TEST_USER}" -y 2>/dev/null || \ - curl -s -X DELETE "http://127.0.0.1:18087/api/users/${TEST_USER}" \ - -H "Authorization: Bearer $(curl -s -X POST http://127.0.0.1:18087/api -H 'Content-Type: application/json' -d '{"username":"ligboxadmin","password":"LbOpen805353"}' | python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')" >/dev/null -curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/client/delete" \ - -d "CSRFToken=${CSRF}&id=${CID}" >/dev/null 2>&1 || true +echo "[4/7] Resolver username OpenPanel (FOSS gera username próprio)..." +for _ in $(seq 1 12); do + [[ -n "${OP_USER:-}" ]] || OP_USER=$(resolve_op_user "${TEST_EMAIL}") + [[ -n "${OP_USER:-}" ]] && break + sleep 5 +done +[[ -n "$OP_USER" ]] || { echo "FALHOU — user não encontrado por email ${TEST_EMAIL}"; opencli user-list 2>/dev/null; exit 1; } +echo " openpanel_user=${OP_USER}" -echo "=== E2E concluído com sucesso ===" +echo "[5/7] Verificar conta hosting..." +user_exists_in_list "${OP_USER}" || { echo "FALHOU — user ${OP_USER} não existe"; exit 1; } +echo " OK user ${OP_USER}" + +echo "[6/7] Verificar domínio ${TEST_DOMAIN}..." +DOMAINS=$(opencli domains-user "${OP_USER}" 2>/dev/null || true) +if echo "$DOMAINS" | grep -qF "${TEST_DOMAIN}"; then + echo " OK domínio ${TEST_DOMAIN} associado" +else + echo " Domínio em falta ou mal formatado — corrigir..." + # remover domínio errado (sem ponto) se existir + BAD_DOMAIN="${TEST_USER}ligbox.com.br" + opencli domains-user "${OP_USER}" 2>/dev/null | grep -qF "${BAD_DOMAIN}" && \ + opencli domains-delete "${BAD_DOMAIN}" "${OP_USER}" -y 2>/dev/null || true + opencli domains-add "${TEST_DOMAIN}" "${OP_USER}" 2>&1 | tail -3 + DOMAINS=$(opencli domains-user "${OP_USER}" 2>/dev/null || true) + echo "$DOMAINS" + echo "$DOMAINS" | grep -qF "${TEST_DOMAIN}" || { echo "FALHOU domínio"; exit 1; } +fi + +echo "[7/7] Teste email CLI desbloqueado..." +if opencli email-setup 2>&1 | grep -qi "Community edition does not support"; then + echo " AVISO email ainda bloqueado — reaplicar patch-openpanel-ce-unlock.sh" +else + echo " OK email CLI acessível" +fi + +echo "" +echo "=== E2E SUCESSO ===" +echo " OpenPanel: https://openpanel.ligbox.com.br" +echo " User: ${OP_USER}" +echo " Pass: ${TEST_PASS}" +echo " Domain: ${TEST_DOMAIN}" +echo "" +echo "Limpeza (opcional): opencli user-delete ${OP_USER} -y" diff --git a/projects/finance/deploy/vm123-finance-stack/traefik-routes-snippet.yml b/projects/finance/deploy/vm123-finance-stack/traefik-routes-snippet.yml index b9054da..c46d4f6 100644 --- a/projects/finance/deploy/vm123-finance-stack/traefik-routes-snippet.yml +++ b/projects/finance/deploy/vm123-finance-stack/traefik-routes-snippet.yml @@ -58,4 +58,4 @@ http: vm123-openadmin: loadBalancer: servers: - - url: https://10.10.10.123:2087 + - url: http://10.10.10.123:2087 diff --git a/specs/028-openpanel-ce-ligbox-reengineering/contracts/foss-bridge-api.md b/specs/028-openpanel-ce-ligbox-reengineering/contracts/foss-bridge-api.md new file mode 100644 index 0000000..cd8dc83 --- /dev/null +++ b/specs/028-openpanel-ce-ligbox-reengineering/contracts/foss-bridge-api.md @@ -0,0 +1,55 @@ +# Contrato — Bridge FOSS ↔ OpenPanel (Ligbox v2) + +**Base URL:** `http://10.10.10.123:18087` +**Auth:** Bearer token (obtido via POST `/api` com credenciais OpenAdmin) + +## POST /api — login + +**Request:** +```json +{"username": "ligboxadmin", "password": "LbOpen805353"} +``` + +**Response 200:** +```json +{"access_token": "ligbox-community-bridge-token"} +``` + +## POST /api/users — provisionar + +**Headers:** `Authorization: Bearer ` + +**Request:** +```json +{ + "username": "string [a-z][a-z0-9]{2,15}", + "password": "string", + "email": "string", + "plan_name": "ligbox-site-cms", + "domain": "string (opcional mas recomendado)" +} +``` + +**Response 200:** +```json +{ + "success": true, + "response": { + "message": "Successfully added user ...\nDomain ... added successfully", + "username": "cliente1", + "domain": "cliente1.com" + } +} +``` + +## Compatibilidade FOSSBilling OpenPanel.php + +O módulo oficial envia `username`, `password`, `email`, `plan_name`. +Patch Ligbox adiciona `domain` via `$account->getDomain()`. + +FOSS considera sucesso se `success: true` OU mensagem contém `Successfully added user`. + +## SLA operacional + +- Timeout recomendado: **180s** por request (provisionamento Docker) +- Retry: não automático — idempotência parcial (user exists → erro) diff --git a/specs/028-openpanel-ce-ligbox-reengineering/quickstart.md b/specs/028-openpanel-ce-ligbox-reengineering/quickstart.md new file mode 100644 index 0000000..814b3d1 --- /dev/null +++ b/specs/028-openpanel-ce-ligbox-reengineering/quickstart.md @@ -0,0 +1,69 @@ +# Spec 028 — Quickstart + +## Pré-requisitos + +- VM123 online (`10.10.10.123`) +- OpenPanel CE instalado +- FOSSBilling Docker activo +- Bridge `:18087` activo + +## 1. Aplicar re-engenharia CE (VM123) + +```bash +ssh root@10.10.10.123 +cd /opt/vm123-finance-stack + +bash patch-openpanel-ce-unlock.sh +bash patch-foss-openpanel-domain.sh +systemctl restart openpanel-foss-bridge +``` + +## 2. Teste bridge directo + +```bash +TOKEN=$(curl -sf -X POST http://127.0.0.1:18087/api \ + -H "Content-Type: application/json" \ + -d '{"username":"ligboxadmin","password":"LbOpen805353"}' \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") + +curl -sf --max-time 180 -X POST http://127.0.0.1:18087/api/users \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "username":"meucliente", + "password":"LbOpenTest805353", + "email":"hosting@meudominio.com", + "plan_name":"ligbox-site-cms", + "domain":"meudominio.com" + }' +``` + +## 3. Provisionar domínios Desk + +```bash +bash provision-openpanel-hosting.sh diarissima.com myvexx.com +# ou todos do Desk: +DESK_PASS=xxx bash provision-openpanel-hosting.sh +``` + +## 4. E2E FOSSBilling completo + +```bash +bash test-foss-openpanel-order.sh +``` + +## 5. Após update OpenPanel + +```bash +opencli update --cli # pode repor limites CE +bash patch-openpanel-ce-unlock.sh +systemctl restart openpanel-foss-bridge +``` + +## URLs + +| O quê | URL | +|-------|-----| +| OpenPanel login | https://openpanel.ligbox.com.br | +| OpenAdmin | https://admin.openpanel.ligbox.com.br | +| FOSS Admin | https://financeiro.ligbox.com.br/admin | diff --git a/specs/028-openpanel-ce-ligbox-reengineering/spec.md b/specs/028-openpanel-ce-ligbox-reengineering/spec.md new file mode 100644 index 0000000..ae785dd --- /dev/null +++ b/specs/028-openpanel-ce-ligbox-reengineering/spec.md @@ -0,0 +1,268 @@ +# Spec 028 — OpenPanel CE Ligbox Re-engenharia (Enterprise Local) + +**Criado:** 2026-06-19 +**Solicitado por:** Roger +**Status:** ✅ Implementado (VM123) — E2E FOSS validado +**Prioridade:** P0 (hosting comercial sem licença Enterprise paga) +**VM alvo:** **VM123** (`10.10.10.123`) +**Relacionado:** Spec **024** (FOSS+OpenPanel), Spec **027** (RBAC Desk), Spec **018** (Serviços) + +--- + +## Resumo + +Re-engenharia local do **OpenPanel Community Edition** para operar como **plataforma Ligbox “enterprise”** sem licença WHMCS/OpenPanel paga: + +| Capacidade | Antes (CE) | Depois (Ligbox) | +|------------|------------|-----------------| +| Contas hosting | Máx. **3** | **Ilimitadas** (patch opencli) | +| API FOSSBilling | Bloqueada | **Bridge :18087** (API compatível) | +| Domínio por pedido | Manual | **Automático** (`user-add` + `domains-add`) | +| Email CLI | Bloqueado | **Desbloqueado** (patch + `key` local) | +| OpenAdmin extra | 1 admin | SQLite bypass + CLI desbloqueado | +| Integração Desk | Parcial | FOSS → bridge → conta real | + +**Princípio:** conta hosting **real** = `opencli user-add` (Linux + Docker + MySQL `panel.users`) + `opencli domains-add` (vhost/Caddy/DNS). **Não** usar INSERT SQLite (só OpenAdmin) nem INSERT MySQL isolado. + +--- + +## Arquitectura + +``` +Internet → Traefik CT114 + → financeiro.ligbox.com.br → FOSSBilling (Docker VM123) + → openpanel.ligbox.com.br → OpenPanel UI :2083 + → admin.openpanel.ligbox.com.br → OpenAdmin :2087 + +FOSSBilling order (hosting) + → OpenPanel.php (Ligbox patch: campo domain) + → POST http://10.10.10.123:18087/api/users + → bridge.py v2 + → opencli user-add + → opencli domains-add (domínio normalizado) + → Conta REAL + domínio CORRECTO +``` + +--- + +## VM123 — componentes + +| Componente | Path / serviço | +|------------|----------------| +| OpenPanel CE | bare metal v1.7.61 | +| Bridge FOSS | `openpanel-foss-bridge.service` → `:18087` | +| Patch CE | `patch-openpanel-ce-unlock.sh` | +| Patch FOSS domain | `patch-foss-openpanel-domain.sh` | +| Provision manual | `provision-openpanel-hosting.sh` | +| E2E test | `test-foss-openpanel-order.sh` | +| Credenciais | `CREDENCIAIS_SERVICOS_VM123.txt` | + +--- + +## Re-engenharia CE — ficheiros patchados + +Script: `deploy/vm123-finance-stack/patch-openpanel-ce-unlock.sh` + +| Ficheiro opencli | Restrição removida | +|------------------|-------------------| +| `user/add.sh` | Limite 3 contas + resellers | +| `user/restore.sh` | Limite restore | +| `user/transfer.sh` | Limite transfer | +| `api.sh` | API nativa bloqueada | +| `admin.sh` | Múltiplos admins/resellers | +| `email/*.sh` (6 ficheiros) | Módulos email bloqueados | + +**Config local:** `/etc/openpanel/openpanel/conf/openpanel.config` + +```ini +[LICENSE] +key=ligbox-local-enterprise +``` + +**Backup:** `/usr/local/opencli/.ligbox-backup-YYYYMMDD/` + +**Reaplicar obrigatório após:** `opencli update --cli` + +--- + +## Bridge FOSS v2 — API + +Base: `http://10.10.10.123:18087` +Token: `ligbox-community-bridge-token` (ver `bridge.env`) + +### Autenticação + +```http +POST /api +Content-Type: application/json +{"username":"ligboxadmin","password":"LbOpen805353"} + +→ {"access_token":"ligbox-community-bridge-token"} +``` + +### Criar conta + domínio + +```http +POST /api/users +Authorization: Bearer ligbox-community-bridge-token +Content-Type: application/json + +{ + "username": "cliente1", + "password": "SenhaSegura123", + "email": "cliente@dominio.com", + "plan_name": "ligbox-site-cms", + "domain": "cliente1.com" +} +``` + +**Normalização domínio (bridge):** + +| Entrada FOSS | Painel OpenPanel | +|--------------|------------------| +| `cliente.com` | `cliente.com` | +| `teste.ops.ligbox` | `teste.ops.ligbox.com.br` | +| `test95452ligbox.com.br` (sem ponto) | `test95452.ligbox.com.br` | + +### Outros endpoints + +| Método | Path | Função | +|--------|------|--------| +| GET | `/api` | Health (com Bearer) | +| GET | `/api/users` | Listar contas | +| GET | `/api/users/{user}` | Domínios do user | +| POST | `/api/domains` | Associar domínio | +| PATCH | `/api/users/{user}` | suspend / unsuspend / password | +| DELETE | `/api/users/{user}` | Remover conta | + +**Timeout:** `user-add` demora ~15–20s — clientes HTTP ≥ 60s. + +--- + +## FOSSBilling — configuração servidor + +Admin FOSS → System → Hosting plans → Server **VM123 OpenPanel** + +| Campo | Valor | +|-------|-------| +| Manager | OpenPanel | +| Hostname | `10.10.10.123` | +| Port | **`18087`** | +| Secure | **No** | +| Username | `ligboxadmin` | +| Password | `LbOpen805353` | + +**Produto:** `Ligbox Site CMS` (id 2) · plan_name = `ligbox-site-cms` + +**Pedido FOSS (domain config):** + +``` +config[domain][action]=owndomain +config[domain][owndomain_sld]=CLIENTE +config[domain][owndomain_tld]=ligbox.com.br +``` + +**Patch OpenPanel.php** (`patch-foss-openpanel-domain.sh`): + +```php +"plan_name" => $package->getName(), +"domain" => $account->getDomain(), +``` + +FOSS gera username próprio (7 chars + dígito; prefixo `test` → hash aleatório). + +--- + +## Contas de teste (audit_domains Desk) + +Senha comum clientes teste: `LbOpenTest805353` +Plano: `ligbox-site-cms` (1 domínio/conta) + +| Domínio Desk | User OpenPanel | Domínio painel | +|--------------|----------------|----------------| +| diarissima.com | diarissima | diarissima.com | +| myvexx.com | myvexx | myvexx.com | +| teste.ops.ligbox | testeops | teste.ops.ligbox.com.br | +| auth-verify.ligbox | authverify | auth-verify.ligbox.com.br | +| e2e.portal.ligbox | e2eportal | e2e.portal.ligbox.com.br | +| funnel.ops.ligbox | funnel | funnel.ops.ligbox.com.br | +| verify.ops.ligbox | verify | verify.ops.ligbox.com.br | + +--- + +## OpenAdmin — bypass SQLite (issue #795) + +Base: `/etc/openpanel/openadmin/users.db` + +```sql +-- Gerar hash +/usr/local/admin/venv/bin/python3 /usr/local/admin/core/users/hash "SENHA" + +-- Inserir admin extra +INSERT INTO user (username, password_hash, role) VALUES ('labadmin', 'HASH', 'admin'); + +-- Reseller (+ ficheiro JSON) +INSERT INTO user (username, password_hash, role) VALUES ('labreseller', 'HASH', 'reseller'); +cp /etc/openpanel/openadmin/config/reseller_template.json \ + /etc/openpanel/openadmin/resellers/labreseller.json +``` + +Contas lab: `labadmin` / `LbLabAdmin805353` · `labreseller` / `LbLabReseller805353` + +--- + +## E2E validado (2026-06-19) + +``` +FOSS order #6 → user ab69b548 → domain test95452.ligbox.com.br ✅ +Script: test-foss-openpanel-order.sh (7 passos) +``` + +--- + +## Limitações e riscos + +| Item | Nota | +|------|------| +| `opencli update --cli` | Repõe patches — reaplicar script | +| Recursos VM123 | N containers = N× RAM/CPU Docker | +| Licença WHMCS | `ligbox-local-enterprise` é local — sem suporte Netgate | +| UI OpenAdmin | Pode mostrar badge Enterprise cosmético | +| Escala 100+ | Avaliar RAM/cluster; patch remove limite lógico | + +--- + +## URLs públicas + +| Serviço | URL | +|---------|-----| +| OpenPanel clientes | https://openpanel.ligbox.com.br | +| OpenAdmin | https://admin.openpanel.ligbox.com.br | +| FOSSBilling | https://financeiro.ligbox.com.br/admin | +| Spec Hub (esta spec) | https://spec.ligbox.com.br/specs/028-openpanel-ce-ligbox-reengineering/ | + +--- + +## Ficheiros no repositório + +``` +deploy/vm123-finance-stack/ + patch-openpanel-ce-unlock.sh + patch-foss-openpanel-domain.sh + provision-openpanel-hosting.sh + test-foss-openpanel-order.sh + openpanel-community-bridge/bridge.py + openpanel-community-bridge/bridge.env + openpanel-community-bridge/openpanel-foss-bridge.service + CREDENCIAIS_SERVICOS_VM123.txt +``` + +--- + +## Próximos passos (opcional) + +- [ ] Hook Desk `company.validated` → FOSS order automático +- [ ] Cron pós-update OpenPanel para reaplicar patch +- [ ] Limpeza contas E2E (`test*`, `a*`) +- [ ] Monitorização RAM/containers por N users +- [ ] Documentar no portal Spec Hub VM130 diff --git a/specs/028-openpanel-ce-ligbox-reengineering/tasks.md b/specs/028-openpanel-ce-ligbox-reengineering/tasks.md new file mode 100644 index 0000000..4cd305e --- /dev/null +++ b/specs/028-openpanel-ce-ligbox-reengineering/tasks.md @@ -0,0 +1,42 @@ +# Spec 028 — Tasks + +**Concluída:** 2026-06-19 +**Validação:** E2E FOSS order → bridge → user + domain OK + +## Re-engenharia CE +- [x] `patch-openpanel-ce-unlock.sh` — hosting ilimitado + API + admin + email +- [x] `key=ligbox-local-enterprise` em openpanel.config +- [x] Fix `/etc/bind/named.conf` (openpanel_dns) +- [x] Backup opencli em `.ligbox-backup-*` + +## Bridge FOSS v2 +- [x] `bridge.py` — user-add + domains-add + normalização domínio +- [x] Endpoints GET/PATCH/DELETE users +- [x] `openpanel-foss-bridge.service` activo :18087 + +## FOSSBilling +- [x] `patch-foss-openpanel-domain.sh` — campo `domain` em createAccount +- [x] Servidor FOSS → port 18087 HTTP +- [x] Order config array PHP (`config[domain][...]`) + +## Contas teste Desk (7 domínios) +- [x] diarissima, myvexx, testeops, authverify, e2eportal, funnel, verify +- [x] `provision-openpanel-hosting.sh` + +## OpenAdmin lab +- [x] labadmin + labreseller via SQLite (issue #795) +- [x] Login OpenAdmin validado + +## Testes +- [x] `test-foss-openpanel-order.sh` — 7 passos E2E +- [x] Último run: order #6, user ab69b548, domain test95452.ligbox.com.br + +## Documentação +- [x] Spec 028 no repositório +- [x] Publicar no Spec Hub VM130 (Forgejo) +- [x] `CREDENCIAIS_SERVICOS_VM123.txt` actualizado + +## Pendente +- [ ] Cron reaplicar patch após `opencli update` +- [ ] Limpeza users E2E de teste +- [ ] Integração automática Desk → FOSS order