Add WG/AWG client config ZIP export and backup restore.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+65
-2
@@ -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`, {
|
||||
|
||||
Reference in New Issue
Block a user