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:
Ligbox Spec Hub 2026-06-19 19:01:06 +00:00
parent 821675ab4a
commit db77a67606
11 changed files with 957 additions and 70 deletions

View file

@ -79,6 +79,11 @@
- [data-model.md](specs/027-desk-rbac-function-matrix/data-model.md) - [data-model.md](specs/027-desk-rbac-function-matrix/data-model.md)
- **contracts/** - **contracts/**
- [vm123-product-roles](specs/027-desk-rbac-function-matrix/contracts/vm123-product-roles.md) - [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 ## VM123 — Finance / Console
- **019-ops-console-active-operations** - **019-ops-console-active-operations**
- [📄 spec.md](specs/019-ops-console-active-operations/spec.md) - [📄 spec.md](specs/019-ops-console-active-operations/spec.md)

View file

@ -1,5 +1,9 @@
#!/usr/bin/env python3 #!/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 from __future__ import annotations
import json import json
@ -8,23 +12,56 @@ import re
import subprocess import subprocess
import sys import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer 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") HOST = os.environ.get("BRIDGE_HOST", "0.0.0.0")
PORT = int(os.environ.get("BRIDGE_PORT", "18087")) PORT = int(os.environ.get("BRIDGE_PORT", "18087"))
ADMIN_USER = os.environ.get("BRIDGE_ADMIN_USER", "ligboxadmin") ADMIN_USER = os.environ.get("BRIDGE_ADMIN_USER", "ligboxadmin")
ADMIN_PASS = os.environ.get("BRIDGE_ADMIN_PASS", "LbOpen805353") ADMIN_PASS = os.environ.get("BRIDGE_ADMIN_PASS", "LbOpen805353")
TOKEN = os.environ.get("BRIDGE_TOKEN", "ligbox-community-bridge-token") 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] cmd = ["opencli", *args]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) 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): class Handler(BaseHTTPRequestHandler):
server_version = "OpenPanelCommunityBridge/1.0" server_version = "LigboxOpenPanelBridge/2.0"
def log_message(self, fmt: str, *args) -> None: def log_message(self, fmt: str, *args) -> None:
sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args)) 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)) length = int(self.headers.get("Content-Length", 0))
if not length: if not length:
return {} 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: def _send(self, code: int, payload: dict) -> None:
body = json.dumps(payload).encode("utf-8") body = json.dumps(payload).encode("utf-8")
@ -44,13 +82,58 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(body) self.wfile.write(body)
def _auth_ok(self) -> bool: def _auth_ok(self) -> bool:
auth = self.headers.get("Authorization", "") return self.headers.get("Authorization", "") == f"Bearer {TOKEN}"
if auth == f"Bearer {TOKEN}":
return True def _provision_user(self, data: dict) -> tuple[int, dict]:
return False 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: def do_POST(self) -> None:
path = urlparse(self.path).path.rstrip("/") or "/" path = urlparse(self.path).path.rstrip("/") or "/"
if path == "/api": if path == "/api":
data = self._read_json() data = self._read_json()
if data.get("username") == ADMIN_USER and data.get("password") == ADMIN_PASS: if data.get("username") == ADMIN_USER and data.get("password") == ADMIN_PASS:
@ -64,23 +147,21 @@ class Handler(BaseHTTPRequestHandler):
return return
if path == "/api/users": 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() data = self._read_json()
username = data.get("username", "") domain = panel_domain_for(data.get("domain") or data.get("domain_name") or "")
password = data.get("password", "") username = (data.get("username") or "").strip().lower()
email = data.get("email", "") if not domain or not USER_RE.fullmatch(username):
plan = data.get("plan_name", "ligbox-site-cms") self._send(400, {"success": False, "error": "domain + username required"})
if not re.fullmatch(r"[a-z][a-z0-9]{2,15}", username):
self._send(400, {"success": False, "error": f"Invalid username: {username}"})
return return
code, out, err = run_opencli( code, out, err = run_opencli("domains-add", domain, username)
"user-add", username, password, email, plan, "--no-sentinel" msg = ok_message(out, err)
) if code == 0 or "success" in msg.lower() or "already exists" in msg.lower():
msg = out or err self._send(200, {"success": True, "response": {"message": msg, "domain": domain}})
if code == 0 or "Successfully added user" in msg:
self._send(200, {
"success": True,
"response": {"message": msg or f"Successfully added user {username}"},
})
else: else:
self._send(500, {"success": False, "error": msg}) self._send(500, {"success": False, "error": msg})
return return
@ -89,12 +170,43 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self) -> None: def do_GET(self) -> None:
path = urlparse(self.path).path.rstrip("/") or "/" path = urlparse(self.path).path.rstrip("/") or "/"
qs = parse_qs(urlparse(self.path).query)
if path == "/api": if path == "/api":
if not self._auth_ok(): if not self._auth_ok():
self._send(401, {"error": "Unauthorized"}) self._send(401, {"error": "Unauthorized"})
return return
self._send(200, {"message": "API is working!"}) self._send(200, {"message": "API is working!", "bridge": "ligbox-v2"})
return 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."}) self._send(404, {"error": "This api route does not exist."})
def do_PATCH(self) -> None: def do_PATCH(self) -> None:
@ -121,7 +233,7 @@ class Handler(BaseHTTPRequestHandler):
if code == 0: if code == 0:
self._send(200, {"success": True}) self._send(200, {"success": True})
else: 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: def do_DELETE(self) -> None:
if not self._auth_ok(): if not self._auth_ok():
@ -137,12 +249,12 @@ class Handler(BaseHTTPRequestHandler):
if code == 0: if code == 0:
self._send(200, {"success": True}) self._send(200, {"success": True})
else: else:
self._send(500, {"success": False, "error": out or err}) self._send(500, {"success": False, "error": ok_message(out, err)})
def main() -> None: def main() -> None:
httpd = ThreadingHTTPServer((HOST, PORT), Handler) 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() httpd.serve_forever()

View 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
"

View 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 "$@"

View 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 "$@"

View file

@ -1,70 +1,118 @@
#!/usr/bin/env bash #!/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 set -euo pipefail
FOSS_URL="${FOSS_URL:-https://financeiro.ligbox.com.br}" FOSS_URL="${FOSS_URL:-https://financeiro.ligbox.com.br}"
ADMIN_EMAIL="${FOSS_ADMIN_EMAIL:-admin@ligbox.com.br}" ADMIN_EMAIL="${FOSS_ADMIN_EMAIL:-admin@ligbox.com.br}"
ADMIN_PASS="${FOSS_ADMIN_PASS:-LbFossAdmin805353}" 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_USER="test$(date +%s | tail -c 6)"
TEST_EMAIL="${TEST_USER}@testprovision.ligbox.com.br" TEST_EMAIL="${TEST_USER}@testprovision.ligbox.com.br"
TEST_PASS="LbTest805353" TEST_PASS="LbTest805353"
TEST_DOMAIN="${TEST_USER}.ligbox.com.br"
COOKIE_JAR="$(mktemp)" COOKIE_JAR="$(mktemp)"
trap 'rm -f "$COOKIE_JAR"' EXIT trap 'rm -f "$COOKIE_JAR"' EXIT
echo "=== Spec 024 E2E: FOSS order → OpenPanel ===" resolve_op_user() {
echo "Test user: ${TEST_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" \ LOGIN=$(curl -sk -c "$COOKIE_JAR" -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/guest/staff/login" \
-d "email=${ADMIN_EMAIL}&password=${ADMIN_PASS}") -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 curl -sk -c "$COOKIE_JAR" -b "$COOKIE_JAR" "${FOSS_URL}/admin" >/dev/null
CSRF=$(awk '$6=="csrf_token" {print $7}' "$COOKIE_JAR" | tail -1) 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" \ 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&currency=BRL") -d "CSRFToken=${CSRF}&email=${TEST_EMAIL}&pass=${TEST_PASS}&first_name=Test&last_name=E2E&status=active&currency=BRL")
CID=$(echo "$CLIENT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('result',''))" 2>/dev/null || true) 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; } [[ -n "$CID" ]] || { echo "FALHOU cliente: $CLIENT"; exit 1; }
echo " Client id=${CID}" 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/7] Criar + activar encomenda hosting (product_id=2)..."
echo "[3/6] Encomendar produto hosting..."
ORDER=$(curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/order/create" \ 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&currency=BRL&activate=1&config=${CONFIG}") -d "CSRFToken=${CSRF}&client_id=${CID}&product_id=2&period=1M&currency=BRL&activate=1" \
echo "$ORDER" | python3 -m json.tool 2>/dev/null || echo "$ORDER" -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) 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 if [[ -n "$OID" ]]; then
echo " Order API falhou — a validar bridge directamente..." echo " order_id=${OID} — aguardar provisionamento (até 120s)..."
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}"
curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/order/activate" \ curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/order/activate" \
-d "CSRFToken=${CSRF}&id=${OID}" >/dev/null 2>&1 || true -d "CSRFToken=${CSRF}&id=${OID}" >/dev/null 2>&1 || true
sleep 5 for i in $(seq 1 24); do
fi OP_USER=$(resolve_op_user "${TEST_EMAIL}")
[[ -n "$OP_USER" ]] && break
echo "[5/6] Verificar utilizador OpenPanel..." sleep 5
if opencli user-list 2>/dev/null | grep -q "${TEST_USER}"; then done
echo " OK — utilizador ${TEST_USER} existe no OpenPanel"
else else
opencli user-list 2>/dev/null | tail -8 echo " Order API sem ID — fallback bridge directo..."
echo " AVISO — verificar manualmente" TOKEN=$(curl -sf --max-time 30 -X POST "${BRIDGE_URL}/api" -H "Content-Type: application/json" \
exit 1 -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 fi
echo "[6/6] Limpeza teste..." echo "[4/7] Resolver username OpenPanel (FOSS gera username próprio)..."
opencli user-delete "${TEST_USER}" -y 2>/dev/null || \ for _ in $(seq 1 12); do
curl -s -X DELETE "http://127.0.0.1:18087/api/users/${TEST_USER}" \ [[ -n "${OP_USER:-}" ]] || OP_USER=$(resolve_op_user "${TEST_EMAIL}")
-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 [[ -n "${OP_USER:-}" ]] && break
curl -sk -b "$COOKIE_JAR" -X POST "${FOSS_URL}/api/admin/client/delete" \ sleep 5
-d "CSRFToken=${CSRF}&id=${CID}" >/dev/null 2>&1 || true 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"

View file

@ -58,4 +58,4 @@ http:
vm123-openadmin: vm123-openadmin:
loadBalancer: loadBalancer:
servers: servers:
- url: https://10.10.10.123:2087 - url: http://10.10.10.123:2087

View file

@ -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)

View 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 |

View 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 ~1520s — 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

View 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