v1.7.0: pick server first, then protocol in create flows.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -219,6 +219,11 @@ GitHub Actions workflows in `.github/workflows/`:
|
|||||||
|
|
||||||
## 📋 Fix / changelog (this fork)
|
## 📋 Fix / changelog (this fork)
|
||||||
|
|
||||||
|
### v1.7.0
|
||||||
|
* **Clearer create flow**: pick **server first**, then **protocol** (WireGuard, AmneziaWG 2.0, …) — invites, guest access, and user connections no longer dump everything into one messy list.
|
||||||
|
* Protocol titles are human-readable and ordered (AWG 2.0 → AWG → Legacy → WireGuard → Xray → Telemt).
|
||||||
|
* 3x-ui is a separate “server” choice; VLESS inbounds still load from that panel’s API.
|
||||||
|
|
||||||
### v1.6.0
|
### v1.6.0
|
||||||
* **3x-ui multi-panel**: register several 3x-ui servers in Settings; pick a panel and load VLESS inbounds over its API when creating users/invites (share link comes from 3x-ui).
|
* **3x-ui multi-panel**: register several 3x-ui servers in Settings; pick a panel and load VLESS inbounds over its API when creating users/invites (share link comes from 3x-ui).
|
||||||
* **Docker / CI**: refreshed `Dockerfile` + compose, `.env.example`, CI checks, GHCR image workflow.
|
* **Docker / CI**: refreshed `Dockerfile` + compose, `.env.example`, CI checks, GHCR image workflow.
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ else:
|
|||||||
application_path = os.path.dirname(__file__)
|
application_path = os.path.dirname(__file__)
|
||||||
|
|
||||||
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
|
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
|
||||||
CURRENT_VERSION = "v1.6.0"
|
CURRENT_VERSION = "v1.7.0"
|
||||||
RELEASES_REPO_URL = "https://git.evilfox.cc/test2/Amnezia-Web-Panel-main"
|
RELEASES_REPO_URL = "https://git.evilfox.cc/test2/Amnezia-Web-Panel-main"
|
||||||
RELEASES_API_LATEST = "https://git.evilfox.cc/api/v1/repos/test2/Amnezia-Web-Panel-main/releases/latest"
|
RELEASES_API_LATEST = "https://git.evilfox.cc/api/v1/repos/test2/Amnezia-Web-Panel-main/releases/latest"
|
||||||
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
|
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
|
||||||
|
|||||||
+113
-29
@@ -127,17 +127,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<label class="form-label">{{ _('server_label') }}</label>
|
||||||
|
<select class="form-select" id="inviteServer" onchange="onInviteServerChange()">
|
||||||
|
{% if xui_configured %}
|
||||||
|
<option value="xui">3x-ui</option>
|
||||||
|
{% endif %}
|
||||||
|
{% for s in servers %}
|
||||||
|
<option value="{{ loop.index0 }}">{{ s.name or s.host }} ({{ s.host }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="form-hint">{{ _('server_then_protocol_hint') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="inviteProtoGroup">
|
||||||
<label class="form-label">{{ _('protocol_label') }}</label>
|
<label class="form-label">{{ _('protocol_label') }}</label>
|
||||||
<select class="form-select" id="inviteProtocol" onchange="updateInviteProtoFields()">
|
<select class="form-select" id="inviteProtocol" onchange="updateInviteProtoFields()">
|
||||||
<option value="xui">3x-ui VLESS</option>
|
<!-- filled by JS from selected server -->
|
||||||
{% for s in servers %}
|
|
||||||
{% set server_idx = loop.index0 %}
|
|
||||||
{% for key, info in (s.protocols or {}).items() %}
|
|
||||||
{% if info.installed and key.split('__')[0] in ['awg','awg2','awg_legacy','xray','wireguard','telemt'] %}
|
|
||||||
<option value="{{ key }}|{{ server_idx }}">{{ s.name or s.host }} — {{ key }}</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -187,9 +192,28 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
const invitesData = {{ invites | tojson }};
|
const invitesData = {{ invites | tojson }};
|
||||||
|
const serversData = {{ servers | tojson }};
|
||||||
const xuiConfigured = {{ 'true' if xui_configured else 'false' }};
|
const xuiConfigured = {{ 'true' if xui_configured else 'false' }};
|
||||||
const xuiDefaultInbound = {{ xui_default_inbound | int }};
|
const xuiDefaultInbound = {{ xui_default_inbound | int }};
|
||||||
const xuiDefaultPanelId = {{ (xui_default_panel_id or '') | tojson }};
|
const xuiDefaultPanelId = {{ (xui_default_panel_id or '') | tojson }};
|
||||||
|
const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt'];
|
||||||
|
const PROTO_TITLES = {
|
||||||
|
awg2: 'AmneziaWG 2.0',
|
||||||
|
awg: 'AmneziaWG',
|
||||||
|
awg_legacy: 'AWG Legacy',
|
||||||
|
wireguard: 'WireGuard',
|
||||||
|
xray: 'Xray (VLESS-Reality)',
|
||||||
|
telemt: 'Telemt',
|
||||||
|
xui: '3x-ui VLESS',
|
||||||
|
};
|
||||||
|
|
||||||
|
function protoTitle(key) {
|
||||||
|
const raw = String(key || '');
|
||||||
|
const base = raw.split('__')[0];
|
||||||
|
const title = PROTO_TITLES[base] || base.toUpperCase();
|
||||||
|
const m = raw.match(/__(\d+)$/);
|
||||||
|
return m ? `${title} #${m[1]}` : title;
|
||||||
|
}
|
||||||
|
|
||||||
function inviteUrl(token) {
|
function inviteUrl(token) {
|
||||||
return `${window.location.origin}/invite/${token}`;
|
return `${window.location.origin}/invite/${token}`;
|
||||||
@@ -200,6 +224,57 @@
|
|||||||
showToast(_('copied'), 'success');
|
showToast(_('copied'), 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fillInviteProtocols(preferProtocol) {
|
||||||
|
const serverVal = document.getElementById('inviteServer').value;
|
||||||
|
const sel = document.getElementById('inviteProtocol');
|
||||||
|
sel.innerHTML = '';
|
||||||
|
|
||||||
|
if (serverVal === 'xui') {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = 'xui';
|
||||||
|
opt.textContent = PROTO_TITLES.xui;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
updateInviteProtoFields();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = serversData[parseInt(serverVal, 10)];
|
||||||
|
const protocols = (server && server.protocols) || {};
|
||||||
|
const installed = Object.keys(protocols).filter(k => {
|
||||||
|
const info = protocols[k] || {};
|
||||||
|
if (!info.installed) return false;
|
||||||
|
return VPN_PROTO_ORDER.includes(k.split('__')[0]);
|
||||||
|
});
|
||||||
|
installed.sort((a, b) => {
|
||||||
|
const ia = VPN_PROTO_ORDER.indexOf(a.split('__')[0]);
|
||||||
|
const ib = VPN_PROTO_ORDER.indexOf(b.split('__')[0]);
|
||||||
|
return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib) || a.localeCompare(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!installed.length) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = '';
|
||||||
|
opt.textContent = _('no_protocols');
|
||||||
|
opt.disabled = true;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
updateInviteProtoFields();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
installed.forEach(key => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = key;
|
||||||
|
opt.textContent = protoTitle(key);
|
||||||
|
if (preferProtocol && preferProtocol === key) opt.selected = true;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
updateInviteProtoFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInviteServerChange() {
|
||||||
|
fillInviteProtocols();
|
||||||
|
}
|
||||||
|
|
||||||
function updateInviteProtoFields() {
|
function updateInviteProtoFields() {
|
||||||
const v = document.getElementById('inviteProtocol').value;
|
const v = document.getElementById('inviteProtocol').value;
|
||||||
document.getElementById('inviteInboundGroup').style.display = (v === 'xui') ? '' : 'none';
|
document.getElementById('inviteInboundGroup').style.display = (v === 'xui') ? '' : 'none';
|
||||||
@@ -237,25 +312,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseProtocolValue() {
|
function parseInviteSelection() {
|
||||||
const raw = document.getElementById('inviteProtocol').value;
|
const serverVal = document.getElementById('inviteServer').value;
|
||||||
if (raw === 'xui') return { protocol: 'xui', server_id: 0 };
|
const protocol = document.getElementById('inviteProtocol').value;
|
||||||
if (raw.includes('|')) {
|
if (serverVal === 'xui' || protocol === 'xui') {
|
||||||
const [protocol, sid] = raw.split('|');
|
return { protocol: 'xui', server_id: 0 };
|
||||||
return { protocol, server_id: parseInt(sid || '0') };
|
|
||||||
}
|
}
|
||||||
return { protocol: raw, server_id: 0 };
|
return { protocol, server_id: parseInt(serverVal || '0', 10) || 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
function setProtocolSelect(protocol, serverId) {
|
function setInviteSelection(protocol, serverId) {
|
||||||
const sel = document.getElementById('inviteProtocol');
|
const serverSel = document.getElementById('inviteServer');
|
||||||
if (protocol === 'xui') sel.value = 'xui';
|
if (protocol === 'xui') {
|
||||||
else {
|
if ([...serverSel.options].some(o => o.value === 'xui')) serverSel.value = 'xui';
|
||||||
const want = `${protocol}|${serverId || 0}`;
|
fillInviteProtocols('xui');
|
||||||
if ([...sel.options].some(o => o.value === want)) sel.value = want;
|
return;
|
||||||
else sel.value = 'xui';
|
|
||||||
}
|
}
|
||||||
updateInviteProtoFields();
|
const sid = String(serverId || 0);
|
||||||
|
if ([...serverSel.options].some(o => o.value === sid)) serverSel.value = sid;
|
||||||
|
else if (serverSel.options.length) serverSel.selectedIndex = xuiConfigured ? 1 : 0;
|
||||||
|
fillInviteProtocols(protocol);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreateInvite() {
|
function openCreateInvite() {
|
||||||
@@ -269,12 +345,17 @@
|
|||||||
document.getElementById('inviteNote').value = '';
|
document.getElementById('inviteNote').value = '';
|
||||||
document.getElementById('inviteClearPwdWrap').style.display = 'none';
|
document.getElementById('inviteClearPwdWrap').style.display = 'none';
|
||||||
document.getElementById('inviteResetUsedWrap').style.display = 'none';
|
document.getElementById('inviteResetUsedWrap').style.display = 'none';
|
||||||
setProtocolSelect('xui', 0);
|
const serverSel = document.getElementById('inviteServer');
|
||||||
|
if (xuiConfigured && [...serverSel.options].some(o => o.value === 'xui')) serverSel.value = 'xui';
|
||||||
|
else if (serverSel.options.length) serverSel.selectedIndex = 0;
|
||||||
|
fillInviteProtocols();
|
||||||
if (xuiDefaultPanelId) {
|
if (xuiDefaultPanelId) {
|
||||||
const p = document.getElementById('inviteXuiPanel');
|
const p = document.getElementById('inviteXuiPanel');
|
||||||
if (p && [...p.options].some(o => o.value === xuiDefaultPanelId)) p.value = xuiDefaultPanelId;
|
if (p && [...p.options].some(o => o.value === xuiDefaultPanelId)) p.value = xuiDefaultPanelId;
|
||||||
}
|
}
|
||||||
loadInviteInbounds(xuiDefaultInbound);
|
if (document.getElementById('inviteProtocol').value === 'xui') {
|
||||||
|
loadInviteInbounds(xuiDefaultInbound);
|
||||||
|
}
|
||||||
openModal('inviteModal');
|
openModal('inviteModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,12 +374,14 @@
|
|||||||
document.getElementById('inviteClearPassword').checked = false;
|
document.getElementById('inviteClearPassword').checked = false;
|
||||||
document.getElementById('inviteResetUsedWrap').style.display = 'block';
|
document.getElementById('inviteResetUsedWrap').style.display = 'block';
|
||||||
document.getElementById('inviteResetUsed').checked = false;
|
document.getElementById('inviteResetUsed').checked = false;
|
||||||
setProtocolSelect(inv.protocol || 'xui', inv.server_id || 0);
|
setInviteSelection(inv.protocol || 'xui', inv.server_id || 0);
|
||||||
const panelSel = document.getElementById('inviteXuiPanel');
|
const panelSel = document.getElementById('inviteXuiPanel');
|
||||||
if (panelSel && inv.xui_panel_id && [...panelSel.options].some(o => o.value === inv.xui_panel_id)) {
|
if (panelSel && inv.xui_panel_id && [...panelSel.options].some(o => o.value === inv.xui_panel_id)) {
|
||||||
panelSel.value = inv.xui_panel_id;
|
panelSel.value = inv.xui_panel_id;
|
||||||
}
|
}
|
||||||
loadInviteInbounds(inv.xui_inbound_id || xuiDefaultInbound);
|
if ((inv.protocol || '') === 'xui') {
|
||||||
|
loadInviteInbounds(inv.xui_inbound_id || xuiDefaultInbound);
|
||||||
|
}
|
||||||
openModal('inviteModal');
|
openModal('inviteModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +392,8 @@
|
|||||||
spinner.classList.remove('hidden');
|
spinner.classList.remove('hidden');
|
||||||
try {
|
try {
|
||||||
const editId = document.getElementById('inviteEditId').value;
|
const editId = document.getElementById('inviteEditId').value;
|
||||||
const proto = parseProtocolValue();
|
const proto = parseInviteSelection();
|
||||||
|
if (!proto.protocol) throw new Error(_('no_protocols'));
|
||||||
const inbound = parseInt(document.getElementById('inviteInboundId').value || '0');
|
const inbound = parseInt(document.getElementById('inviteInboundId').value || '0');
|
||||||
if (proto.protocol === 'xui' && !inbound) throw new Error(_('invite_inbound_required'));
|
if (proto.protocol === 'xui' && !inbound) throw new Error(_('invite_inbound_required'));
|
||||||
|
|
||||||
|
|||||||
+87
-18
@@ -110,21 +110,26 @@
|
|||||||
<div id="guestCreateFields"
|
<div id="guestCreateFields"
|
||||||
style="{% if not settings.guest.allow_create %}display:none;{% endif %} background: var(--bg-primary); padding: var(--space-md); border-radius: var(--radius-md);">
|
style="{% if not settings.guest.allow_create %}display:none;{% endif %} background: var(--bg-primary); padding: var(--space-md); border-radius: var(--radius-md);">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">{{ _('protocol_label') }}</label>
|
<label class="form-label">{{ _('server_label') }}</label>
|
||||||
<select class="form-select" id="guest_create_protocol" onchange="updateGuestCreateFields()">
|
<select class="form-select" id="guest_create_server" onchange="fillGuestProtocols()">
|
||||||
<option value="xui" {% if settings.guest.create_protocol == 'xui' %}selected{% endif %}>3x-ui VLESS</option>
|
{% if xui_servers %}
|
||||||
{% for s in servers %}
|
<option value="xui" {% if settings.guest.create_protocol == 'xui' %}selected{% endif %}>3x-ui</option>
|
||||||
{% set server_idx = loop.index0 %}
|
|
||||||
{% for key, info in (s.protocols or {}).items() %}
|
|
||||||
{% if info.installed and key.split('__')[0] in ['awg','awg2','awg_legacy','xray','wireguard','telemt'] %}
|
|
||||||
<option value="{{ key }}|{{ server_idx }}"
|
|
||||||
{% if settings.guest.create_protocol == key and settings.guest.create_server_id == server_idx %}selected{% endif %}>
|
|
||||||
{{ s.name or s.host }} — {{ key }}
|
|
||||||
</option>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% for s in servers %}
|
||||||
|
<option value="{{ loop.index0 }}"
|
||||||
|
{% if settings.guest.create_protocol != 'xui' and settings.guest.create_server_id == loop.index0 %}selected{% endif %}>
|
||||||
|
{{ s.name or s.host }} ({{ s.host }})
|
||||||
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<div class="form-hint">{{ _('server_then_protocol_hint') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{{ _('protocol_label') }}</label>
|
||||||
|
<select class="form-select" id="guest_create_protocol" onchange="updateGuestCreateFields()">
|
||||||
|
<!-- filled by JS -->
|
||||||
|
</select>
|
||||||
|
<input type="hidden" id="guest_create_protocol_pref" value="{{ settings.guest.create_protocol or '' }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="guestInboundGroup" style="{% if settings.guest.create_protocol != 'xui' %}display:none;{% endif %}">
|
<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>
|
<label class="form-label">{{ _('xui_server_select_label') }}</label>
|
||||||
@@ -1275,13 +1280,14 @@
|
|||||||
|
|
||||||
let createProtocol = 'xui';
|
let createProtocol = 'xui';
|
||||||
let createServerId = 0;
|
let createServerId = 0;
|
||||||
const guestProtoRaw = document.getElementById('guest_create_protocol').value;
|
const guestServerVal = document.getElementById('guest_create_server')?.value || 'xui';
|
||||||
if (guestProtoRaw === 'xui') {
|
const guestProtoVal = document.getElementById('guest_create_protocol')?.value || '';
|
||||||
|
if (guestServerVal === 'xui' || guestProtoVal === 'xui') {
|
||||||
createProtocol = 'xui';
|
createProtocol = 'xui';
|
||||||
} else if (guestProtoRaw.includes('|')) {
|
createServerId = 0;
|
||||||
const parts = guestProtoRaw.split('|');
|
} else {
|
||||||
createProtocol = parts[0];
|
createProtocol = guestProtoVal;
|
||||||
createServerId = parseInt(parts[1] || '0');
|
createServerId = parseInt(guestServerVal || '0', 10) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const guest = {
|
const guest = {
|
||||||
@@ -1314,6 +1320,68 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const guestServersData = {{ servers | default([]) | tojson }};
|
||||||
|
const VPN_PROTO_ORDER = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt'];
|
||||||
|
const PROTO_TITLES = {
|
||||||
|
awg2: 'AmneziaWG 2.0',
|
||||||
|
awg: 'AmneziaWG',
|
||||||
|
awg_legacy: 'AWG Legacy',
|
||||||
|
wireguard: 'WireGuard',
|
||||||
|
xray: 'Xray (VLESS-Reality)',
|
||||||
|
telemt: 'Telemt',
|
||||||
|
xui: '3x-ui VLESS',
|
||||||
|
};
|
||||||
|
function protoTitle(key) {
|
||||||
|
const raw = String(key || '');
|
||||||
|
const base = raw.split('__')[0];
|
||||||
|
const title = PROTO_TITLES[base] || base.toUpperCase();
|
||||||
|
const m = raw.match(/__(\d+)$/);
|
||||||
|
return m ? `${title} #${m[1]}` : title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillGuestProtocols() {
|
||||||
|
const serverSel = document.getElementById('guest_create_server');
|
||||||
|
const protoSel = document.getElementById('guest_create_protocol');
|
||||||
|
if (!serverSel || !protoSel) return;
|
||||||
|
const prefer = document.getElementById('guest_create_protocol_pref')?.value || protoSel.value || '';
|
||||||
|
const serverVal = serverSel.value;
|
||||||
|
protoSel.innerHTML = '';
|
||||||
|
if (serverVal === 'xui') {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = 'xui';
|
||||||
|
opt.textContent = PROTO_TITLES.xui;
|
||||||
|
protoSel.appendChild(opt);
|
||||||
|
updateGuestCreateFields();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const server = guestServersData[parseInt(serverVal, 10)];
|
||||||
|
const protocols = (server && server.protocols) || {};
|
||||||
|
const installed = Object.keys(protocols).filter(k => {
|
||||||
|
const info = protocols[k] || {};
|
||||||
|
return !!info.installed && VPN_PROTO_ORDER.includes(k.split('__')[0]);
|
||||||
|
}).sort((a, b) => {
|
||||||
|
const ia = VPN_PROTO_ORDER.indexOf(a.split('__')[0]);
|
||||||
|
const ib = VPN_PROTO_ORDER.indexOf(b.split('__')[0]);
|
||||||
|
return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib) || a.localeCompare(b);
|
||||||
|
});
|
||||||
|
if (!installed.length) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = '';
|
||||||
|
opt.disabled = true;
|
||||||
|
opt.textContent = _('no_protocols');
|
||||||
|
protoSel.appendChild(opt);
|
||||||
|
} else {
|
||||||
|
installed.forEach(key => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = key;
|
||||||
|
opt.textContent = protoTitle(key);
|
||||||
|
if (prefer && prefer === key) opt.selected = true;
|
||||||
|
protoSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateGuestCreateFields();
|
||||||
|
}
|
||||||
|
|
||||||
function updateGuestCreateFields() {
|
function updateGuestCreateFields() {
|
||||||
const v = document.getElementById('guest_create_protocol')?.value;
|
const v = document.getElementById('guest_create_protocol')?.value;
|
||||||
const group = document.getElementById('guestInboundGroup');
|
const group = document.getElementById('guestInboundGroup');
|
||||||
@@ -1321,6 +1389,7 @@
|
|||||||
if (v === 'xui') loadGuestInbounds();
|
if (v === 'xui') loadGuestInbounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fillGuestProtocols();
|
||||||
if (document.getElementById('guest_create_protocol')?.value === 'xui') {
|
if (document.getElementById('guest_create_protocol')?.value === 'xui') {
|
||||||
loadGuestInbounds();
|
loadGuestInbounds();
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-20
@@ -143,9 +143,10 @@
|
|||||||
<select class="form-select" id="newUserServer">
|
<select class="form-select" id="newUserServer">
|
||||||
<option value="">{{ _('no_create_conn') }}</option>
|
<option value="">{{ _('no_create_conn') }}</option>
|
||||||
{% for s in servers %}
|
{% for s in servers %}
|
||||||
<option value="{{ loop.index0 }}">{{ s.name }} ({{ s.host }})</option>
|
<option value="{{ loop.index0 }}">{{ s.name or s.host }} ({{ s.host }})</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<div class="form-hint">{{ _('server_then_protocol_hint') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="newUserProtoGroup" style="display:none;">
|
<div class="form-group" id="newUserProtoGroup" style="display:none;">
|
||||||
@@ -293,10 +294,14 @@
|
|||||||
<div class="form-group" id="ucServerGroup">
|
<div class="form-group" id="ucServerGroup">
|
||||||
<label class="form-label">{{ _('server_label') }}</label>
|
<label class="form-label">{{ _('server_label') }}</label>
|
||||||
<select class="form-select" id="ucServer">
|
<select class="form-select" id="ucServer">
|
||||||
|
{% if xui_servers %}
|
||||||
|
<option value="xui">3x-ui</option>
|
||||||
|
{% endif %}
|
||||||
{% for s in servers %}
|
{% for s in servers %}
|
||||||
<option value="{{ loop.index0 }}">{{ s.name }} ({{ s.host }})</option>
|
<option value="{{ loop.index0 }}">{{ s.name or s.host }} ({{ s.host }})</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
<div class="form-hint">{{ _('server_then_protocol_hint') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="ucProtoGroup">
|
<div class="form-group" id="ucProtoGroup">
|
||||||
@@ -626,30 +631,55 @@
|
|||||||
const select = document.getElementById(selectId);
|
const select = document.getElementById(selectId);
|
||||||
const group = groupId ? document.getElementById(groupId) : null;
|
const group = groupId ? document.getElementById(groupId) : null;
|
||||||
select.innerHTML = '';
|
select.innerHTML = '';
|
||||||
const vpnBases = new Set(['awg', 'awg2', 'awg_legacy', 'xray', 'telemt', 'wireguard']);
|
const vpnOrder = ['awg2', 'awg', 'awg_legacy', 'wireguard', 'xray', 'telemt'];
|
||||||
|
const titles = {
|
||||||
|
awg2: 'AmneziaWG 2.0',
|
||||||
|
awg: 'AmneziaWG',
|
||||||
|
awg_legacy: 'AWG Legacy',
|
||||||
|
wireguard: 'WireGuard',
|
||||||
|
xray: 'Xray (VLESS-Reality)',
|
||||||
|
telemt: 'Telemt',
|
||||||
|
xui: '3x-ui VLESS',
|
||||||
|
};
|
||||||
|
const protoTitle = (key) => {
|
||||||
|
const base = String(key || '').split('__')[0];
|
||||||
|
const title = titles[base] || base.toUpperCase();
|
||||||
|
const m = String(key || '').match(/__(\d+)$/);
|
||||||
|
return m ? `${title} #${m[1]}` : title;
|
||||||
|
};
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
|
// Special source: 3x-ui (server picker value "xui")
|
||||||
|
if (String(serverId) === 'xui') {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = 'xui';
|
||||||
|
opt.textContent = titles.xui;
|
||||||
|
select.appendChild(opt);
|
||||||
|
count = 1;
|
||||||
|
if (group) group.style.display = '';
|
||||||
|
if (selectId === 'ucProtocol') updateXuiInboundVisibility();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const server = (serverId !== '' && serversData[serverId]) ? serversData[serverId] : null;
|
const server = (serverId !== '' && serversData[serverId]) ? serversData[serverId] : null;
|
||||||
const protocols = (server && server.protocols) || {};
|
const protocols = (server && server.protocols) || {};
|
||||||
|
const installed = Object.keys(protocols).filter(key => {
|
||||||
|
const info = protocols[key] || {};
|
||||||
|
if (!info.installed) return false;
|
||||||
|
return vpnOrder.includes(key.split('__')[0]);
|
||||||
|
}).sort((a, b) => {
|
||||||
|
const ia = vpnOrder.indexOf(a.split('__')[0]);
|
||||||
|
const ib = vpnOrder.indexOf(b.split('__')[0]);
|
||||||
|
return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib) || a.localeCompare(b);
|
||||||
|
});
|
||||||
|
|
||||||
for (const [key, info] of Object.entries(protocols)) {
|
installed.forEach(key => {
|
||||||
if (!info.installed) continue;
|
|
||||||
const base = key.split('__')[0];
|
|
||||||
if (!vpnBases.has(base)) continue;
|
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = key;
|
opt.value = key;
|
||||||
opt.textContent = key === 'awg' ? 'AmneziaWG' : (key === 'awg2' ? 'AmneziaWG 2.0' : (key === 'awg_legacy' ? 'AWG Legacy' : (key === 'xray' ? 'Xray' : (key === 'wireguard' ? 'WireGuard' : (key === 'telemt' ? 'Telemt' : key.toUpperCase())))));
|
opt.textContent = protoTitle(key);
|
||||||
select.appendChild(opt);
|
select.appendChild(opt);
|
||||||
count++;
|
count++;
|
||||||
}
|
});
|
||||||
|
|
||||||
if (xuiConfigured && selectId === 'ucProtocol') {
|
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = 'xui';
|
|
||||||
opt.textContent = '3x-ui VLESS';
|
|
||||||
select.appendChild(opt);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
if (group) group.style.display = '';
|
if (group) group.style.display = '';
|
||||||
@@ -724,9 +754,12 @@
|
|||||||
|
|
||||||
document.getElementById('ucServer').addEventListener('change', (e) => {
|
document.getElementById('ucServer').addEventListener('change', (e) => {
|
||||||
updateProtocols(e.target.value, 'ucProtocol');
|
updateProtocols(e.target.value, 'ucProtocol');
|
||||||
if (document.getElementById('ucMode').value === 'existing') {
|
const mode = document.getElementById('ucMode').value;
|
||||||
|
if (mode === 'existing' && e.target.value !== 'xui') {
|
||||||
fetchExistingClients();
|
fetchExistingClients();
|
||||||
}
|
}
|
||||||
|
updateTelemtOptions('ucProtocol', 'ucTelemtOptions');
|
||||||
|
updateXuiInboundVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('ucProtocol').addEventListener('change', (e) => {
|
document.getElementById('ucProtocol').addEventListener('change', (e) => {
|
||||||
@@ -898,13 +931,18 @@
|
|||||||
spinner.classList.remove('hidden');
|
spinner.classList.remove('hidden');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const serverRaw = document.getElementById('ucServer').value;
|
||||||
|
const protocol = document.getElementById('ucProtocol').value;
|
||||||
const body = {
|
const body = {
|
||||||
server_id: parseInt(document.getElementById('ucServer').value) || 0,
|
server_id: serverRaw === 'xui' ? 0 : (parseInt(serverRaw, 10) || 0),
|
||||||
protocol: document.getElementById('ucProtocol').value,
|
protocol: protocol,
|
||||||
name: document.getElementById('ucName').value || 'VPN Connection',
|
name: document.getElementById('ucName').value || 'VPN Connection',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mode === 'existing') {
|
if (mode === 'existing') {
|
||||||
|
if (serverRaw === 'xui' || protocol === 'xui') {
|
||||||
|
throw new Error(_('link_existing_xui_unsupported') || 'Link existing is only for Amnezia server protocols');
|
||||||
|
}
|
||||||
const clientId = document.getElementById('ucExistingClient').value;
|
const clientId = document.getElementById('ucExistingClient').value;
|
||||||
if (!clientId) throw new Error(_('select_connection'));
|
if (!clientId) throw new Error(_('select_connection'));
|
||||||
body.client_id = clientId;
|
body.client_id = clientId;
|
||||||
|
|||||||
@@ -153,6 +153,7 @@
|
|||||||
"server_label": "Server",
|
"server_label": "Server",
|
||||||
"no_create_conn": "— Do not create connection —",
|
"no_create_conn": "— Do not create connection —",
|
||||||
"protocol_label": "Protocol",
|
"protocol_label": "Protocol",
|
||||||
|
"server_then_protocol_hint": "Choose a server first, then a protocol installed on it (WireGuard, AmneziaWG 2.0, etc.).",
|
||||||
"edit_user_title": "Edit user: {}",
|
"edit_user_title": "Edit user: {}",
|
||||||
"edit_user_tg": "Telegram ID (optional)",
|
"edit_user_tg": "Telegram ID (optional)",
|
||||||
"edit_user_email": "Email (optional)",
|
"edit_user_email": "Email (optional)",
|
||||||
|
|||||||
@@ -153,6 +153,7 @@
|
|||||||
"server_label": "Сервер",
|
"server_label": "Сервер",
|
||||||
"no_create_conn": "— Не создавать подключение —",
|
"no_create_conn": "— Не создавать подключение —",
|
||||||
"protocol_label": "Протокол",
|
"protocol_label": "Протокол",
|
||||||
|
"server_then_protocol_hint": "Сначала выберите сервер, затем протокол, установленный на нём (WireGuard, AmneziaWG 2.0 и т.д.).",
|
||||||
"edit_user_title": "Редактировать пользователя: {}",
|
"edit_user_title": "Редактировать пользователя: {}",
|
||||||
"edit_user_tg": "Telegram ID (опционально)",
|
"edit_user_tg": "Telegram ID (опционально)",
|
||||||
"edit_user_email": "Email (опционально)",
|
"edit_user_email": "Email (опционально)",
|
||||||
|
|||||||
Reference in New Issue
Block a user