Add multi-server 3x-ui registry with per-panel subscription URLs.

Admins can manage several 3x-ui panels by name and select which one issues invite/user configs via that server's /sub base URL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-26 03:19:09 +03:00
co-authored by Cursor
parent 83bb73179b
commit 8abe692624
14 changed files with 767 additions and 104 deletions
+6
View File
@@ -87,6 +87,12 @@ def init_schema():
cur.execute(
"ALTER TABLE invite_links ADD COLUMN IF NOT EXISTS duration_days INTEGER NOT NULL DEFAULT 0"
)
cur.execute(
"ALTER TABLE invite_links ADD COLUMN IF NOT EXISTS xui_panel_id TEXT NOT NULL DEFAULT ''"
)
cur.execute(
"ALTER TABLE user_connections ADD COLUMN IF NOT EXISTS xui_panel_id TEXT NOT NULL DEFAULT ''"
)
conn.commit()
_schema_ready = True
logger.info('PostgreSQL schema ready')
+2
View File
@@ -42,6 +42,7 @@ CREATE TABLE IF NOT EXISTS user_connections (
protocol TEXT NOT NULL DEFAULT '',
client_id TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
xui_panel_id TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ,
last_bytes BIGINT NOT NULL DEFAULT 0
);
@@ -83,6 +84,7 @@ CREATE TABLE IF NOT EXISTS invite_links (
protocol TEXT NOT NULL DEFAULT 'xui',
server_id INTEGER NOT NULL DEFAULT 0,
xui_inbound_id INTEGER NOT NULL DEFAULT 0,
xui_panel_id TEXT NOT NULL DEFAULT '',
password_hash TEXT,
expires_at TIMESTAMPTZ,
duration_days INTEGER NOT NULL DEFAULT 0,
+18 -6
View File
@@ -47,6 +47,7 @@ DEFAULT_SETTINGS = {
'xui_inbound_id': 0,
'xui_sub_url': '',
},
'xui_servers': [],
'guest': {
'enabled': False,
'token': '',
@@ -56,6 +57,7 @@ DEFAULT_SETTINGS = {
'create_protocol': 'xui',
'create_server_id': 0,
'create_inbound_id': 0,
'create_xui_panel_id': '',
},
}
@@ -103,6 +105,12 @@ def _merge_settings(raw: Optional[dict]) -> dict:
for key, value in raw.items():
if key not in settings:
settings[key] = value
# Multi 3x-ui panels: migrate legacy sync.xui_* if needed
try:
from managers.xui_servers import ensure_xui_servers
ensure_xui_servers(settings)
except Exception:
settings.setdefault('xui_servers', [])
return settings
@@ -179,6 +187,7 @@ def _row_to_connection(row) -> dict:
'protocol': row['protocol'] or '',
'client_id': row['client_id'] or '',
'name': row['name'] or '',
'xui_panel_id': (row.get('xui_panel_id') or '') if hasattr(row, 'get') else (row['xui_panel_id'] if 'xui_panel_id' in row else ''),
'created_at': _ts_iso(row['created_at']),
'last_bytes': int(row['last_bytes'] or 0),
}
@@ -208,6 +217,7 @@ def _row_to_invite(row) -> dict:
'protocol': row['protocol'] or 'xui',
'server_id': int(row['server_id'] or 0),
'xui_inbound_id': int(row['xui_inbound_id'] or 0),
'xui_panel_id': (row.get('xui_panel_id') or '') if hasattr(row, 'get') else '',
'password_hash': row['password_hash'],
'expires_at': _ts_iso(row['expires_at']),
'duration_days': int(row.get('duration_days') or 0),
@@ -239,7 +249,7 @@ def load_data() -> dict:
cur.execute(
'SELECT id, user_id, server_id, protocol, client_id, name, '
'created_at, last_bytes FROM user_connections '
'xui_panel_id, created_at, last_bytes FROM user_connections '
'ORDER BY created_at NULLS LAST, id'
)
user_connections = [_row_to_connection(r) for r in cur.fetchall()]
@@ -253,7 +263,7 @@ def load_data() -> dict:
cur.execute(
'SELECT id, name, token, enabled, max_uses, used_count, user_id, '
'protocol, server_id, xui_inbound_id, password_hash, expires_at, '
'protocol, server_id, xui_inbound_id, xui_panel_id, password_hash, expires_at, '
'duration_days, note, created_at FROM invite_links '
'ORDER BY created_at DESC NULLS LAST, name'
)
@@ -358,8 +368,8 @@ def save_data(data: dict) -> None:
for conn_row in connections:
cur.execute(
'INSERT INTO user_connections ('
'id, user_id, server_id, protocol, client_id, name, created_at, last_bytes'
') VALUES (%s, %s, %s, %s, %s, %s, %s, %s)',
'id, user_id, server_id, protocol, client_id, name, xui_panel_id, created_at, last_bytes'
') VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)',
(
_as_uuid(conn_row['id']),
_as_uuid(conn_row['user_id']),
@@ -367,6 +377,7 @@ def save_data(data: dict) -> None:
conn_row.get('protocol') or '',
conn_row.get('client_id') or '',
conn_row.get('name') or '',
conn_row.get('xui_panel_id') or '',
_parse_ts(conn_row.get('created_at')),
int(conn_row.get('last_bytes') or 0),
),
@@ -393,9 +404,9 @@ def save_data(data: dict) -> None:
cur.execute(
'INSERT INTO invite_links ('
'id, name, token, enabled, max_uses, used_count, user_id, '
'protocol, server_id, xui_inbound_id, password_hash, expires_at, '
'protocol, server_id, xui_inbound_id, xui_panel_id, password_hash, expires_at, '
'duration_days, note, created_at'
') VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)',
') VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)',
(
_as_uuid(link['id']),
link.get('name') or '',
@@ -407,6 +418,7 @@ def save_data(data: dict) -> None:
link.get('protocol') or 'xui',
int(link.get('server_id') or 0),
int(link.get('xui_inbound_id') or 0),
link.get('xui_panel_id') or '',
link.get('password_hash'),
_parse_ts(link.get('expires_at')),
int(link.get('duration_days') or 0),