From d634c3d6bdd58b0cd802630b65830654031cb703 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 26 Apr 2026 10:53:48 -0400 Subject: [PATCH 1/3] feat(team): self-serve member admin UI + admins can change roles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .changeset/team-self-serve-member-admin.md | 17 + server/public/team.html | 95 +++- server/src/routes/organizations.ts | 180 ++++++- .../member-by-email-policy.test.ts | 460 ++++++++++++++++++ 4 files changed, 720 insertions(+), 32 deletions(-) create mode 100644 .changeset/team-self-serve-member-admin.md create mode 100644 server/tests/integration/member-by-email-policy.test.ts diff --git a/.changeset/team-self-serve-member-admin.md b/.changeset/team-self-serve-member-admin.md new file mode 100644 index 0000000000..4e394df093 --- /dev/null +++ b/.changeset/team-self-serve-member-admin.md @@ -0,0 +1,17 @@ +--- +--- + +feat(team): self-serve member admin in team.html, admins can change roles + +Closes the self-service gap left after #3235 — an org owner can now do everything from the team UI without filing an escalation. + +## What changed + +- **Team UI consolidates "invite" and "promote" into one "Add member" flow** (`server/public/team.html`). The modal posts to the unified `POST /api/organizations/:orgId/members/by-email` endpoint that walks the four-state machine (invite / create / update / no-op). The same modal handles adding a new teammate and promoting an existing member. +- **Auto-provision toggle in the Verified Domains card** — owners can flip `auto_provision_verified_domain` per org via the existing `PATCH /api/organizations/:orgId/settings`. Hidden when no verified domain exists. Owner-only (admins shouldn't be able to widen org membership unilaterally, especially now that admins can promote auto-joined members to admin). +- **Admins can change member roles** — `PATCH /api/organizations/:orgId/members/:membershipId` and Path 3 of `/members/by-email` now allow org admins to promote `member ↔ admin`. Caps in place: admins can't assign `owner`, can't change a current owner's role, and (matching the existing PATCH endpoint) no caller of either endpoint can change their own role. Owners and AAO super-admins are unrestricted. +- **`/members/by-email` accepts `seat_type`** — the unified endpoint is now a true superset of `/invitations`. seat_type is staged via `invitation_seat_types` for both Path 1 (invite) and Path 2 (direct add) so the `organization_membership.created` webhook hands the right seat_type to the local cache. Path 2 clears any stale staging rows for the same `(org, email)` pair before staging, so a prior failed direct-add can't pollute a later invite. + +## Tests + +- New `server/tests/integration/member-by-email-policy.test.ts` (16 tests) covers all role-cap branches, self-role-change blocking on both endpoints, seat_type propagation, and the owner-only auto-provision toggle. diff --git a/server/public/team.html b/server/public/team.html index 4698ed3276..2ee1d9de1f 100644 --- a/server/public/team.html +++ b/server/public/team.html @@ -270,16 +270,16 @@ - +