Skip to content

feat(invites): staff act on a member's whole invite subtree (#639) - #927

Merged
obrien-k merged 1 commit into
mainfrom
feat/639-invite-subtree-actions
Oct 3, 2026
Merged

obrien-k merged 1 commit into
mainfrom
feat/639-invite-subtree-actions

Conversation

@obrien-k

@obrien-k obrien-k commented Oct 3, 2026

Copy link
Copy Markdown
Member

Closes #639. The grill outcome is on the issue. The record is ADR-0056.

The change

  • GET /users/{id}/invite-subtree/preview (invites_manage): every descendant of the member, the member excluded and ordered by depth, with count, disabled and withoutInvites.
  • POST /users/{id}/invite-subtree/action { action, reason, expectedCount }: note, disable or revoke_invites on every descendant, in one transaction.
    • It needs invites_manage plus the single-member permission: users_edit, users_disable or invites_edit. Without it, it answers 403 naming the permission, before anything is read.
    • It answers 409 if the subtree no longer has expectedCount members. Nothing is written.
    • Every descendant gets a UserModerationNote with the reason. A member the action changes is audited exactly as the single-member route audits them (user.disabled, user.can_invite_changed), plus subtreeRootId. A member already in that state gets the note only.
    • The root gets one user.invite_subtree_action row: { action, reason, count, userIds }, listing the ids changed.
    • No cap, no bulk undo, no messages to members.
  • modules/accountDisable.ts: the one staff disable write. POST /users/{id}/disable uses it too, so fix(governance): nothing sets User.banDate, so the ban standing and Contagion's infected source are unreachable #634's decision lands in one place. That route's responses are unchanged. It no longer reads before writing: a write matching no row is the 404, which closes the race Bookmarking a nonexistent artist/release/community/request answers 500, not 404 #564's P2025 catch guarded. Its three unit tests are updated in place.
  • modules/inviteSubtreeWalk.ts: the recursive walk, moved out of modules/user.ts. It now takes a transaction client, so the run walks the tree it writes.

Verification

  • 8 integration tests in inviteSubtreeActions.integration.ts, against a tree with one member already disabled and one already without invite privileges:
    • the preview, its counts and order, 403 and 404;
    • each action's writes, notes and audits, with the root and an outsider untouched;
    • a stale count 409s with nothing written;
    • a missing action permission 403s with nothing written.
  • Negative controls, each caught by 1 test:
    • removing the count check;
    • removing the action-permission check;
    • disabling members who were already disabled.
  • The existing invite tree files (inviteTree, inviteTreeEdge, profileInviteTree, inviteControls) pass: 44 tests.
  • The full unit suite passes (3,709 tests). Lint, tsc and typecheck:test are clean. openapi:completeness, openapi:gate-marks, openapi:failure-coverage, prisma:guard-coverage, changelog:check and env:coverage pass.
  • Lizard, checked before pushing:
    • The two flagged files shrink: modules/user.ts goes from 574 to 560 lines and routes/api/user.ts from 998 to 996. The new router is mounted on the users router rather than in createApp, which is flagged at 121.
    • Every new function is within the limits.
    • lib/openapi.ts grows, as any contract change does.
  • Agentlinter: the changed AGENTS.md lines carry no acronyms, and the ADR has no absolute wording.

A stellar-ui follow-up for the staff tool is filed separately.

🤖 Generated with Claude Code

Staff preview every descendant of a member (the member excluded), then
apply one action to all of them: a note, a disable, or revoking invite
privileges. The apply names the previewed count and 409s if the tree has
changed. One transaction, no cap, no bulk undo, no messages. Each action
needs invites_manage plus its single-member permission (ADR-0056).

POST /users/:id/disable and a subtree run share one disable write,
modules/accountDisable.ts, so #634 lands in one place. The subtree walk
moves to modules/inviteSubtreeWalk.ts and takes a transaction client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@obrien-k obrien-k added this to the v0.11.0 milestone Oct 3, 2026
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 56 complexity

Metric Results
Complexity 56

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@obrien-k
obrien-k merged commit 0de4d5b into main Oct 3, 2026
6 checks passed
@obrien-k
obrien-k deleted the feat/639-invite-subtree-actions branch October 3, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(invites): staff actions on a whole invite subtree — note, disable, or revoke invite privileges for every descendant

1 participant