feat(admin): add revoke-lifetime-membership endpoint + UI #63

Merged
YousifShkara merged 1 commit from feat/admin-user-management into main 2026-06-04 04:50:51 +02:00
Owner

Grant existed (grant_lifetime_membership) but had no inverse; once an
admin granted lifetime there was no way to undo it without hand-editing
the database. Wire the revoke path end-to-end.

bunyip-api:

  • UserRepository::revoke_lifetime_membership flips the user back to
    subscription_tier='standard', lifetime_member=FALSE,
    subscription_status='none', subscription_override_by=NULL,
    trial_ends_at=NULL. Matches post-Stripe-cancel state. Guarded by
    WHERE deleted_at IS NULL AND lifetime_member = TRUE so revoking a
    non-lifetime user 404s instead of silently mutating state.
  • handlers::admin::revoke_lifetime_membership calls the repo + writes
    an AuditAction::AdminMembershipRevoked entry (variant already
    existed). Skips Stripe cleanup: the /bin/bash subscription the grant
    created keeps generating /bin/bash invoices, harmless, can be addressed
    separately.
  • POST /v1/admin/users/{user_id}/lifetime/revoke

bunyip-web:

  • api::admin::revoke_lifetime client wrapper.
  • handlers::admin::user_revoke_lifetime POST handler, admin_guard +
    redirect_cookies back to the detail page.
  • POST /admin/users/:id/lifetime/revoke route.
  • /admin/users list row and the user-detail Actions card now show a
    Revoke Lifetime button when lifetime_member is true, and the Grant
    Lifetime button when it is false. Mutually exclusive so the UI never
    offers both at once.
Grant existed (grant_lifetime_membership) but had no inverse; once an admin granted lifetime there was no way to undo it without hand-editing the database. Wire the revoke path end-to-end. bunyip-api: - UserRepository::revoke_lifetime_membership flips the user back to subscription_tier='standard', lifetime_member=FALSE, subscription_status='none', subscription_override_by=NULL, trial_ends_at=NULL. Matches post-Stripe-cancel state. Guarded by WHERE deleted_at IS NULL AND lifetime_member = TRUE so revoking a non-lifetime user 404s instead of silently mutating state. - handlers::admin::revoke_lifetime_membership calls the repo + writes an AuditAction::AdminMembershipRevoked entry (variant already existed). Skips Stripe cleanup: the /bin/bash subscription the grant created keeps generating /bin/bash invoices, harmless, can be addressed separately. - POST /v1/admin/users/{user_id}/lifetime/revoke bunyip-web: - api::admin::revoke_lifetime client wrapper. - handlers::admin::user_revoke_lifetime POST handler, admin_guard + redirect_cookies back to the detail page. - POST /admin/users/:id/lifetime/revoke route. - /admin/users list row and the user-detail Actions card now show a Revoke Lifetime button when lifetime_member is true, and the Grant Lifetime button when it is false. Mutually exclusive so the UI never offers both at once.
feat(admin): add revoke-lifetime-membership endpoint + UI
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt / clippy / build / test (pull_request) Successful in 1m2s
e20979b1bb
Grant existed (grant_lifetime_membership) but had no inverse; once an
admin granted lifetime there was no way to undo it without hand-editing
the database. Wire the revoke path end-to-end.

bunyip-api:
- UserRepository::revoke_lifetime_membership flips the user back to
  subscription_tier='standard', lifetime_member=FALSE,
  subscription_status='none', subscription_override_by=NULL,
  trial_ends_at=NULL. Matches post-Stripe-cancel state. Guarded by
  WHERE deleted_at IS NULL AND lifetime_member = TRUE so revoking a
  non-lifetime user 404s instead of silently mutating state.
- handlers::admin::revoke_lifetime_membership calls the repo + writes
  an AuditAction::AdminMembershipRevoked entry (variant already
  existed). Skips Stripe cleanup: the /bin/bash subscription the grant
  created keeps generating /bin/bash invoices, harmless, can be addressed
  separately.
- POST /v1/admin/users/{user_id}/lifetime/revoke

bunyip-web:
- api::admin::revoke_lifetime client wrapper.
- handlers::admin::user_revoke_lifetime POST handler, admin_guard +
  redirect_cookies back to the detail page.
- POST /admin/users/:id/lifetime/revoke route.
- /admin/users list row and the user-detail Actions card now show a
  Revoke Lifetime button when lifetime_member is true, and the Grant
  Lifetime button when it is false. Mutually exclusive so the UI never
  offers both at once.
YousifShkara deleted branch feat/admin-user-management 2026-06-04 04:50:51 +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!63
No description provided.