Template
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:
@@ -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')
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user