Make the reset-every-threshold delete ask first

DELETE /api/user/thresholds without ?metric= erased the account's whole
override set on an omitted query parameter alone: no confirmation, no dry
run, and no rate limit of its own while the PUT beside it had one.

The wide form now wants { "confirm": "RESET_THRESHOLDS" } in the body — the
same typed-confirmation shape /api/settings/account and /api/settings/data
already use — and refuses 422 without writing anything. The ?metric= form
is untouched: it resets one band the person can retype, and the targets
sheet fires it in a loop. Both forms now run the PUT's 30 / 5 min limit.

The settings card's "reset all" control already opened a confirm dialog;
it now sends that answer to the server too. Its token is compared against
the route's declaration in the tests, so the two ends cannot drift apart
into a permanent 422.
This commit is contained in:
Marc-André Bombeck committed 2026-08-23 16:55:59 +02:00
1 parent c4544556f7
commit fffdc83ca4
5 files changed
+282 -15

No files matched your search

@@ -108,10 +108,19 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
const resetMutation = useMutation({
mutationFn: async (metric: ThresholdMetric | null) => {
const url = metric
? `/api/user/thresholds?metric=${metric}`
: "/api/user/thresholds";
const res = await apiFetchRaw(url, { method: "DELETE" });
// `metric === null` is the reset-everything arm behind the confirm
// dialog at the foot of the card. The server asks for the same decision
// in the body, so the dialog's answer is what gets sent — the wide form
// is not reachable by dropping the query parameter.
const res = metric
? await apiFetchRaw(`/api/user/thresholds?metric=${metric}`, {
method: "DELETE",
})
: await apiFetchRaw("/api/user/thresholds", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ confirm: "RESET_THRESHOLDS" }),
});
if (!res.ok) throw new Error("reset failed");
},
onSuccess: () => {