Skip to content

feat(team): self-serve member admin UI + admins can change roles#3264

Merged
bokelley merged 3 commits into
mainfrom
bokelley/winnipeg-v11
Apr 26, 2026
Merged

feat(team): self-serve member admin UI + admins can change roles#3264
bokelley merged 3 commits into
mainfrom
bokelley/winnipeg-v11

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Closes the self-service gap left after #3235:

  • Org owner can now do the full add / promote / invite flow from the team page (server/public/team.html) — no more escalations for "promote so-and-so to admin."
  • Org admins can change member ↔ admin roles (previously owner-only). Admins still can't assign owner or change an existing owner's role.
  • Auto-provision toggle is now visible in the Verified Domains card (owner-only, since flipping it on widens org membership).

Changes

Backend (server/src/routes/organizations.ts)

  • POST /:orgId/members/by-email accepts optional seat_type and persists it via invitation_seat_types for both Path 1 (invite) and Path 2 (direct add). Path 2 clears stale (org, email) staging rows before writing.
  • Path 3 (role update) blocks self-role-change to mirror the existing PATCH endpoint — without this, an owner calling Path 3 with their own email could demote themselves to member, leaving the org owner-less.
  • PATCH /:orgId/members/:membershipId and Path 3 of /members/by-email now allow admins to manage member ↔ admin. Admins capped: no owner assignment, no changing an existing owner's role.
  • PATCH /:orgId/settings restricts auto_provision_verified_domain to owner-only (and AAO super-admin). With admins able to promote member → admin, an admin shouldn't also be able to widen membership unilaterally — that's a single-actor escalation chain.
  • GET /:orgId/domains returns auto_provision_verified_domain so the toggle UI renders state in one round trip.

UI (server/public/team.html)

  • "+ Invite Member" → "+ Add member"; modal calls /members/by-email instead of /invitations (the unified endpoint subsumes the old one).
  • Auto-provision toggle in the Verified Domains card. Visible only to owners and only when at least one verified domain exists.

Tests

  • server/tests/integration/member-by-email-policy.test.ts — 16 tests covering role-cap branches (admin promote member→admin, admin denied owner-assign, admin denied owner-target, admin demotes another admin, owner unrestricted, member denied with specific message), self-role-change blocking on both endpoints, seat_type propagation including unknown-value rejection, and the owner-only auto-provision toggle.
  • Existing membership-webhook.test.ts still passes (43/43 across both files).

Reviews addressed

Ran code-reviewer and security-reviewer on the draft. Findings closed:

Test plan

  • npm run typecheck clean
  • 43/43 integration tests pass against real Postgres
  • Browser-tested: source-level UI assertions all pass (interactive click test blocked by a dev-only marketing overlay; not a real-mode issue)
  • CI green

🤖 Generated with Claude Code

Closes the self-service gap left after #3235. The team page now does the
full add/promote/invite flow without anyone filing an escalation, and
org admins can manage member ↔ admin roles directly.

Backend:
- POST /api/organizations/:orgId/members/by-email accepts optional
  seat_type and persists it via invitation_seat_types for Path 1 (invite)
  and Path 2 (direct add). Path 2 clears any stale (org, email) staging
  row before writing, so a prior failed attempt can't leak seat_type to
  a later invite. Path 3 (role update) blocks self-role-change to mirror
  the existing PATCH endpoint.
- PATCH /:orgId/members/:membershipId and /members/by-email Path 3 allow
  admins to change member ↔ admin. Admins cannot assign owner or change
  an existing owner's role; owners and AAO super-admins are unrestricted.
- PATCH /:orgId/settings restricts auto_provision_verified_domain to
  owners (and AAO super-admins) — turning it on widens org membership in
  a way admins shouldn't be able to do unilaterally given the new
  role-cap policy.
- GET /:orgId/domains returns auto_provision_verified_domain alongside
  the domain list so the toggle UI renders state in one round trip.

UI (server/public/team.html):
- Replaces "+ Invite Member" with "+ Add member"; the modal calls the
  unified by-email endpoint instead of /invitations.
- Adds an auto-provision toggle inside the Verified Domains card,
  visible only to owners and only when at least one domain is verified.

Tests: server/tests/integration/member-by-email-policy.test.ts (16 tests)
covers role-cap branches, self-role-change blocking, seat_type
propagation, and the owner-only toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread server/tests/integration/member-by-email-policy.test.ts Fixed
bokelley and others added 2 commits April 26, 2026 11:17
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The overlay intercepts clicks during automated UI tests and isn't
relevant for dev-mode users (whose preferences are always null). This
keeps it hidden when running against http://localhost (also 127.0.0.1
and ::1) so dev-mode and Playwright runs aren't blocked from
interacting with the page underneath.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit f3b5c7c into main Apr 26, 2026
12 checks passed
@bokelley bokelley deleted the bokelley/winnipeg-v11 branch April 26, 2026 16:01
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.

1 participant