Orquestra create_client/order FOSS, bridge OpenPanel com metadata, upsert res.partner Odoo, mail-bundle wizard e webhook order-activated. Co-authored-by: Cursor <cursoragent@cursor.com>
160 lines
5.3 KiB
Python
160 lines
5.3 KiB
Python
"""OpenPanel Community bridge client."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
BRIDGE_URL = os.getenv("OPENPANEL_BRIDGE_URL", "http://10.10.10.123:18087").rstrip("/")
|
|
BRIDGE_TOKEN = os.getenv("OPENPANEL_BRIDGE_TOKEN", "")
|
|
OPENADMIN_URL = os.getenv("OPENADMIN_URL", "https://admin.openpanel.ligbox.com.br")
|
|
OPENPANEL_URL = os.getenv("OPENPANEL_URL", "https://openpanel.ligbox.com.br")
|
|
DEFAULT_PLAN = os.getenv("OPENPANEL_DEFAULT_PLAN", "ligbox-site-cms")
|
|
|
|
|
|
class OpenPanelBridgeError(Exception):
|
|
pass
|
|
|
|
|
|
def bridge_configured() -> bool:
|
|
return bool(BRIDGE_TOKEN)
|
|
|
|
|
|
def _headers() -> dict[str, str]:
|
|
if not bridge_configured():
|
|
raise OpenPanelBridgeError("OPENPANEL_BRIDGE_TOKEN ausente")
|
|
return {"Authorization": f"Bearer {BRIDGE_TOKEN}"}
|
|
|
|
|
|
def autologin_payload(username: str) -> dict[str, Any]:
|
|
"""MVP: devolve URL OpenAdmin + instrução CONNECT (Enterprise futuro)."""
|
|
return {
|
|
"username": username,
|
|
"openadmin_url": OPENADMIN_URL,
|
|
"openpanel_url": OPENPANEL_URL,
|
|
"note": "CONNECT autologin requer OpenPanel Enterprise API — use OpenAdmin manualmente",
|
|
"bridge_configured": bridge_configured(),
|
|
}
|
|
|
|
|
|
def health() -> dict[str, Any]:
|
|
if not bridge_configured():
|
|
return {"ok": False, "reason": "OPENPANEL_BRIDGE_TOKEN ausente", "bridge_url": BRIDGE_URL}
|
|
try:
|
|
with httpx.Client(timeout=10.0) as client:
|
|
res = client.get(f"{BRIDGE_URL}/api", headers=_headers())
|
|
body: dict[str, Any] = {}
|
|
try:
|
|
body = res.json()
|
|
except Exception:
|
|
pass
|
|
return {
|
|
"ok": res.status_code < 400,
|
|
"status": res.status_code,
|
|
"bridge_url": BRIDGE_URL,
|
|
"bridge": body.get("bridge"),
|
|
}
|
|
except Exception as exc:
|
|
return {"ok": False, "reason": str(exc), "bridge_url": BRIDGE_URL}
|
|
|
|
|
|
def list_users() -> dict[str, Any]:
|
|
with httpx.Client(timeout=30.0) as client:
|
|
res = client.get(f"{BRIDGE_URL}/api/users", headers=_headers())
|
|
if res.status_code >= 400:
|
|
raise OpenPanelBridgeError(f"bridge list_users HTTP {res.status_code}: {res.text[:300]}")
|
|
return res.json()
|
|
|
|
|
|
def get_user(username: str) -> dict[str, Any]:
|
|
with httpx.Client(timeout=30.0) as client:
|
|
res = client.get(f"{BRIDGE_URL}/api/users/{username}", headers=_headers())
|
|
if res.status_code >= 400:
|
|
raise OpenPanelBridgeError(f"bridge get_user HTTP {res.status_code}: {res.text[:300]}")
|
|
return res.json()
|
|
|
|
|
|
def provision_user(
|
|
*,
|
|
username: str,
|
|
password: str,
|
|
email: str,
|
|
domain: str,
|
|
plan_name: str | None = None,
|
|
metadata: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
payload: dict[str, Any] = {
|
|
"username": username.strip().lower(),
|
|
"password": password,
|
|
"email": email,
|
|
"domain": domain.strip().lower(),
|
|
"plan_name": plan_name or DEFAULT_PLAN,
|
|
}
|
|
if metadata:
|
|
payload["metadata"] = metadata
|
|
with httpx.Client(timeout=180.0) as client:
|
|
res = client.post(f"{BRIDGE_URL}/api/users", headers=_headers(), json=payload)
|
|
data = res.json() if res.content else {}
|
|
if res.status_code >= 400 or not data.get("success", True):
|
|
raise OpenPanelBridgeError(data.get("error") or f"bridge provision HTTP {res.status_code}")
|
|
return data
|
|
|
|
|
|
def provision_user_safe(
|
|
*,
|
|
username: str,
|
|
password: str,
|
|
email: str,
|
|
domain: str,
|
|
plan_name: str | None = None,
|
|
metadata: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Idempotente — reutiliza user existente se bridge reportar duplicado."""
|
|
try:
|
|
out = provision_user(
|
|
username=username,
|
|
password=password,
|
|
email=email,
|
|
domain=domain,
|
|
plan_name=plan_name,
|
|
metadata=metadata,
|
|
)
|
|
out["created"] = True
|
|
return out
|
|
except OpenPanelBridgeError as exc:
|
|
msg = str(exc).lower()
|
|
if "exist" in msg or "already" in msg:
|
|
try:
|
|
existing = get_user(username)
|
|
return {
|
|
"success": True,
|
|
"created": False,
|
|
"reused": True,
|
|
"username": username,
|
|
"domain": domain,
|
|
"response": existing,
|
|
}
|
|
except OpenPanelBridgeError:
|
|
raise exc
|
|
raise
|
|
|
|
|
|
def add_domain(*, username: str, domain: str) -> dict[str, Any]:
|
|
payload = {"username": username.strip().lower(), "domain": domain.strip().lower()}
|
|
with httpx.Client(timeout=180.0) as client:
|
|
res = client.post(f"{BRIDGE_URL}/api/domains", headers=_headers(), json=payload)
|
|
data = res.json() if res.content else {}
|
|
if res.status_code >= 400 or not data.get("success", True):
|
|
raise OpenPanelBridgeError(data.get("error") or f"bridge add_domain HTTP {res.status_code}")
|
|
return data
|
|
|
|
|
|
def delete_user(username: str) -> dict[str, Any]:
|
|
with httpx.Client(timeout=120.0) as client:
|
|
res = client.delete(f"{BRIDGE_URL}/api/users/{username.strip().lower()}", headers=_headers())
|
|
data = res.json() if res.content else {}
|
|
if res.status_code >= 400 or not data.get("success", True):
|
|
raise OpenPanelBridgeError(data.get("error") or f"bridge delete HTTP {res.status_code}")
|
|
return data
|