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:
@@ -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">×</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}}
|
||||
|
||||
Reference in New Issue
Block a user