Add WG/AWG client config ZIP export and backup restore.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-26 05:54:32 +03:00
co-authored by Cursor
parent f2311c0795
commit 2b28892a9b
5 changed files with 306 additions and 5 deletions
+65 -2
View File
@@ -751,12 +751,16 @@
</div>
<input type="hidden" id="backupProto" value="" />
<div class="form-hint" style="margin-bottom:var(--space-md);">{{ _('backup_desc') }}</div>
<div class="config-actions" style="margin-bottom:var(--space-md);">
<div class="config-actions" style="margin-bottom:var(--space-md); flex-wrap:wrap;">
<button class="btn btn-primary btn-sm" onclick="createProtocolBackup()" id="createBackupBtn" style="flex:1">
{{ _('create_backup') }}
</button>
<button class="btn btn-secondary btn-sm hidden" onclick="exportProtocolClients()" id="exportClientsBtn" style="flex:1">
{{ _('export_client_configs') }}
</button>
<button class="btn btn-secondary btn-sm" onclick="loadProtocolBackups()">{{ _('refresh') }}</button>
</div>
<div class="form-hint hidden" id="exportClientsHint" style="margin-bottom:var(--space-md);">{{ _('export_client_configs_hint') }}</div>
<div id="backupLoading" class="form-hint hidden">{{ _('loading') }}</div>
<div id="backupEmpty" class="empty-state hidden" style="padding:var(--space-xl);">
<div class="empty-icon">{{ icon('package') }}</div>
@@ -2014,6 +2018,10 @@
try { return new Date(mtime * 1000).toLocaleString(); } catch (_) { return '—'; }
}
function isWgBackupFamily(proto) {
return ['awg', 'awg2', 'awg_legacy', 'wireguard'].includes(protoBase(proto));
}
function backupDownloadUrl(proto, filename) {
return `/api/servers/${SERVER_ID}/backups/download`;
}
@@ -2021,6 +2029,9 @@
async function showProtocolBackups(proto) {
document.getElementById('backupProto').value = proto;
document.getElementById('backupModalTitle').textContent = `${_('backups')}${getProtoTitle(proto)}`;
const wg = isWgBackupFamily(proto);
document.getElementById('exportClientsBtn').classList.toggle('hidden', !wg);
document.getElementById('exportClientsHint').classList.toggle('hidden', !wg);
openModal('backupModal');
await loadProtocolBackups();
}
@@ -2041,12 +2052,13 @@
return;
}
list.innerHTML = backups.map(b => `
<div class="protocol-info-item" style="align-items:center; gap:var(--space-sm);">
<div class="protocol-info-item" style="align-items:center; gap:var(--space-sm); flex-wrap:wrap;">
<div style="min-width:0; flex:1;">
<div class="protocol-info-value" style="word-break:break-all;">${b.name}</div>
<div class="protocol-info-label">${formatBytes(b.size)}${formatBackupTime(b.mtime)}</div>
</div>
<button class="btn btn-secondary btn-sm" onclick="downloadProtocolBackup('${proto}', '${b.name}')">${_('download')}</button>
<button class="btn btn-danger btn-sm" onclick="restoreProtocolBackup('${proto}', '${b.name}')">${_('restore_protocol_backup')}</button>
</div>
`).join('');
} catch (err) {
@@ -2074,6 +2086,57 @@
}
}
async function exportProtocolClients() {
const proto = document.getElementById('backupProto').value;
const btn = document.getElementById('exportClientsBtn');
const oldText = btn.textContent;
btn.disabled = true;
btn.textContent = _('exporting_client_configs');
try {
const res = await fetch(`/api/servers/${SERVER_ID}/backups/export-clients`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ protocol: proto })
});
if (!res.ok) {
let message = 'Export failed';
try {
const data = await res.json();
message = data.error || message;
} catch (_) {}
throw new Error(message);
}
const blob = await res.blob();
const cd = res.headers.get('content-disposition') || '';
const match = cd.match(/filename="?([^"]+)"?/i);
const filename = match ? match[1] : `${proto}-clients.zip`;
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
showToast(_('export_client_configs_done'), 'success');
} catch (err) {
showToast(_('error') + ': ' + err.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = oldText;
}
}
async function restoreProtocolBackup(proto, filename) {
if (!confirm(_('restore_protocol_backup_confirm'))) return;
try {
await apiCall(`/api/servers/${SERVER_ID}/backups/restore`, 'POST', { protocol: proto, filename });
showToast(_('restore_protocol_backup_done'), 'success');
} catch (err) {
showToast(_('error') + ': ' + err.message, 'error');
}
}
async function downloadProtocolBackup(proto, filename) {
try {
const res = await fetch(`/api/servers/${SERVER_ID}/backups/download`, {