Files
Crussell/backend
popertots 9111258461 fix: 2FA disable now requires the verification code (enforced mode)
The disable flow was broken in enforced (production) mode: the account page
posted {code:''} to /api/user/2fa/disable, but the backend mints + validates a
code when 2FA is enforced, so the empty code always failed with 400 and a user
could never disable 2FA through the UI. In unenforced (dev/mock) mode the
backend short-circuits and no code is needed — which is why the user only saw
the confirmation dialog and no code prompt.

Backend:
- New POST /api/user/2fa/disable/code (SendDisableCodeHandler): mints +
  delivers a fresh disable-flow code via the existing ensurePendingTwoFACode
  machinery (per-user 1-min mint cooldown, 429 when throttled, 5-attempt
  lockout preserved on the disable call itself). This is the disable-flow
  equivalent of /api/user/2fa/setup. Runs unconditionally (no dev
  short-circuit) so the step is exercisable in dev too. Route mounted in
  main.go beside the other 2FA routes.
- Tests: mints fresh code, reuses valid pending code (hash unchanged),
  mint-throttled 429 (after the pending code is dropped, as a lockout does),
  unauthorized 401, unenforced still mints.

Frontend (account page):
- The disable confirmation now branches on twoFactorRequired: enforced →
  POST /api/user/2fa/disable/code to mint, then a 6-digit code-entry input +
  'Confirm Disable' button that posts the code to /api/user/2fa/disable;
  unenforced (dev) → unchanged direct disable. Code entry mirrors the enable
  flow's input styling; mint-throttle 429 / wrong-code 400 / lockout 429 all
  surface as toasts with the entry kept open for retry.

Verification: go test -tags test,dev -count=1 -parallel 8 ./... (all 20
packages ok, 0 failures incl. 5 new 2FA tests), go build ./... and -tags dev,
go vet clean, svelte-check 0 errors 0 warnings, env-docs gate OK.
2026-08-22 00:34:50 +01:00
..