Add manual, optionally-encrypted backup download (no auto-upload)

New POST /backup/download (admin-only) tars the whole jsondb directory
(all servers/clients/users/settings) and streams it back as a file
download. If a passphrase is given, the archive is encrypted first
(AES-256-GCM, scrypt-derived key, backup/encrypt.go) - a small
self-contained format, not gpg/OpenPGP-compatible, to avoid shelling
out to an external binary or adding a PGP dependency.

Deliberately does NOT upload anywhere automatically (e.g. to
Nextcloud) - the archive only ever leaves the server as this one HTTP
response to the requesting admin, who is responsible for storing it
themselves. New "Download Backup" button + passphrase modal on the
Global Settings page.
This commit is contained in:
sysops
2026-07-12 00:16:47 +02:00
parent 31f504e61b
commit 144cb2982d
5 changed files with 277 additions and 0 deletions
+93
View File
@@ -120,9 +120,57 @@ Global Settings
</div>
</div>
<!-- /.row -->
<div class="row">
<div class="col-md-6">
<div class="card card-warning">
<div class="card-header">
<h3 class="card-title">Backup</h3>
</div>
<div class="card-body">
<p>Download a snapshot of the entire database (all servers, clients, users
and settings, including private keys). Optionally encrypt it with a
passphrase before it's downloaded - nothing is ever uploaded
automatically, this only produces a file for you to store yourself.</p>
<button type="button" class="btn btn-outline-warning" data-toggle="modal"
data-target="#modal_download_backup">Download Backup</button>
</div>
</div>
</div>
</div>
</div>
</section>
<div class="modal fade" id="modal_download_backup">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Download Backup</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="_backup_passphrase" class="control-label">Encryption passphrase (optional)</label>
<input type="password" class="form-control" id="_backup_passphrase"
placeholder="Leave empty for an unencrypted archive">
<small class="form-text text-muted">
If set, the archive is encrypted with AES-256-GCM. Keep the passphrase
somewhere safe - without it the backup cannot be restored.
</small>
</div>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" id="btn_confirm_download_backup">Download</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
<div class="modal fade" id="modal_endpoint_address_suggestion">
<div class="modal-dialog">
<div class="modal-content">
@@ -280,5 +328,50 @@ Global Settings
$("#modal_endpoint_address_suggestion").modal('hide');
});
});
// Download backup: fetch as a blob (can't be a plain form submit
// since the app's CSRF middleware requires application/json), then
// trigger a client-side download of the response.
$(document).ready(function () {
$("#btn_confirm_download_backup").click(function () {
const passphrase = $("#_backup_passphrase").val();
const btn = $(this);
btn.prop('disabled', true).text('Preparing...');
fetch('{{.basePath}}/backup/download', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({passphrase: passphrase})
}).then(function (resp) {
if (!resp.ok) {
return resp.json().then(function (body) {
throw new Error(body.message || 'Backup failed');
});
}
const disposition = resp.headers.get('Content-Disposition') || '';
const match = disposition.match(/filename=([^;]+)/);
const filename = match ? match[1].trim() : 'wireguard-ui-multi-backup.tar.gz';
return resp.blob().then(function (blob) {
return {blob: blob, filename: filename};
});
}).then(function (result) {
const url = window.URL.createObjectURL(result.blob);
const a = document.createElement('a');
a.href = url;
a.download = result.filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
$("#modal_download_backup").modal('hide');
$("#_backup_passphrase").val('');
toastr.success('Backup downloaded');
}).catch(function (err) {
toastr.error(err.message);
}).finally(function () {
btn.prop('disabled', false).text('Download');
});
});
});
</script>
{{end}}