pinning + new version of backups
This commit is contained in:
@@ -576,27 +576,6 @@ async function loadCurrentUser() {
|
||||
usersLink?.classList.remove("hidden");
|
||||
}
|
||||
|
||||
if (currentUser.role === "admin" && !localStorage.getItem("overlord_settings_exported")) {
|
||||
localStorage.setItem("overlord_settings_exported", "1");
|
||||
try {
|
||||
const expRes = await fetch("/api/settings/export", { credentials: "include" });
|
||||
if (expRes.ok) {
|
||||
const blob = await expRes.blob();
|
||||
const disposition = expRes.headers.get("Content-Disposition") || "";
|
||||
const match = disposition.match(/filename="?([^"]+)"?/);
|
||||
const filename = match ? match[1] : "overlord-settings.json";
|
||||
const dlUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = dlUrl;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(dlUrl);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (currentUser.role === "admin" || currentUser.role === "operator" || currentUser.canBuild) {
|
||||
buildLink?.classList.remove("hidden");
|
||||
}
|
||||
|
||||
@@ -2018,7 +2018,7 @@ async function exportSettings() {
|
||||
const blob = await res.blob();
|
||||
const disposition = res.headers.get("Content-Disposition") || "";
|
||||
const filenameMatch = disposition.match(/filename="?([^"]+)"?/);
|
||||
const filename = filenameMatch ? filenameMatch[1] : "overlord-settings.json";
|
||||
const filename = filenameMatch ? filenameMatch[1] : "overlord-server-backup.zip";
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
@@ -2046,35 +2046,39 @@ async function importSettings(event) {
|
||||
|
||||
event.target.value = "";
|
||||
|
||||
if (file.size > 512 * 1024) {
|
||||
showExportImportMessage("File too large (max 512 KB).", "error");
|
||||
if (file.size > 1024 * 1024 * 1024) {
|
||||
showExportImportMessage("File too large (max 1 GB).", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
const text = await file.text();
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
showExportImportMessage("Invalid JSON file.", "error");
|
||||
return;
|
||||
const isLegacyJson = file.name.toLowerCase().endsWith(".json") || file.type === "application/json";
|
||||
let requestBody;
|
||||
let contentType;
|
||||
if (isLegacyJson) {
|
||||
try {
|
||||
const data = JSON.parse(await file.text());
|
||||
if (!data || typeof data !== "object") throw new Error();
|
||||
requestBody = JSON.stringify(data);
|
||||
contentType = "application/json";
|
||||
} catch {
|
||||
showExportImportMessage("Invalid legacy settings JSON file.", "error");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
requestBody = await file.arrayBuffer();
|
||||
contentType = "application/zip";
|
||||
}
|
||||
|
||||
if (!data || typeof data !== "object") {
|
||||
showExportImportMessage("File does not contain a valid settings object.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm("Import settings from this file? This will overwrite your current configuration.")) {
|
||||
if (!confirm("Stage this server restore? On the next restart it will replace the current database, identity keys, TLS certificate, plugins, and persistent server data.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/settings/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": contentType },
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
const result = await res.json().catch(() => ({}));
|
||||
@@ -2086,7 +2090,8 @@ async function importSettings(event) {
|
||||
const appliedStr = result.applied?.length ? result.applied.join(", ") : "nothing";
|
||||
const warningStr = result.warnings?.length ? " \u26A0 " + result.warnings.join(" ") : "";
|
||||
const msgType = result.warnings?.length ? "warning" : "ok";
|
||||
showExportImportMessage(`Imported: ${appliedStr}.${warningStr}`, msgType);
|
||||
const restartStr = result.restartRequired ? " Restart the server to apply the restore." : "";
|
||||
showExportImportMessage(`Imported: ${appliedStr}.${restartStr}${warningStr}`, msgType);
|
||||
|
||||
await loadSecurityPolicy();
|
||||
await loadTlsSettings();
|
||||
|
||||
@@ -1536,16 +1536,16 @@
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Export & Import Settings</h2>
|
||||
<p class="text-sm text-slate-400">Backup or restore your Overlord configuration (notifications, security, TLS, appearance, auth tokens).</p>
|
||||
<p class="text-sm text-slate-400">Create or restore a portable server backup without changing its pinned identity.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-slate-700 bg-slate-900 px-4 py-4 flex flex-col sm:flex-row sm:items-center gap-4">
|
||||
<div class="flex-1 space-y-1">
|
||||
<p class="text-sm text-slate-300">
|
||||
<strong>Export</strong> — Downloads a JSON file with all current settings and auth tokens.
|
||||
<strong>Export</strong> — Downloads a checksummed ZIP containing the database, settings, identity keys, TLS certificate/private key, plugins, and persistent server files.
|
||||
</p>
|
||||
<p class="text-xs text-slate-500">Keep this file secure — it contains sensitive tokens.</p>
|
||||
<p class="text-xs text-slate-500">Keep this file secure — it contains private keys, credentials, users, and server data.</p>
|
||||
</div>
|
||||
<button
|
||||
id="export-settings-btn"
|
||||
@@ -1560,16 +1560,16 @@
|
||||
<div class="rounded-lg border border-slate-700 bg-slate-900 px-4 py-4 flex flex-col sm:flex-row sm:items-center gap-4">
|
||||
<div class="flex-1 space-y-1">
|
||||
<p class="text-sm text-slate-300">
|
||||
<strong>Import</strong> — Restore settings from a previously exported JSON file.
|
||||
<strong>Import</strong> — Validate and stage a portable ZIP backup. Legacy settings JSON files remain supported.
|
||||
</p>
|
||||
<p class="text-xs text-slate-500">Auth token changes require a server restart. Environment variables will still take priority.</p>
|
||||
<p class="text-xs text-slate-500">Portable restores are validated now and applied before startup with rollback protection. Explicit environment variables still take priority.</p>
|
||||
</div>
|
||||
<label
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-700 hover:bg-slate-600 transition-colors text-white text-sm font-medium whitespace-nowrap cursor-pointer"
|
||||
>
|
||||
<i class="fa-solid fa-upload"></i>
|
||||
Import Settings
|
||||
<input id="import-settings-file" type="file" accept=".json,application/json" class="hidden" />
|
||||
<input id="import-settings-file" type="file" accept=".zip,application/zip,.json,application/json" class="hidden" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user