feat: add backup verification and restore preview

This commit is contained in:
AI Bot
2026-06-06 19:08:24 +08:00
parent 643da5b738
commit 1edfa6ecd5
3 changed files with 403 additions and 4 deletions

View File

@@ -1,14 +1,28 @@
import { NextRequest } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import { buildRequestAuditMeta } from "@/lib/boss-audit";
import { requireRequestSession } from "@/lib/boss-auth";
import { requireCsrfSafeMutation } from "@/lib/boss-csrf";
import { appendPermissionAuditLog, type PermissionAuditLog } from "@/lib/boss-data";
import {
createBossStateBackup,
getBossStateBackupStatus,
listBossStateBackups,
listBossStateBackupProjections,
previewBossStateRestore,
projectBossStateBackupSnapshot,
restoreBossStateBackup,
verifyBossStateBackup,
} from "@/lib/boss-state-backups";
type BackupAuditAction = Extract<
PermissionAuditLog["action"],
| "backup.snapshot_created"
| "backup.snapshot_verified"
| "backup.restore_previewed"
| "backup.restore_dry_run"
| "backup.snapshot_restored"
>;
function forbidden() {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
@@ -17,6 +31,21 @@ function stringValue(value: unknown) {
return typeof value === "string" ? value.trim() : "";
}
async function appendBackupAuditLog(request: NextRequest, input: {
actorAccount: string;
action: BackupAuditAction;
snapshotId: string;
detail?: string;
}) {
await appendPermissionAuditLog({
actorAccount: input.actorAccount,
action: input.action,
detail: input.detail ?? `snapshot:${input.snapshotId}`,
afterJson: { snapshotId: input.snapshotId },
...buildRequestAuditMeta(request),
});
}
async function requireHighestAdmin(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
@@ -33,7 +62,7 @@ export async function GET(request: NextRequest) {
if (auth.response) return auth.response;
const status = await getBossStateBackupStatus();
const snapshots = await listBossStateBackups(50);
const snapshots = await listBossStateBackupProjections(50);
return jsonNoStore({ ok: true, status, snapshots });
}
@@ -53,8 +82,44 @@ export async function POST(request: NextRequest) {
actorAccount: auth.session.account,
reason: stringValue(body.reason) || "manual",
});
const projectedSnapshot = await projectBossStateBackupSnapshot(snapshot);
await appendBackupAuditLog(request, {
actorAccount: auth.session.account,
action: "backup.snapshot_created",
snapshotId: snapshot.snapshotId,
});
const status = await getBossStateBackupStatus();
return jsonNoStore({ ok: true, action, snapshot, status });
return jsonNoStore({ ok: true, action, snapshot: projectedSnapshot, status });
}
if (action === "verify_snapshot") {
const snapshotId = stringValue(body.snapshotId);
if (!snapshotId) {
return jsonNoStore({ ok: false, message: "BACKUP_SNAPSHOT_ID_REQUIRED" }, { status: 400 });
}
const verification = await verifyBossStateBackup(snapshotId);
await appendBackupAuditLog(request, {
actorAccount: auth.session.account,
action: "backup.snapshot_verified",
snapshotId,
detail: `snapshot:${snapshotId}; ok:${verification.ok}`,
});
return jsonNoStore({ ok: true, action, verification });
}
if (action === "preview_restore" || action === "dry_run_restore") {
const snapshotId = stringValue(body.snapshotId);
if (!snapshotId) {
return jsonNoStore({ ok: false, message: "BACKUP_SNAPSHOT_ID_REQUIRED" }, { status: 400 });
}
const preview = await previewBossStateRestore({ snapshotId, willWriteState: false });
await appendBackupAuditLog(request, {
actorAccount: auth.session.account,
action: action === "preview_restore" ? "backup.restore_previewed" : "backup.restore_dry_run",
snapshotId,
detail: `snapshot:${snapshotId}; projects:${preview.impact.projects.current}->${preview.impact.projects.after}`,
});
return jsonNoStore({ ok: true, action, preview });
}
if (action === "restore_snapshot") {
@@ -66,6 +131,11 @@ export async function POST(request: NextRequest) {
snapshotId,
actorAccount: auth.session.account,
});
await appendBackupAuditLog(request, {
actorAccount: auth.session.account,
action: "backup.snapshot_restored",
snapshotId,
});
const status = await getBossStateBackupStatus();
return jsonNoStore({ ok: true, action, ...result, status });
}