feat: add backup verification and restore preview
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user