Let 3x-ui assign inbound and share link on create.
Stop requiring inbound selection in the site UI; send the create request to the chosen panel and use the link it returns. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3909,6 +3909,7 @@ async def api_add_user_connection(request: Request, user_id: str, req: AddUserCo
|
||||
created = await xui_create_vless_config(
|
||||
data.get('settings', {}),
|
||||
name=req.name or user.get('username') or 'user',
|
||||
# Optional hint only — panel assigns inbound + share link
|
||||
inbound_id=req.xui_inbound_id or panel.get('default_inbound_id') or None,
|
||||
panel_id=panel_id,
|
||||
)
|
||||
@@ -4490,8 +4491,6 @@ async def _create_config_for_protocol(
|
||||
"""Create VPN client; returns {client_id, config, subscription_url, protocol, server_id}."""
|
||||
protocol = protocol or 'xui'
|
||||
if protocol_base(protocol) == 'xui':
|
||||
if not xui_inbound_id:
|
||||
raise RuntimeError('Select a VLESS inbound for this invite link')
|
||||
from managers.xui_api import xui_create_vless_config
|
||||
from managers.xui_servers import get_xui_server, ensure_xui_servers
|
||||
ensure_xui_servers(data.get('settings') or {})
|
||||
@@ -4499,11 +4498,13 @@ async def _create_config_for_protocol(
|
||||
if not panel:
|
||||
raise RuntimeError('Add a 3x-ui server in Settings first')
|
||||
panel_id = panel.get('id') or ''
|
||||
# Prefer invite/default inbound; otherwise 3x-ui picks first VLESS on that panel
|
||||
inbound = xui_inbound_id or panel.get('default_inbound_id') or None
|
||||
expiry_ms = _expiry_ms_from_duration_days(duration_days)
|
||||
created = await xui_create_vless_config(
|
||||
data.get('settings', {}),
|
||||
name=name,
|
||||
inbound_id=xui_inbound_id,
|
||||
inbound_id=inbound,
|
||||
expiry_time=expiry_ms,
|
||||
panel_id=panel_id,
|
||||
)
|
||||
@@ -4515,6 +4516,7 @@ async def _create_config_for_protocol(
|
||||
'protocol': 'xui',
|
||||
'server_id': 0,
|
||||
'xui_panel_id': panel_id,
|
||||
'xui_inbound_id': int(created.get('inbound_id') or 0),
|
||||
'expires_at': (
|
||||
datetime.fromtimestamp(expiry_ms / 1000).isoformat()
|
||||
if expiry_ms > 0 else None
|
||||
@@ -4609,10 +4611,9 @@ async def api_create_invite(request: Request, req: InviteCreateRequest):
|
||||
if not panel:
|
||||
return JSONResponse({'error': 'Add a 3x-ui server in Settings first'}, status_code=400)
|
||||
panel_id = panel.get('id') or ''
|
||||
# Inbound is optional — 3x-ui panel assigns it (or uses server default) on redeem
|
||||
if not inbound_id:
|
||||
inbound_id = int(panel.get('default_inbound_id') or 0)
|
||||
if not inbound_id:
|
||||
return JSONResponse({'error': 'Select a VLESS inbound'}, status_code=400)
|
||||
link = {
|
||||
'id': str(uuid.uuid4()),
|
||||
'name': (req.name or 'Invite').strip() or 'Invite',
|
||||
@@ -4677,9 +4678,14 @@ async def api_update_invite(request: Request, invite_id: str, req: InviteUpdateR
|
||||
link['enabled'] = bool(req.enabled)
|
||||
if req.reset_used:
|
||||
link['used_count'] = 0
|
||||
# Validate inbound for xui
|
||||
if protocol_base(link.get('protocol') or 'xui') == 'xui' and not int(link.get('xui_inbound_id') or 0):
|
||||
return JSONResponse({'error': 'Select a VLESS inbound'}, status_code=400)
|
||||
if protocol_base(link.get('protocol') or 'xui') == 'xui':
|
||||
from managers.xui_servers import get_xui_server, ensure_xui_servers
|
||||
ensure_xui_servers(data.setdefault('settings', {}))
|
||||
panel = get_xui_server(data.get('settings') or {}, link.get('xui_panel_id') or None)
|
||||
if not panel:
|
||||
return JSONResponse({'error': 'Add a 3x-ui server in Settings first'}, status_code=400)
|
||||
if not link.get('xui_panel_id'):
|
||||
link['xui_panel_id'] = panel.get('id') or ''
|
||||
save_data(data)
|
||||
return {'status': 'success', 'invite': _invite_public_view(link)}
|
||||
|
||||
|
||||
+129
-28
@@ -222,34 +222,119 @@ class XuiApi:
|
||||
},
|
||||
)
|
||||
|
||||
links = await self.get_client_links(email)
|
||||
vless = next((u for u in links if isinstance(u, str) and u.startswith('vless://')), None)
|
||||
if not vless and links:
|
||||
vless = links[0]
|
||||
# Share URL is resolved by the caller via resolve_share_link (panel assigns link)
|
||||
return {
|
||||
'client_id': email,
|
||||
'uuid': client_uuid,
|
||||
'sub_id': sub_id,
|
||||
'email': email,
|
||||
'config': vless or '',
|
||||
'links': links,
|
||||
'config': '',
|
||||
'links': [],
|
||||
'expiry_time': expiry_time,
|
||||
'inbound_id': int(inbound_id),
|
||||
}
|
||||
|
||||
async def get_client_links(self, email: str) -> list:
|
||||
email = (email or '').strip()
|
||||
if not email:
|
||||
return []
|
||||
path = f'/panel/api/clients/links/{quote(email, safe="")}'
|
||||
for path in (
|
||||
f'/panel/api/clients/links/{quote(email, safe="")}',
|
||||
f'/panel/api/inbounds/clientLinks/0/{quote(email, safe="")}',
|
||||
):
|
||||
try:
|
||||
payload = await self._request('GET', path)
|
||||
obj = payload.get('obj') if isinstance(payload, dict) else None
|
||||
if isinstance(obj, list):
|
||||
return [x for x in obj if isinstance(x, str) and x.strip()]
|
||||
except XuiApiError as e:
|
||||
logger.warning('get_client_links %s failed: %s', path, e)
|
||||
return []
|
||||
|
||||
async def get_sub_links(self, sub_id: str) -> list:
|
||||
"""Ask 3x-ui for protocol URLs produced for this subscription id."""
|
||||
sub_id = (sub_id or '').strip()
|
||||
if not sub_id:
|
||||
return []
|
||||
path = f'/panel/api/inbounds/getSubLinks/{quote(sub_id, safe="")}'
|
||||
try:
|
||||
payload = await self._request('GET', path)
|
||||
obj = payload.get('obj') if isinstance(payload, dict) else None
|
||||
if isinstance(obj, list):
|
||||
return [x for x in obj if isinstance(x, str) and x.strip()]
|
||||
except XuiApiError as e:
|
||||
logger.warning('get_client_links failed: %s', e)
|
||||
logger.warning('getSubLinks failed: %s', e)
|
||||
return []
|
||||
|
||||
async def get_panel_sub_base(self) -> str:
|
||||
"""Try to read public subscription base URL from 3x-ui panel settings."""
|
||||
for path, method in (
|
||||
('/panel/api/setting', 'GET'),
|
||||
('/panel/setting/all', 'POST'),
|
||||
('/panel/api/settings', 'GET'),
|
||||
):
|
||||
try:
|
||||
payload = await self._request(method, path)
|
||||
except XuiApiError:
|
||||
continue
|
||||
obj = payload.get('obj') if isinstance(payload, dict) else payload
|
||||
if not isinstance(obj, dict):
|
||||
continue
|
||||
# Common field names across 3x-ui versions
|
||||
sub_uri = (obj.get('subURI') or obj.get('subUri') or obj.get('sub_uri') or '').strip()
|
||||
sub_path = (obj.get('subPath') or obj.get('sub_path') or '/sub').strip() or '/sub'
|
||||
sub_port = obj.get('subPort') or obj.get('sub_port')
|
||||
sub_listen = (obj.get('subListen') or '').strip()
|
||||
if sub_uri:
|
||||
return sub_uri.rstrip('/')
|
||||
# Build from panel host + sub path if only path is configured
|
||||
if sub_path and self.base_url:
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
parsed = urlparse(self.base_url)
|
||||
host = sub_listen or parsed.hostname or ''
|
||||
if not host:
|
||||
continue
|
||||
scheme = 'https' if parsed.scheme == 'https' else 'http'
|
||||
netloc = f'{host}:{sub_port}' if sub_port else host
|
||||
path_part = sub_path if sub_path.startswith('/') else f'/{sub_path}'
|
||||
return urlunparse((scheme, netloc, path_part.rstrip('/'), '', '', ''))
|
||||
return ''
|
||||
|
||||
async def resolve_share_link(self, *, email: str, sub_id: str = '', configured_sub_base: str = '') -> dict:
|
||||
"""Ask the panel for the shareable subscription / protocol link (do not invent config)."""
|
||||
email = (email or '').strip()
|
||||
sub_id = (sub_id or '').strip()
|
||||
base = (configured_sub_base or '').strip().rstrip('/')
|
||||
if not base:
|
||||
try:
|
||||
base = (await self.get_panel_sub_base() or '').rstrip('/')
|
||||
except Exception as e:
|
||||
logger.warning('get_panel_sub_base failed: %s', e)
|
||||
base = ''
|
||||
|
||||
links = []
|
||||
if sub_id:
|
||||
links = await self.get_sub_links(sub_id)
|
||||
if not links and email:
|
||||
links = await self.get_client_links(email)
|
||||
|
||||
# Prefer HTTP(S) link returned by 3x-ui; else build from panel/settings sub base + subId
|
||||
panel_http = next(
|
||||
(u for u in links if isinstance(u, str) and u.startswith(('http://', 'https://'))),
|
||||
'',
|
||||
)
|
||||
subscription_url = panel_http or (f'{base}/{sub_id}' if base and sub_id else '')
|
||||
share = subscription_url or next(
|
||||
(u for u in links if isinstance(u, str) and u.startswith(('vless://', 'vmess://', 'trojan://', 'ss://'))),
|
||||
'',
|
||||
)
|
||||
return {
|
||||
'subscription_url': subscription_url,
|
||||
'links': links,
|
||||
'share': share,
|
||||
'sub_base': base,
|
||||
}
|
||||
|
||||
async def delete_client(self, email: str) -> None:
|
||||
email = (email or '').strip()
|
||||
if not email:
|
||||
@@ -323,28 +408,27 @@ async def xui_create_vless_config(
|
||||
expiry_time: int = 0,
|
||||
panel_id: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Create a VLESS client on 3x-ui and return {client_id, config, subscription_url, links}."""
|
||||
"""Create a client via 3x-ui API; inbound + share link come from the panel response."""
|
||||
scoped = _resolve_settings(settings, panel_id)
|
||||
creds = _settings_creds(scoped)
|
||||
inbound = inbound_id if inbound_id is not None else creds.get('inbound_id')
|
||||
inbound = inbound_id if inbound_id not in (None, 0, '0') else creds.get('inbound_id')
|
||||
try:
|
||||
inbound = int(inbound)
|
||||
inbound = int(inbound or 0)
|
||||
except (TypeError, ValueError):
|
||||
inbound = 0
|
||||
|
||||
async with XuiApi.from_panel_settings(scoped) as api:
|
||||
# Panel assigns inbound: use configured default, else first VLESS on that panel
|
||||
if not inbound:
|
||||
vless_inbounds = await api.list_vless_inbounds()
|
||||
if not vless_inbounds:
|
||||
raise XuiApiError('No VLESS inbound found on 3x-ui — create one in the panel first')
|
||||
inbound = int(vless_inbounds[0]['id'])
|
||||
|
||||
# Sanitize email: 3x-ui emails are unique free-form ids
|
||||
base = ''.join(ch if ch.isalnum() or ch in '._-+@' else '_' for ch in (name or 'user').strip())
|
||||
base = base[:48] or f'user_{secrets.token_hex(4)}'
|
||||
email = base
|
||||
created = None
|
||||
# Avoid collisions
|
||||
for _ in range(5):
|
||||
try:
|
||||
created = await api.add_vless_client(
|
||||
@@ -367,22 +451,32 @@ async def xui_create_vless_config(
|
||||
expiry_time=int(expiry_time or 0),
|
||||
)
|
||||
|
||||
sub_url = build_subscription_url(scoped, created.get('sub_id') or '')
|
||||
# Prefer subscription URL as the main share string when configured
|
||||
if sub_url:
|
||||
created['subscription_url'] = sub_url
|
||||
created['config'] = sub_url
|
||||
else:
|
||||
created['subscription_url'] = ''
|
||||
created['inbound_id'] = inbound
|
||||
# Ask the panel for the subscription / protocol link (do not invent config here)
|
||||
share = await api.resolve_share_link(
|
||||
email=created.get('email') or email,
|
||||
sub_id=created.get('sub_id') or '',
|
||||
configured_sub_base=creds.get('sub_url') or '',
|
||||
)
|
||||
subscription_url = share.get('subscription_url') or ''
|
||||
panel_share = share.get('share') or ''
|
||||
panel_links = share.get('links') or created.get('links') or []
|
||||
|
||||
created['subscription_url'] = subscription_url
|
||||
created['links'] = panel_links
|
||||
# What we show the user: panel subscription URL, else panel protocol link
|
||||
created['config'] = subscription_url or panel_share or created.get('config') or ''
|
||||
if not created['config'] and panel_links:
|
||||
created['config'] = panel_links[0]
|
||||
created['inbound_id'] = int(created.get('inbound_id') or inbound)
|
||||
created['panel_id'] = panel_id or ''
|
||||
return created
|
||||
|
||||
|
||||
async def xui_get_config(settings: dict, email: str, panel_id: Optional[str] = None) -> str:
|
||||
scoped = _resolve_settings(settings, panel_id)
|
||||
creds = _settings_creds(scoped)
|
||||
async with XuiApi.from_panel_settings(scoped) as api:
|
||||
# Prefer subscription URL when subId is available on the client
|
||||
sub_id = ''
|
||||
try:
|
||||
for inbound in await api.list_inbounds():
|
||||
if not isinstance(inbound, dict):
|
||||
@@ -398,14 +492,21 @@ async def xui_get_config(settings: dict, email: str, panel_id: Optional[str] = N
|
||||
for c in settings_obj.get('clients') or []:
|
||||
if isinstance(c, dict) and (c.get('email') or '') == email:
|
||||
sub_id = (c.get('subId') or c.get('sub_id') or '').strip()
|
||||
sub_url = build_subscription_url(scoped, sub_id)
|
||||
if sub_url:
|
||||
return sub_url
|
||||
break
|
||||
if sub_id:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning('Could not resolve subscription URL for %s: %s', email, e)
|
||||
logger.warning('Could not resolve subId for %s: %s', email, e)
|
||||
|
||||
links = await api.get_client_links(email)
|
||||
vless = next((u for u in links if u.startswith('vless://')), None)
|
||||
share = await api.resolve_share_link(
|
||||
email=email,
|
||||
sub_id=sub_id,
|
||||
configured_sub_base=creds.get('sub_url') or '',
|
||||
)
|
||||
if share.get('share'):
|
||||
return share['share']
|
||||
links = share.get('links') or await api.get_client_links(email)
|
||||
vless = next((u for u in links if isinstance(u, str) and u.startswith('vless://')), None)
|
||||
if vless:
|
||||
return vless
|
||||
if links:
|
||||
|
||||
+10
-46
@@ -42,8 +42,11 @@
|
||||
<div>
|
||||
<div style="font-weight:700; font-size:1.05rem;">{{ inv.name }}</div>
|
||||
<div style="font-size:0.8rem; color:var(--text-muted); margin-top:2px;">
|
||||
{% if inv.protocol == 'xui' %}3x-ui VLESS{% else %}{{ inv.protocol }}{% endif %}
|
||||
· inbound #{{ inv.xui_inbound_id or '—' }}
|
||||
{% if inv.protocol == 'xui' %}
|
||||
3x-ui · {% for s in xui_servers if s.id == inv.xui_panel_id %}{{ s.name }}{% else %}{{ _('xui_server_select_label') }}{% endfor %}
|
||||
{% else %}
|
||||
{{ inv.protocol }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if inv.available %}
|
||||
@@ -138,17 +141,12 @@
|
||||
|
||||
<div class="form-group" id="inviteInboundGroup">
|
||||
<label class="form-label">{{ _('xui_server_select_label') }}</label>
|
||||
<select class="form-select" id="inviteXuiPanel" onchange="loadInviteInbounds()">
|
||||
<select class="form-select" id="inviteXuiPanel">
|
||||
{% for s in xui_servers %}
|
||||
<option value="{{ s.id }}">{{ s.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-hint">{{ _('xui_server_select_hint') }}</div>
|
||||
<label class="form-label" style="margin-top:var(--space-sm);">{{ _('xui_inbound_label') }}</label>
|
||||
<select class="form-select" id="inviteInboundId">
|
||||
<option value="">{{ _('invite_inbound_loading') }}</option>
|
||||
</select>
|
||||
<div class="form-hint">{{ _('invite_inbound_hint') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -183,7 +181,6 @@
|
||||
<script>
|
||||
const invitesData = {{ invites | tojson }};
|
||||
const xuiConfigured = {{ 'true' if xui_configured else 'false' }};
|
||||
const xuiDefaultInbound = {{ xui_default_inbound | int }};
|
||||
const xuiDefaultPanelId = {{ (xui_default_panel_id or '') | tojson }};
|
||||
|
||||
function inviteUrl(token) {
|
||||
@@ -198,38 +195,6 @@
|
||||
function updateInviteProtoFields() {
|
||||
const v = document.getElementById('inviteProtocol').value;
|
||||
document.getElementById('inviteInboundGroup').style.display = (v === 'xui') ? '' : 'none';
|
||||
if (v === 'xui') loadInviteInbounds(document.getElementById('inviteInboundId').value || xuiDefaultInbound);
|
||||
}
|
||||
|
||||
async function loadInviteInbounds(selected) {
|
||||
const select = document.getElementById('inviteInboundId');
|
||||
select.innerHTML = `<option value="">${_('invite_inbound_loading')}</option>`;
|
||||
if (!xuiConfigured) {
|
||||
select.innerHTML = `<option value="">${_('invite_inbound_need_xui')}</option>`;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const panelId = document.getElementById('inviteXuiPanel')?.value || xuiDefaultPanelId || '';
|
||||
const q = panelId ? `?panel_id=${encodeURIComponent(panelId)}` : '';
|
||||
const data = await apiCall('/api/settings/xui/inbounds' + q);
|
||||
const list = data.inbounds || [];
|
||||
if (!list.length) {
|
||||
select.innerHTML = `<option value="">${_('invite_inbound_empty')}</option>`;
|
||||
return;
|
||||
}
|
||||
select.innerHTML = '';
|
||||
list.forEach((ib, idx) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ib.id;
|
||||
opt.textContent = `${ib.remark || 'VLESS'} (:${ib.port})`;
|
||||
const prefer = selected || xuiDefaultInbound;
|
||||
if (prefer && String(ib.id) === String(prefer)) opt.selected = true;
|
||||
else if (!prefer && idx === 0) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
} catch (e) {
|
||||
select.innerHTML = `<option value="">${_('error')}: ${e.message}</option>`;
|
||||
}
|
||||
}
|
||||
|
||||
function parseProtocolValue() {
|
||||
@@ -269,7 +234,6 @@
|
||||
const p = document.getElementById('inviteXuiPanel');
|
||||
if (p && [...p.options].some(o => o.value === xuiDefaultPanelId)) p.value = xuiDefaultPanelId;
|
||||
}
|
||||
loadInviteInbounds(xuiDefaultInbound);
|
||||
openModal('inviteModal');
|
||||
}
|
||||
|
||||
@@ -293,7 +257,6 @@
|
||||
if (panelSel && inv.xui_panel_id && [...panelSel.options].some(o => o.value === inv.xui_panel_id)) {
|
||||
panelSel.value = inv.xui_panel_id;
|
||||
}
|
||||
loadInviteInbounds(inv.xui_inbound_id || xuiDefaultInbound);
|
||||
openModal('inviteModal');
|
||||
}
|
||||
|
||||
@@ -305,8 +268,9 @@
|
||||
try {
|
||||
const editId = document.getElementById('inviteEditId').value;
|
||||
const proto = parseProtocolValue();
|
||||
const inbound = parseInt(document.getElementById('inviteInboundId').value || '0');
|
||||
if (proto.protocol === 'xui' && !inbound) throw new Error(_('invite_inbound_required'));
|
||||
if (proto.protocol === 'xui' && !document.getElementById('inviteXuiPanel')?.value) {
|
||||
throw new Error(_('invite_inbound_need_xui'));
|
||||
}
|
||||
|
||||
const body = {
|
||||
name: document.getElementById('inviteName').value || 'Invite',
|
||||
@@ -315,7 +279,7 @@
|
||||
user_id: document.getElementById('inviteUserId').value || '',
|
||||
protocol: proto.protocol,
|
||||
server_id: proto.server_id,
|
||||
xui_inbound_id: inbound,
|
||||
xui_inbound_id: 0,
|
||||
xui_panel_id: document.getElementById('inviteXuiPanel')?.value || '',
|
||||
note: document.getElementById('inviteNote').value || '',
|
||||
};
|
||||
|
||||
+6
-55
@@ -127,15 +127,13 @@
|
||||
</div>
|
||||
<div class="form-group" id="guestInboundGroup" style="{% if settings.guest.create_protocol != 'xui' %}display:none;{% endif %}">
|
||||
<label class="form-label">{{ _('xui_server_select_label') }}</label>
|
||||
<select class="form-select" id="guest_create_xui_panel" onchange="loadXuiInbounds()">
|
||||
<select class="form-select" id="guest_create_xui_panel">
|
||||
{% for s in xui_servers %}
|
||||
<option value="{{ s.id }}" {% if settings.guest.create_xui_panel_id == s.id %}selected{% endif %}>{{ s.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label class="form-label" style="margin-top:var(--space-sm);">{{ _('xui_inbound_label') }}</label>
|
||||
<select class="form-select" id="guest_create_inbound_id">
|
||||
<option value="0">{{ _('xui_inbound_auto') }}</option>
|
||||
</select>
|
||||
<div class="form-hint">{{ _('xui_server_select_hint') }}</div>
|
||||
<input type="hidden" id="guest_create_inbound_id" value="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -852,30 +850,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Guest inbound loader still uses primary panel
|
||||
async function loadXuiInbounds() {
|
||||
const select = document.getElementById('guest_create_inbound_id') || document.getElementById('xuiInboundSelect');
|
||||
if (!select) return;
|
||||
const prev = select.value || '0';
|
||||
select.innerHTML = `<option value="0">${_('xui_inbound_auto')}</option>`;
|
||||
try {
|
||||
const panelSel = document.getElementById('guest_create_xui_panel');
|
||||
const panelId = panelSel ? panelSel.value : '';
|
||||
const q = panelId ? `?panel_id=${encodeURIComponent(panelId)}` : '';
|
||||
const res = await fetch('/api/settings/xui/inbounds' + q);
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Failed to load inbounds');
|
||||
(data.inbounds || []).forEach(ib => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ib.id;
|
||||
opt.textContent = `${ib.remark || 'VLESS'} (:${ib.port})`;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
if ([...select.options].some(o => o.value === String(prev))) select.value = String(prev);
|
||||
} catch (err) {
|
||||
console.warn('loadXuiInbounds:', err);
|
||||
}
|
||||
}
|
||||
// Inbound + share link are assigned by the selected 3x-ui panel on create
|
||||
async function loadXuiInbounds() {}
|
||||
async function loadGuestInbounds() {}
|
||||
|
||||
document.getElementById('syncCreateConns').addEventListener('change', (e) => {
|
||||
document.getElementById('autoConnFields').style.display = e.target.checked ? 'block' : 'none';
|
||||
@@ -1331,32 +1308,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGuestInbounds() {
|
||||
const select = document.getElementById('guest_create_inbound_id');
|
||||
if (!select) return;
|
||||
const preferred = {{ settings.guest.create_inbound_id | default(0) | int }};
|
||||
select.innerHTML = `<option value="0">${_('xui_inbound_auto')}</option>`;
|
||||
try {
|
||||
const res = await fetch('/api/settings/xui/inbounds');
|
||||
const data = await res.json();
|
||||
if (!res.ok) return;
|
||||
(data.inbounds || []).forEach(ib => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ib.id;
|
||||
opt.textContent = `${ib.remark || 'VLESS'} (:${ib.port})`;
|
||||
if (String(ib.id) === String(preferred)) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
if (document.getElementById('guest_allow_create')?.checked) {
|
||||
loadGuestInbounds();
|
||||
}
|
||||
document.getElementById('guest_allow_create')?.addEventListener('change', (e) => {
|
||||
if (e.target.checked) loadGuestInbounds();
|
||||
});
|
||||
|
||||
async function toggleBot() {
|
||||
const btn = document.getElementById('toggleBotBtn');
|
||||
const text = document.getElementById('toggleBotBtnText');
|
||||
|
||||
+4
-31
@@ -293,16 +293,12 @@
|
||||
|
||||
<div class="form-group" id="ucXuiInboundGroup" style="display:none;">
|
||||
<label class="form-label">{{ _('xui_server_select_label') }}</label>
|
||||
<select class="form-select" id="ucXuiPanel" onchange="loadXuiInbounds()">
|
||||
<select class="form-select" id="ucXuiPanel">
|
||||
{% for s in xui_servers %}
|
||||
<option value="{{ s.id }}" {% if s.id == xui_default_panel_id %}selected{% endif %}>{{ s.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label class="form-label" style="margin-top:var(--space-sm);">{{ _('xui_inbound_label') }}</label>
|
||||
<select class="form-select" id="ucXuiInbound">
|
||||
<option value="">{{ _('xui_inbound_auto') }}</option>
|
||||
</select>
|
||||
<div class="form-hint">{{ _('xui_inbound_hint') }}</div>
|
||||
<div class="form-hint">{{ _('xui_server_select_hint') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="ucNameGroup">
|
||||
@@ -643,35 +639,14 @@
|
||||
}
|
||||
|
||||
async function loadXuiInbounds() {
|
||||
const select = document.getElementById('ucXuiInbound');
|
||||
if (!select) return;
|
||||
select.innerHTML = `<option value="">${_('xui_inbound_auto')}</option>`;
|
||||
try {
|
||||
const panelId = document.getElementById('ucXuiPanel')?.value || '';
|
||||
const q = panelId ? `?panel_id=${encodeURIComponent(panelId)}` : '';
|
||||
const data = await apiCall('/api/settings/xui/inbounds' + q);
|
||||
(data.inbounds || []).forEach(ib => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = ib.id;
|
||||
opt.textContent = `${ib.remark || 'VLESS'} (:${ib.port})`;
|
||||
if (String(ib.id) === String(xuiDefaultInboundId)) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
} catch (err) {
|
||||
showToast(`${_('error')}: ${err.message}`, 'error');
|
||||
}
|
||||
// Inbound is assigned by the selected 3x-ui panel — nothing to load here.
|
||||
}
|
||||
|
||||
function updateXuiInboundVisibility() {
|
||||
const proto = document.getElementById('ucProtocol')?.value;
|
||||
const group = document.getElementById('ucXuiInboundGroup');
|
||||
if (!group) return;
|
||||
if (proto === 'xui') {
|
||||
group.style.display = 'block';
|
||||
loadXuiInbounds();
|
||||
} else {
|
||||
group.style.display = 'none';
|
||||
}
|
||||
group.style.display = (proto === 'xui') ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Show/hide connection fields
|
||||
@@ -872,8 +847,6 @@
|
||||
if (!clientId) throw new Error(_('select_connection'));
|
||||
body.client_id = clientId;
|
||||
} else if (body.protocol === 'xui') {
|
||||
const inbound = document.getElementById('ucXuiInbound').value;
|
||||
if (inbound) body.xui_inbound_id = parseInt(inbound);
|
||||
body.xui_panel_id = document.getElementById('ucXuiPanel')?.value || '';
|
||||
} else if (body.protocol === 'telemt') {
|
||||
body.telemt_quota = document.getElementById('ucTelemtQuota').value || null;
|
||||
|
||||
@@ -301,16 +301,16 @@
|
||||
"xui_sync_hint": "Clients from 3x-ui inbounds will be created, disabled, and deleted as panel users (matched by email)",
|
||||
"xui_inbound_label": "VLESS inbound",
|
||||
"xui_inbound_auto": "Auto (first VLESS inbound)",
|
||||
"xui_inbound_hint": "Used when creating 3x-ui VLESS configs. Create a VLESS inbound in 3x-ui first.",
|
||||
"xui_inbound_hint": "The 3x-ui panel assigns the inbound when creating a client. This site only sends the API request.",
|
||||
"xui_servers_title": "3x-ui servers",
|
||||
"xui_servers_hint": "Add multiple 3x-ui panels. When creating invites/configs, pick a server by name — its subscription base URL is used.",
|
||||
"xui_servers_hint": "Add 3x-ui panels. When creating invites/configs, pick a server — the panel assigns the inbound and returns the share link.",
|
||||
"xui_servers_empty": "No 3x-ui servers yet. Click Add server.",
|
||||
"xui_servers_manage_hint": "Panel URLs and subscription bases are managed in the “3x-ui servers” section below.",
|
||||
"xui_server_add": "Add server",
|
||||
"xui_server_name_label": "Name / description",
|
||||
"xui_server_name_hint": "Shown in invites and user forms (e.g. US, NL, Home).",
|
||||
"xui_server_select_label": "3x-ui server",
|
||||
"xui_server_select_hint": "Choose which 3x-ui panel (by description) will issue the subscription URL.",
|
||||
"xui_server_select_hint": "Pick a 3x-ui panel — it assigns the inbound and returns the subscription link.",
|
||||
"xui_server_delete_confirm": "Delete this 3x-ui server from the panel?",
|
||||
"sync_now_btn": "🔄 Sync now",
|
||||
"delete_sync_btn": "🗑 Delete synchronization",
|
||||
|
||||
@@ -301,16 +301,16 @@
|
||||
"xui_sync_hint": "Клиенты из inbounds 3x-ui будут создаваться, отключаться и удаляться как пользователи панели (связь по email)",
|
||||
"xui_inbound_label": "VLESS inbound",
|
||||
"xui_inbound_auto": "Авто (первый VLESS inbound)",
|
||||
"xui_inbound_hint": "Используется при создании конфигов 3x-ui VLESS. Сначала создайте VLESS inbound в 3x-ui.",
|
||||
"xui_inbound_hint": "Inbound выбирает панель 3x-ui при создании клиента. Здесь сайт только отправляет запрос.",
|
||||
"xui_servers_title": "Серверы 3x-ui",
|
||||
"xui_servers_hint": "Можно добавить несколько панелей 3x-ui. При создании инвайта/конфига выберите сервер по описанию — будет выдан URL подписки этого сервера.",
|
||||
"xui_servers_hint": "Добавьте панели 3x-ui. При создании инвайта/конфига выберите сервер — панель сама назначит inbound и вернёт ссылку.",
|
||||
"xui_servers_empty": "Серверов 3x-ui пока нет. Нажмите «Добавить сервер».",
|
||||
"xui_servers_manage_hint": "URL панелей и базовые URL подписок настраиваются в разделе «Серверы 3x-ui» ниже.",
|
||||
"xui_server_add": "Добавить сервер",
|
||||
"xui_server_name_label": "Название / описание",
|
||||
"xui_server_name_hint": "Так сервер будет отображаться в инвайтах и у пользователей (например: US, NL, Home).",
|
||||
"xui_server_select_label": "Сервер 3x-ui",
|
||||
"xui_server_select_hint": "Выберите, с какой панели 3x-ui выдавать URL подписки.",
|
||||
"xui_server_select_hint": "Выберите панель 3x-ui — она сама назначит inbound и вернёт ссылку подписки.",
|
||||
"xui_server_delete_confirm": "Удалить этот сервер 3x-ui из панели?",
|
||||
"sync_now_btn": "🔄 Синхронизировать сейчас",
|
||||
"delete_sync_btn": "🗑 Удалить синхронизацию",
|
||||
|
||||
Reference in New Issue
Block a user