"use client"; import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, Loader2, Trash2 } from "lucide-react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { SettingsCardActions } from "@/components/settings/_card-actions"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { useTranslations } from "@/lib/i18n/context"; import { apiDelete } from "@/lib/api/api-fetch"; import { randomId } from "@/lib/random-id"; import { recentProofErrorMessage, useRecentProof, } from "@/components/settings/security-section/use-recent-proof"; /** * The literal an admin must type to arm the wipe. Mirrors the Backups * `RESTORE` gate — and matches the server's typed-token defence at * `src/app/api/admin/data/route.ts` (which 422s any other `confirm`). */ const WIPE_CONFIRM_TOKEN = "DELETE ALL"; export function DangerZoneSection() { const { t } = useTranslations(); const queryClient = useQueryClient(); const [wipeMsg, setWipeMsg] = useState(null); // Typed-confirmation gate. The destructive button stays disabled until // the admin types `DELETE ALL` verbatim, matching the Backups Restore // dialog (which forces typing `RESTORE`) so the FAR more destructive // global wipe is not the weaker gate. Reset whenever the dialog closes. const [open, setOpen] = useState(false); const [typed, setTyped] = useState(""); const matched = typed.trim() === WIPE_CONFIRM_TOKEN; // Wiping every account asks for a fresh proof unless this session signed // in or re-proved within five minutes. const recentProof = useRecentProof(); const wipeAllData = useMutation({ mutationFn: async () => { // Idempotency-Key prevents a double-submit from re-running the // destructive transaction, matching the Backups restore path. const idempotencyKey = `admin-data-wipe-${randomId()}`; // The server returns one count per table that actually held rows. It // used to return nine fixed names, which is how nobody noticed the // wipe had stopped covering the schema: the result line read the same // whether it had cleared nine tables or ninety. return recentProof.run(() => apiDelete<{ cleared: boolean; deletedRows: number; models: Record; }>( "/api/admin/data", { confirm: WIPE_CONFIRM_TOKEN }, { headers: { "Idempotency-Key": idempotencyKey } }, ), ); }, onSuccess: (data) => { queryClient.invalidateQueries(); setWipeMsg( t("admin.deletedResult", { rows: data.deletedRows, tables: Object.keys(data.models).length, }), ); }, onError: (err: Error) => { setWipeMsg(recentProofErrorMessage(err, err.message)); }, }); return ( // v1.18.10 — the card SURFACE is neutral like every other admin / // settings section (`SettingsCard`: bg-card + border-border): a red card // edge reads as alarming, not premium. The danger signal lives ONLY in // the destructive delete button + its typed-confirmation dialog, which // are alarm enough. The header keeps the `AlertTriangle` icon so the // section still flags its intent. {recentProof.dialog}

{t("admin.deleteAllDetail")}

{wipeMsg && (

{wipeMsg}

)} { setOpen(next); if (!next) setTyped(""); }} > {t("admin.deleteAllConfirm")} {t("admin.deleteAllConfirmDescription")}
setTyped(e.target.value)} autoComplete="off" spellCheck={false} placeholder={WIPE_CONFIRM_TOKEN} />
{t("common.cancel")} { if (!matched) { e.preventDefault(); return; } setOpen(false); setTyped(""); wipeAllData.mutate(); }} > {wipeAllData.isPending && ( )} {t("admin.finalDelete")}
); }