Template
Add double-VPN cascade from entry server to exit server.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -477,6 +477,67 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cascade (double VPN) -->
|
||||
<section class="card" id="cascadeSection" style="margin-bottom: var(--space-xl);">
|
||||
<div class="card-header" style="align-items:flex-start; flex-wrap:wrap; gap:var(--space-sm);">
|
||||
<div>
|
||||
<h2 class="card-title" style="margin:0;">{{ _('cascade_title') }}</h2>
|
||||
<p class="form-hint" style="margin:var(--space-xs) 0 0;">{{ _('cascade_desc') }}</p>
|
||||
</div>
|
||||
<span class="badge badge-warn" id="cascadeStatusBadge">{{ _('cascade_off') }}</span>
|
||||
</div>
|
||||
<div style="padding: 0 var(--space-md) var(--space-md);">
|
||||
<div class="grid" style="display:grid; grid-template-columns:1fr 1fr; gap:var(--space-md);">
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('cascade_entry_protocol') }}</label>
|
||||
<select class="form-select" id="cascadeEntryProtocol">
|
||||
<option value="awg2">AmneziaWG 2.0</option>
|
||||
<option value="awg">AmneziaWG</option>
|
||||
<option value="awg_legacy">AWG Legacy</option>
|
||||
<option value="wireguard">WireGuard</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('cascade_exit_server') }}</label>
|
||||
<select class="form-select" id="cascadeExitServer" onchange="fillCascadeExitProtocols()">
|
||||
<option value="">{{ _('cascade_exit_server_none') }}</option>
|
||||
{% for s in all_servers %}
|
||||
{% if loop.index0 != server_id %}
|
||||
<option value="{{ loop.index0 }}"
|
||||
data-protocols="{% for k in (s.protocols or {}).keys() %}{{ k }}{% if not loop.last %},{% endif %}{% endfor %}">
|
||||
{{ s.name or s.host }} ({{ s.host }})
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">{{ _('cascade_exit_protocol') }}</label>
|
||||
<select class="form-select" id="cascadeExitProtocol">
|
||||
<option value="awg2">AmneziaWG 2.0</option>
|
||||
<option value="awg">AmneziaWG</option>
|
||||
<option value="awg_legacy">AWG Legacy</option>
|
||||
<option value="wireguard">WireGuard</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-hint" style="margin-bottom:var(--space-md);">{{ _('cascade_hint') }}</div>
|
||||
<div class="flex gap-sm" style="flex-wrap:wrap;">
|
||||
<button type="button" class="btn btn-primary" id="cascadeEnableBtn" onclick="setupCascade(true)">
|
||||
{{ _('cascade_enable') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="cascadeDisableBtn" onclick="setupCascade(false)">
|
||||
{{ _('cascade_disable') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="loadCascadeStatus()">{{ _('refresh') }}</button>
|
||||
</div>
|
||||
<div id="cascadeBusy" class="hidden" style="display:none; align-items:center; gap:var(--space-sm); margin-top:var(--space-md);">
|
||||
<div class="spinner" style="width:18px;height:18px;border-width:2px;"></div>
|
||||
<span id="cascadeBusyText">{{ _('cascade_working') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Connections Section (renamed from Clients) -->
|
||||
<div class="clients-section" id="connectionsSection" style="display:none;">
|
||||
<div class="clients-header">
|
||||
@@ -2607,8 +2668,107 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Cascade (double VPN) ==========
|
||||
const CASCADE_PROTOS = ['awg2', 'awg', 'awg_legacy', 'wireguard'];
|
||||
|
||||
function fillCascadeExitProtocols() {
|
||||
const sel = document.getElementById('cascadeExitServer');
|
||||
const opt = sel.options[sel.selectedIndex];
|
||||
const protoSel = document.getElementById('cascadeExitProtocol');
|
||||
const raw = (opt && opt.dataset.protocols) ? opt.dataset.protocols.split(',').filter(Boolean) : [];
|
||||
const available = raw.filter(p => CASCADE_PROTOS.includes(protoBase(p)));
|
||||
const prev = protoSel.value;
|
||||
protoSel.innerHTML = '';
|
||||
const list = available.length ? available : CASCADE_PROTOS;
|
||||
list.forEach(p => {
|
||||
const o = document.createElement('option');
|
||||
o.value = p;
|
||||
o.textContent = getProtoTitle(p);
|
||||
protoSel.appendChild(o);
|
||||
});
|
||||
if (list.includes(prev)) protoSel.value = prev;
|
||||
}
|
||||
|
||||
function setCascadeBusy(on, text) {
|
||||
const busy = document.getElementById('cascadeBusy');
|
||||
const busyText = document.getElementById('cascadeBusyText');
|
||||
const en = document.getElementById('cascadeEnableBtn');
|
||||
const dis = document.getElementById('cascadeDisableBtn');
|
||||
if (busyText && text) busyText.textContent = text;
|
||||
if (busy) {
|
||||
busy.classList.toggle('hidden', !on);
|
||||
busy.style.display = on ? 'flex' : 'none';
|
||||
}
|
||||
if (en) en.disabled = !!on;
|
||||
if (dis) dis.disabled = !!on;
|
||||
}
|
||||
|
||||
function updateCascadeBadge(cascade, live) {
|
||||
const badge = document.getElementById('cascadeStatusBadge');
|
||||
if (!badge) return;
|
||||
const enabled = !!(cascade && cascade.enabled);
|
||||
const up = !!(live && live.up);
|
||||
badge.className = 'badge ' + (enabled && up ? 'badge-success' : (enabled ? 'badge-warn' : 'badge-warn'));
|
||||
if (!enabled) badge.textContent = _('cascade_off');
|
||||
else if (up) badge.textContent = _('cascade_on');
|
||||
else badge.textContent = _('cascade_configured_down');
|
||||
}
|
||||
|
||||
async function loadCascadeStatus() {
|
||||
try {
|
||||
const res = await apiCall(`/api/servers/${SERVER_ID}/cascade`, 'POST', {});
|
||||
const c = res.cascade || {};
|
||||
if (c.entry_protocol) document.getElementById('cascadeEntryProtocol').value = c.entry_protocol;
|
||||
if (c.exit_server_id !== undefined && c.exit_server_id !== null && c.exit_server_id !== '') {
|
||||
document.getElementById('cascadeExitServer').value = String(c.exit_server_id);
|
||||
fillCascadeExitProtocols();
|
||||
}
|
||||
if (c.exit_protocol) {
|
||||
const exitProto = document.getElementById('cascadeExitProtocol');
|
||||
if ([...exitProto.options].some(o => o.value === c.exit_protocol)) {
|
||||
exitProto.value = c.exit_protocol;
|
||||
}
|
||||
}
|
||||
updateCascadeBadge(c, res.live || {});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupCascade(enable) {
|
||||
const entryProtocol = document.getElementById('cascadeEntryProtocol').value;
|
||||
const exitServerId = document.getElementById('cascadeExitServer').value;
|
||||
const exitProtocol = document.getElementById('cascadeExitProtocol').value;
|
||||
if (enable && exitServerId === '') {
|
||||
showToast(_('cascade_exit_required'), 'error');
|
||||
return;
|
||||
}
|
||||
if (enable && !confirm(_('cascade_enable_confirm'))) return;
|
||||
if (!enable && !confirm(_('cascade_disable_confirm'))) return;
|
||||
|
||||
setCascadeBusy(true, enable ? _('cascade_working') : _('cascade_disabling'));
|
||||
showToast(enable ? _('cascade_working') : _('cascade_disabling'), 'info');
|
||||
try {
|
||||
const res = await apiCall(`/api/servers/${SERVER_ID}/cascade/setup`, 'POST', {
|
||||
enabled: !!enable,
|
||||
entry_protocol: entryProtocol,
|
||||
exit_server_id: parseInt(exitServerId || '0', 10),
|
||||
exit_protocol: exitProtocol,
|
||||
});
|
||||
updateCascadeBadge(res.cascade || {}, { up: !!(res.cascade && res.cascade.up) });
|
||||
showToast(enable ? _('cascade_enabled') : _('cascade_disabled'), 'success');
|
||||
await loadCascadeStatus();
|
||||
} catch (err) {
|
||||
showToast(_('error') + ': ' + err.message, 'error');
|
||||
} finally {
|
||||
setCascadeBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Init ==========
|
||||
applyInstalledAppsVisibility();
|
||||
fillCascadeExitProtocols();
|
||||
loadCascadeStatus();
|
||||
checkServer();
|
||||
loadServerStats();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user