feat(admin): route every significant admin action through the shared confirm (BUNYIP-430) #430

Merged
longjacksonle merged 1 commit from feat/BUNYIP-430-shared-admin-confirm into main 2026-08-01 21:39:13 +02:00

What and why

Every significant, hard-to-reverse admin control now routes through the one shared confirmation dialog and names both the action and the specific user it affects, so an admin who clicked the wrong row is told who they are about to change before anything happens.

The shared component is the data-confirm attribute plus a single delegated submit handler in assets/js/app.js that prompts on submit and cancels the POST when the admin declines. It already existed and is CSP-safe. The pre-v0.9.0 controls used inline onsubmit="return confirm(...)", which the strict Content-Security-Policy (script-src 'self', no inline handlers) silently blocks: that is why production fired Grant/Revoke lifetime and Reset password on a single unconfirmed click. Current main already migrated to data-confirm, so v0.9.1 (the hotfix already cut) restores the confirmations; this PR adds the user's name and closes the coverage gaps, rather than a third round of per-button dialogs.

Changes

  • Extract the per-user Actions card into a pure user_actions_card(target, is_admin_target) so the confirmations are unit-testable. Grant lifetime, Revoke lifetime, Reset password, role change (BUNYIP-109), Suspend and Delete now name the target user by email.
  • Name the user in the Identity and security confirms too (force-verify email, clear 2FA).
  • Add the confirmations the sweep found missing: grant/revoke app entitlement (names the app), restore-from-backup (overwrites the current profile), delete-a-documentation-entry.
  • Tests: the three named actions plus role change confirm and name the user, and every state-changing form in the Actions card carries data-confirm (cancelling the shared dialog leaves state unchanged).

Admin-surface sweep (acceptance criterion 6)

Routed through the dialog by this PR: grant/revoke app entitlement, restore-from-backup, delete documentation entry.

Already confirmed (left as-is, now the one shared component): Grant/Revoke lifetime, Reset password, role change, Suspend, Delete user, force-verify email, clear 2FA, reactivate user, Stripe product/price archive, IP-ban lift, rate-limit reset, rate-limit-config revert, feedback delete, application delete, application-group delete.

Deliberately NOT confirmed, with reason:

  • Seed import / template load: idempotent and scoped to the reserved demo domain; they never touch real accounts.
  • Feedback triage (status change / archive / mark-spam / unmark-spam / respond / restore): reversible.
  • Application reorder (swap-order), field reorder, restricted-toggle: reversible toggles / ordering.
  • Config saves (edit email settings, tier settings, auto-ban settings, Stripe product/price/catalog create, edit user email, docs add/edit): form edits with their own inputs, not one-click state flips.

Acceptance criteria

  • A single reusable confirmation dialog component exists (the data-confirm attribute + the one app.js handler).
  • Grant lifetime, Revoke lifetime and Reset password all confirm before acting.
  • The BUNYIP-109 controls (role change, application-group delete) use the same shared component.
  • The dialog states the action and the target user by name (email).
  • Cancelling leaves state completely unchanged (the app.js handler cancels the submit; tests assert every form is gated).
  • The admin surface has been swept, the result recorded here and on the issue, and the gaps routed through the dialog.
  • Tests covering confirm and cancel for the three named actions (plus role change).

Verification

just check-container green: fmt + clippy -D warnings + full suite (171 web tests incl. 5 new BUNYIP-430 tests, 312 domain). The confirmations are server-rendered markup pinned by exact-string tests on the real user_actions_card; the dialog itself is the native window.confirm the app.js handler already drives for the other data-confirm forms.

🤖 Generated with Claude Code

https://claude.ai/code/session_018TXaT3P192nDsZzbzHETb9

## What and why Every significant, hard-to-reverse admin control now routes through the one shared confirmation dialog and names both the action and the specific user it affects, so an admin who clicked the wrong row is told who they are about to change before anything happens. The shared component is the `data-confirm` attribute plus a single delegated submit handler in `assets/js/app.js` that prompts on submit and cancels the POST when the admin declines. It already existed and is CSP-safe. The pre-v0.9.0 controls used inline `onsubmit="return confirm(...)"`, which the strict Content-Security-Policy (`script-src 'self'`, no inline handlers) silently blocks: that is why production fired Grant/Revoke lifetime and Reset password on a single unconfirmed click. Current main already migrated to `data-confirm`, so v0.9.1 (the hotfix already cut) restores the confirmations; this PR adds the user's name and closes the coverage gaps, rather than a third round of per-button dialogs. ## Changes - Extract the per-user Actions card into a pure `user_actions_card(target, is_admin_target)` so the confirmations are unit-testable. Grant lifetime, Revoke lifetime, Reset password, role change (BUNYIP-109), Suspend and Delete now name the target user by email. - Name the user in the Identity and security confirms too (force-verify email, clear 2FA). - Add the confirmations the sweep found missing: grant/revoke app entitlement (names the app), restore-from-backup (overwrites the current profile), delete-a-documentation-entry. - Tests: the three named actions plus role change confirm and name the user, and every state-changing form in the Actions card carries `data-confirm` (cancelling the shared dialog leaves state unchanged). ## Admin-surface sweep (acceptance criterion 6) Routed through the dialog by this PR: grant/revoke app entitlement, restore-from-backup, delete documentation entry. Already confirmed (left as-is, now the one shared component): Grant/Revoke lifetime, Reset password, role change, Suspend, Delete user, force-verify email, clear 2FA, reactivate user, Stripe product/price archive, IP-ban lift, rate-limit reset, rate-limit-config revert, feedback delete, application delete, application-group delete. Deliberately NOT confirmed, with reason: - Seed import / template load: idempotent and scoped to the reserved demo domain; they never touch real accounts. - Feedback triage (status change / archive / mark-spam / unmark-spam / respond / restore): reversible. - Application reorder (swap-order), field reorder, restricted-toggle: reversible toggles / ordering. - Config saves (edit email settings, tier settings, auto-ban settings, Stripe product/price/catalog create, edit user email, docs add/edit): form edits with their own inputs, not one-click state flips. ## Acceptance criteria - [x] A single reusable confirmation dialog component exists (the `data-confirm` attribute + the one app.js handler). - [x] Grant lifetime, Revoke lifetime and Reset password all confirm before acting. - [x] The BUNYIP-109 controls (role change, application-group delete) use the same shared component. - [x] The dialog states the action and the target user by name (email). - [x] Cancelling leaves state completely unchanged (the app.js handler cancels the submit; tests assert every form is gated). - [x] The admin surface has been swept, the result recorded here and on the issue, and the gaps routed through the dialog. - [x] Tests covering confirm and cancel for the three named actions (plus role change). ## Verification `just check-container` green: fmt + clippy `-D warnings` + full suite (171 web tests incl. 5 new BUNYIP-430 tests, 312 domain). The confirmations are server-rendered markup pinned by exact-string tests on the real `user_actions_card`; the dialog itself is the native `window.confirm` the app.js handler already drives for the other `data-confirm` forms. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018TXaT3P192nDsZzbzHETb9
feat(admin): route every significant admin action through the shared confirm (BUNYIP-430)
All checks were successful
E2E PR gate / Install + reachability (no deployment secrets) (pull_request) Successful in 16s
Check / fmt + clippy + build + tests (pull_request) Successful in 7m46s
Create release / Create release from merged PR (pull_request) Has been skipped
3625651dba
Every significant, hard-to-reverse admin control now routes through the one shared confirmation dialog (the `data-confirm` attribute, handled once in `assets/js/app.js`, which prompts on submit and cancels the POST when the admin declines) and names both the action and the specific user it affects, so an admin who opened the wrong row is told who they are about to change before anything happens.

The shared component already existed and is CSP-safe (an external delegated submit handler, unlike the pre-v0.9.0 inline `onsubmit="return confirm(...)"` that the strict Content-Security-Policy silently blocked, which is why production fired these actions on a single unconfirmed click). This change makes the prompts name the user and closes the coverage gaps a sweep turned up, rather than adding another round of per-button dialogs.

Changes:
- Extract the per-user Actions card into a pure `user_actions_card(target, is_admin_target)` so the confirmations are unit-testable, and make Grant lifetime, Revoke lifetime, Reset password, role change (BUNYIP-109), Suspend and Delete name the target user by email.
- Name the user in the Identity and security confirms too (force-verify email, clear 2FA).
- Add the missing confirmations found by the sweep: grant/revoke app entitlement (names the app), restore-from-backup (overwrites the current profile), and delete-a-documentation-entry.
- Tests: assert the three named actions plus role change confirm and name the user, and that every state-changing form in the Actions card carries `data-confirm` (so cancelling the shared dialog leaves state unchanged).

Sweep result: the seed import / template-load controls were deliberately left unconfirmed because they are idempotent and scoped to the reserved demo domain (they never touch real accounts); feedback triage (status / archive / mark-spam / respond / restore) and list reordering are reversible; config saves (email, tier settings, auto-ban, Stripe create/catalog) are form edits, not one-click state flips. The Stripe archive, IP-ban lift, rate-limit reset, feedback delete, application delete and application-group delete controls already carried `data-confirm` and were left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018TXaT3P192nDsZzbzHETb9
longjacksonle deleted branch feat/BUNYIP-430-shared-admin-confirm 2026-08-01 21:39:13 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!430
No description provided.