fix(backups): address restore review findings

This commit is contained in:
Marc-André Bombeck committed 2026-07-20 13:42:38 +02:00
1 parent 7eb3b91925
commit e9507a0c93
8 files changed
+29 -15

No files matched your search

+1 -1
View File
@@ -6243,7 +6243,7 @@
"restore": "Wiederherstellen",
"restoreAria": "Backup für {username} wiederherstellen",
"restoreTitle": "Aus Backup wiederherstellen",
"restoreDescription": "Dies ersetzt ALLE aktuellen Daten von {username} mit dem Snapshot von {when}. Vorhandene Messungen, Medikamente, Einnahmen, Stimmungseinträge, Benachrichtigungs-Kanäle, Push-Abos und Telegram-Lösch-Aufträge werden entfernt.",
"restoreDescription": "Dies ersetzt ALLE aktuellen Daten von {username} mit dem Snapshot von {when}. Vorhandene Messungen, Medikamente, Einnahmen, Stimmungseinträge, Benachrichtigungs-Kanäle, Push-Abos und Telegram-Lösch-Aufträge werden entfernt. Falls im Snapshot enthalten, überschreiben instanzweite Einstellungen die Konfiguration der gesamten Installation.",
"restorePromptLabel": "Tippe RESTORE zum Fortfahren",
"restoreConfirm": "Jetzt wiederherstellen",
"restoreInProgress": "Wiederherstellung läuft…",
+1 -1
View File
@@ -6243,7 +6243,7 @@
"restore": "Restore",
"restoreAria": "Restore from backup for {username}",
"restoreTitle": "Restore from backup",
"restoreDescription": "This will replace ALL current data for {username} with the snapshot from {when}. Existing measurements, medications, intake events, mood entries, notification channels, push subscriptions and queued Telegram deletions will be deleted.",
"restoreDescription": "This will replace ALL current data for {username} with the snapshot from {when}. Existing measurements, medications, intake events, mood entries, notification channels, push subscriptions and queued Telegram deletions will be deleted. If included in the snapshot, instance-wide settings overwrite the configuration for the entire installation.",
"restorePromptLabel": "Type RESTORE to continue",
"restoreConfirm": "Restore now",
"restoreInProgress": "Restoring…",
@@ -100,7 +100,7 @@ const handler = apiHandler(
details: { reason: "missing_confirmation", backupId: id },
});
return apiError(
"Confirmation token missing — body must include confirm: 'RESTORE'",
"Confirmation token missing — restoring user data and included instance-wide settings requires confirm: 'RESTORE'",
422,
);
}
@@ -403,13 +403,11 @@ const handler = apiHandler(
unit: measurement.unit,
source: measurement.source ?? "MANUAL",
measuredAt: new Date(measurement.measuredAt),
notes: measurement.notes ?? null,
notes: null,
notesEncrypted:
measurement.notesEncrypted !== undefined
? measurement.notesEncrypted === null
? null
: decodeEncryptedBytes(measurement.notesEncrypted)
: encryptNote(measurement.notes ?? null),
measurement.notesEncrypted == null
? encryptNote(measurement.notes ?? null)
: decodeEncryptedBytes(measurement.notesEncrypted),
externalId: measurement.externalId ?? null,
externalSourceVersion: measurement.externalSourceVersion ?? null,
glucoseContext: (measurement.glucoseContext ?? null) as never,
@@ -421,7 +421,7 @@ describe("GET /api/export/full-backup", () => {
expect(res.headers.get("content-disposition")).toMatch(/\.json"/);
const json = await res.json();
expect(json).toMatchObject({
schemaVersion: "1",
schemaVersion: "2",
userId: "user-1",
measurements: [],
medications: [],
@@ -1,5 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import de from "../../../../messages/de.json";
import en from "../../../../messages/en.json";
/**
* v1.4.15 phase-C5 — `/admin/backups` empty state.
@@ -76,4 +78,13 @@ describe("BackupsSection — empty state", () => {
const matches = (html.match(/Backup now/g) ?? []).length;
expect(matches).toBeGreaterThanOrEqual(2);
});
it("warns in both catalogs that restore overwrites instance-wide settings", () => {
expect(de.admin.section.backups.restoreDescription).toMatch(
/instanzweite Einstellungen/i,
);
expect(en.admin.section.backups.restoreDescription).toMatch(
/instance-wide settings/i,
);
});
});
@@ -76,7 +76,7 @@ const appSettings = {
assistantHealthScoreExplainerEnabled: false,
moduleAvailabilityJson: { nutrients: false, insights: true },
documentMaxFileBytes: 12_345_678,
documentQuotaBytes: 9_876_543_210n,
documentQuotaBytes: BigInt("9876543210"),
};
function makePrisma() {
@@ -159,7 +159,7 @@ describe("canonical disaster-recovery backup round-trip", () => {
assistantHealthScoreExplainerEnabled: false,
moduleAvailabilityJson: { insights: true, nutrients: false },
documentMaxFileBytes: 12_345_678,
documentQuotaBytes: 9_876_543_210n,
documentQuotaBytes: BigInt("9876543210"),
},
});
@@ -917,12 +917,12 @@ describe("canonical disaster-recovery backup round-trip", () => {
const originalCreateMany = tx.measurement.createMany.bind(
tx.measurement,
);
vi.spyOn(tx.measurement, "upsert").mockImplementation(async (args) => {
vi.spyOn(tx.measurement, "upsert").mockImplementation((args) => {
stableWriteQueries += 1;
return originalUpsert(args);
});
vi.spyOn(tx.measurement, "createMany").mockImplementation(
async (args) => {
(args) => {
stableWriteQueries += 1;
return originalCreateMany(args);
},
@@ -100,7 +100,8 @@ describe("POST /api/admin/backups/[id]/restore", () => {
unit: "kg",
measuredAt: "2026-05-08T07:00:00.000Z",
source: "MANUAL",
notes: null,
notes: "private legacy note",
notesEncrypted: null,
},
{
type: "PULSE",
@@ -221,6 +222,8 @@ describe("POST /api/admin/backups/[id]/restore", () => {
expect(measurements[0]!.type).toBe("WEIGHT");
expect(measurements[0]!.value).toBe(80.5);
expect(measurements[1]!.type).toBe("PULSE");
expect(measurements[0]!.notes).toBeNull();
expect(measurements[0]!.notesEncrypted).not.toBeNull();
const medications = await prisma.medication.findMany({
where: { userId: admin.id },
@@ -303,6 +306,8 @@ describe("POST /api/admin/backups/[id]/restore", () => {
});
expect(res.status).toBe(422);
const error = (await res.json()) as { error: string };
expect(error.error).toMatch(/instance-wide settings/i);
const denied = await prisma.auditLog.count({
where: {
action: "admin.backups.restore.denied",