mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
fix(backup): make the console restore leave instance settings alone
A disaster-recovery snapshot carries the singleton `app_settings` row — registration, the MFA requirement, the default locale and timezone, module availability, the notification and AI configuration, the document size cap and quota — and the restore wrote it back as a side effect of restoring one account. An operator putting a single record back on Wednesday reconfigured the whole host from Monday's file, silently, with nothing on screen saying so. Rebuilding a host from a snapshot is a real case, so the write stays: it is now `restoreInstanceSettings: true` on the request and a checkbox beside the typed gate, off unless the operator ticks it. Restoring an account and reconfiguring the installation are two decisions, so they are two answers. The 422 confirmation sentence and the dialog copy drop the instance-wide claim; the opt-in carries it instead, in all seven catalogues. Integration coverage runs both arms: without the flag the settings row keeps its own values, with it the file's values land.
This commit is contained in:
1 parent
5cb5c7df44
commit
6b366b4f27
12 files changed
+189
-22
No files matched your search
@@ -11,6 +11,10 @@
|
||||
* Metadata-only portable document exports are rejected before mutation; the
|
||||
* importer never fabricates content. Audit rows remain outside the wipe, and
|
||||
* cache invalidation runs only after the complete restore transaction.
|
||||
*
|
||||
* Everything above is scoped to ONE account. The instance-wide settings a
|
||||
* disaster-recovery payload also carries are not, and they are opt-in for that
|
||||
* reason: see the `restoreInstanceSettings` block below.
|
||||
*/
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
@@ -142,17 +146,26 @@ const handler = apiHandler(
|
||||
const { id } = await params;
|
||||
annotate({ action: { name: "admin.backups.restore" }, meta: { id } });
|
||||
|
||||
let body: { confirm?: string } = {};
|
||||
let body: { confirm?: string; restoreInstanceSettings?: boolean } = {};
|
||||
try {
|
||||
const raw = await request.text();
|
||||
if (raw.length > 64 * 1024) {
|
||||
return apiError(`Request body exceeds ${64 * 1024} bytes`, 413);
|
||||
}
|
||||
body = JSON.parse(raw) as { confirm?: string };
|
||||
body = JSON.parse(raw) as {
|
||||
confirm?: string;
|
||||
restoreInstanceSettings?: boolean;
|
||||
};
|
||||
} catch {
|
||||
return apiError("Invalid JSON body", 400);
|
||||
}
|
||||
|
||||
// Restoring one account's data and reconfiguring the whole host are two
|
||||
// different decisions, so they are two different answers. Anything but a
|
||||
// literal `true` restores the account alone.
|
||||
const restoreInstanceSettings = body.restoreInstanceSettings === true;
|
||||
annotate({ meta: { restore_instance_settings: restoreInstanceSettings } });
|
||||
|
||||
if (body.confirm !== "RESTORE") {
|
||||
await auditLog("admin.backups.restore.denied", {
|
||||
userId: admin.id,
|
||||
@@ -160,7 +173,7 @@ const handler = apiHandler(
|
||||
details: { reason: "missing_confirmation", backupId: id },
|
||||
});
|
||||
return apiError(
|
||||
"Confirmation token missing — restoring user data and included instance-wide settings requires confirm: 'RESTORE'",
|
||||
"Confirmation token missing — replacing an account's data requires confirm: 'RESTORE'",
|
||||
422,
|
||||
);
|
||||
}
|
||||
@@ -292,6 +305,7 @@ const handler = apiHandler(
|
||||
ownerId,
|
||||
ownerUsername: owner.username,
|
||||
snapshotExportedAt: payload.exportedAt,
|
||||
restoreInstanceSettings,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -441,7 +455,20 @@ const handler = apiHandler(
|
||||
where: { userId: ownerId },
|
||||
});
|
||||
|
||||
if (payload.appSettings) {
|
||||
// The one section of the file that is not this account's.
|
||||
//
|
||||
// A disaster-recovery payload carries the singleton `app_settings`
|
||||
// row — registration, the MFA requirement, the default locale and
|
||||
// timezone, module availability, the notification and AI
|
||||
// configuration, the document size cap and quota. Writing it back as
|
||||
// a side effect of restoring ONE account reconfigures the host for
|
||||
// everybody on it, silently and hours or weeks out of date: an
|
||||
// operator putting a single record back on Wednesday has no reason to
|
||||
// expect Monday's registration switch and Monday's upload cap to come
|
||||
// with it. Rebuilding a host from a snapshot is a real case, so the
|
||||
// write stays available — as an answer the operator gives, not one
|
||||
// the account restore assumes.
|
||||
if (payload.appSettings && restoreInstanceSettings) {
|
||||
const settings = payload.appSettings;
|
||||
const settingsData = {
|
||||
registrationEnabled: settings.registrationEnabled,
|
||||
@@ -2084,6 +2111,10 @@ const handler = apiHandler(
|
||||
backupId: id,
|
||||
ownerId,
|
||||
ownerUsername: owner.username,
|
||||
// Whether this restore also rewrote the host's own settings. The one
|
||||
// effect of a restore that reaches accounts other than `ownerId`, so
|
||||
// the trail has to be able to answer it later.
|
||||
restoreInstanceSettings,
|
||||
cleared,
|
||||
// The durable half of the report. The response reaches whoever was
|
||||
// looking at the screen; the audit row is still here next week when
|
||||
|
||||
Reference in new issue
Block a user