fix(admin): harden the Danger-Zone wipe to match the Backups Restore gate

The global data wipe auto-supplied confirm:'DELETE ALL' from a single-click
dialog, leaving the server's typed-token defence dead and making the most
destructive action a weaker gate than the less-destructive restore. Mirror
RestoreRowDialog: require typing 'DELETE ALL' into an Input that arms the
destructive button, reset on close.

Also attach an Idempotency-Key header to the wipe so a double-submit cannot
re-run the transaction, add the documented post-requireAdmin()
'admin-data-delete:<userId>' rate-limit bucket alongside the anonymous IP
throttle, and switch BackupsSection off the non-reactive formatDateTime
onto the locale-reactive useFormatters() hook its siblings use.
This commit is contained in:
Marc-André Bombeck committed 2026-06-16 23:05:11 +02:00
1 parent e23f57448a
commit 88c4a8428f
9 files changed
+81 -7

No files matched your search

+16
View File
@@ -26,6 +26,22 @@ export const DELETE = apiHandler(async (request: NextRequest) => {
const { user } = await requireAdmin();
annotate({ action: { name: "admin.data.delete" } });
// v1.18.1 — the documented convention buckets authenticated admin
// mutations on `userId`, not the (pre-auth, spoofable) client IP. The
// IP bucket above is the anonymous first-line throttle; this is the
// canonical per-admin bucket the rate-limit contract specifies.
const userRl = await checkRateLimit(
`admin-data-delete:${user.id}`,
5,
60 * 1000,
);
if (!userRl.allowed) {
return NextResponse.json(
{ data: null, error: "Rate limit exceeded" },
{ status: 429, headers: rateLimitHeaders(userRl) },
);
}
let confirm = "";
try {
const raw = await request.text();