Add Spec 028: OpenPanel CE Ligbox re-engineering.
Documenta bypass CE, bridge FOSS :18087, E2E validado e scripts de deploy VM123.
This commit is contained in:
parent
821675ab4a
commit
db77a67606
11 changed files with 957 additions and 70 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
20
projects/finance/deploy/vm123-finance-stack/patch-foss-openpanel-domain.sh
Executable file
20
projects/finance/deploy/vm123-finance-stack/patch-foss-openpanel-domain.sh
Executable file
|
|
@ -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
|
||||
"
|
||||
148
projects/finance/deploy/vm123-finance-stack/patch-openpanel-ce-unlock.sh
Executable file
148
projects/finance/deploy/vm123-finance-stack/patch-openpanel-ce-unlock.sh
Executable file
|
|
@ -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 "$@"
|
||||
120
projects/finance/deploy/vm123-finance-stack/provision-openpanel-hosting.sh
Executable file
120
projects/finance/deploy/vm123-finance-stack/provision-openpanel-hosting.sh
Executable file
|
|
@ -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 "$@"
|
||||
128
projects/finance/deploy/vm123-finance-stack/test-foss-openpanel-order.sh
Normal file → Executable file
128
projects/finance/deploy/vm123-finance-stack/test-foss-openpanel-order.sh
Normal file → Executable file
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -58,4 +58,4 @@ http:
|
|||
vm123-openadmin:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: https://10.10.10.123:2087
|
||||
- url: http://10.10.10.123:2087
|
||||
|
|
|
|||
|
|
@ -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 <token>`
|
||||
|
||||
**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)
|
||||
69
specs/028-openpanel-ce-ligbox-reengineering/quickstart.md
Normal file
69
specs/028-openpanel-ce-ligbox-reengineering/quickstart.md
Normal file
|
|
@ -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 |
|
||||
268
specs/028-openpanel-ce-ligbox-reengineering/spec.md
Normal file
268
specs/028-openpanel-ce-ligbox-reengineering/spec.md
Normal file
|
|
@ -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
|
||||
42
specs/028-openpanel-ce-ligbox-reengineering/tasks.md
Normal file
42
specs/028-openpanel-ce-ligbox-reengineering/tasks.md
Normal file
|
|
@ -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
|
||||
Loading…
Reference in a new issue