Skip to content

feat(teams): admin toggle to restrict inviting to admins#5032

Merged
whutchinson98 merged 4 commits into
mainfrom
claude/july-7th-performance-r2j30u
Jul 20, 2026
Merged

feat(teams): admin toggle to restrict inviting to admins#5032
whutchinson98 merged 4 commits into
mainfrom
claude/july-7th-performance-r2j30u

Conversation

@jbecke

@jbecke jbecke commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #4994, implementing the resolution of the invite-permission review thread: @whutchinson98 flagged that member-level invites are asymmetric with admin-only revocation; @jbecke's call was to keep member invites as the default but give admins an off switch. #4994 merged before this landed, so it ships here as its own PR.

What changed

Backend

  • New team.allow_non_admin_invites column, BOOLEAN NOT NULL DEFAULT TRUE (migration 20260720145727).
  • invite_users_to_team now checks the setting after the member-role extractor: when the team has turned it off, a non-admin caller gets 403 NonAdminInvitesDisabled ("only team admins may invite users to this team"). Admins/owners are unaffected; teams that never touch the setting behave exactly as on main.
  • New admin-only endpoint POST /team/non-admin-invites/toggle, mirroring the auto-join-domain toggle (same extractor pattern, returns the authoritative new value). Registered in the authentication_service OpenAPI spec.

Web

  • Team settings gains a "Members can invite" toggle (admin/owner only), right under "Auto-join on domain".
  • The Invite button in the Members section hides for plain members when the team has restricted inviting (backend still enforces via 403 regardless).
  • The generated service-auth client was extended by hand to match the new spec (Team.allow_non_admin_invites, ToggleNonAdminInvitesResponse) since codegen can't run in this sandbox — the next orval regen should produce identical output.

Review nits from #4994

Tests

  • Service: non-admin blocked when setting off (no persistence side effects), admin allowed when setting off, toggle delegates to the repository.
  • Repository (live Postgres): defaults to true, toggles off/on, TeamDoesNotExist for missing teams from both getter and toggle.
  • cargo test -p teams: 172 passed, 0 failed against local Postgres 16 with all migrations. Clippy clean; authentication_service compiles; .sqlx cache regenerated via workspace prepare (minimal diff: 3 reworked team queries + 2 new ones).
  • Web typecheck rides CI (bun install can't fetch private git deps in this sandbox).

Generated by Claude Code

Follow-up to #4994 per review discussion: members can invite by default,
but team admins can now turn 'allow non-admin invites' off so only
admins/owners may send invites.

- New team.allow_non_admin_invites column (default TRUE) + migration
- invite_users_to_team returns 403 NonAdminInvitesDisabled for non-admin
  callers when the setting is off
- New admin-only POST /team/non-admin-invites/toggle endpoint (mirrors
  the auto-join toggle), registered in swagger
- Web: 'Members can invite' toggle in team settings; invite button hidden
  for members when restricted; generated client extended to match the
  new spec (orval regen will produce the same output)
- Replace em-dashes with plain hyphens in PR-added comments/messages
- Tests: service gating (member blocked / admin allowed / toggle
  delegation), repo toggle + default + missing-team, mock updates
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c47e092c-ce95-4e2a-9af7-19a690c3ce1f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a persistent allow_non_admin_invites team setting that defaults to enabled. Non-admin invitations are rejected when disabled, while admins and owners remain authorized. Adds an admin-only toggle endpoint and OpenAPI registration, updates the authentication client and query cache, and exposes the setting in team settings. The Members section now conditionally displays the Invite action based on the setting and caller role.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows conventional commits format and is under the 72-character limit.
Description check ✅ Passed The description matches the implemented invite restriction setting, backend endpoint, web toggle, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/teams/src/inbound/axum_router/invite_to_team.rs (1)

57-89: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Eliminate duplicated error response mapping.

InviteUsersToTeamError already implements IntoResponse (as seen in crates/teams/src/inbound/axum_router/mod.rs), so we can avoid duplicating its variant mappings here by delegating directly to it. This prevents the need to make parallel updates across multiple files when adding new domain errors like NonAdminInvitesDisabled.

♻️ Proposed refactor to delegate the response conversion
-            InviteToTeamError::InviteUsersToTeamError(e) => match e {
-                InviteUsersToTeamError::TooManyEmails => (
-                    StatusCode::BAD_REQUEST,
-                    Json(ErrorResponse {
-                        message: "too many emails".into(),
-                    }),
-                ),
-                InviteUsersToTeamError::NonAdminInvitesDisabled => (
-                    StatusCode::FORBIDDEN,
-                    Json(ErrorResponse {
-                        message: "only team admins may invite users to this team".into(),
-                    }),
-                ),
-                InviteUsersToTeamError::NotEnoughOpenSeats => (
-                    StatusCode::BAD_REQUEST,
-                    Json(ErrorResponse {
-                        message: "free team member limit reached; upgrade to invite more members"
-                            .into(),
-                    }),
-                ),
-                InviteUsersToTeamError::CustomerError(_) => (
-                    StatusCode::INTERNAL_SERVER_ERROR,
-                    Json(ErrorResponse {
-                        message: "internal server error".into(),
-                    }),
-                ),
-                _ => (
-                    StatusCode::INTERNAL_SERVER_ERROR,
-                    Json(ErrorResponse {
-                        message: "unable to invite users to team".into(),
-                    }),
-                ),
-            },
+            InviteToTeamError::InviteUsersToTeamError(e) => return e.into_response(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/teams/src/inbound/axum_router/invite_to_team.rs` around lines 57 - 89,
Replace the nested InviteUsersToTeamError variant-to-response match in the
InviteToTeamError::InviteUsersToTeamError arm with direct delegation to
InviteUsersToTeamError’s existing IntoResponse implementation. Preserve the
outer InviteToTeamError handling and allow all current and future domain error
mappings to be handled centrally.
🧹 Nitpick comments (1)
crates/teams/src/outbound/team_repo.rs (1)

1489-1529: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use query_scalar! for these bool queries
Both statements return a single bool, so query_scalar! can drop the manual row mapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/teams/src/outbound/team_repo.rs` around lines 1489 - 1529, Update
get_team_allow_non_admin_invites and toggle_allow_non_admin_invites to use
sqlx::query_scalar! for their single-boolean SELECT and UPDATE RETURNING
queries. Fetch the optional scalar bool directly, preserve the existing
TeamDoesNotExist error when no value is returned, and remove the manual
row-field mapping.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/teams/src/inbound/axum_router/invite_to_team.rs`:
- Around line 57-89: Replace the nested InviteUsersToTeamError
variant-to-response match in the InviteToTeamError::InviteUsersToTeamError arm
with direct delegation to InviteUsersToTeamError’s existing IntoResponse
implementation. Preserve the outer InviteToTeamError handling and allow all
current and future domain error mappings to be handled centrally.

---

Nitpick comments:
In `@crates/teams/src/outbound/team_repo.rs`:
- Around line 1489-1529: Update get_team_allow_non_admin_invites and
toggle_allow_non_admin_invites to use sqlx::query_scalar! for their
single-boolean SELECT and UPDATE RETURNING queries. Fetch the optional scalar
bool directly, preserve the existing TeamDoesNotExist error when no value is
returned, and remove the manual row-field mapping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e6e70dd8-1cd1-44b3-9828-026fa67868e0

📥 Commits

Reviewing files that changed from the base of the PR and between 22b24ca and 3466b09.

⛔ Files ignored due to path filters (8)
  • .sqlx/query-05f7a9a439044165e4523a2b25261b11f805ba2dcf3f0b0a03422502ee73f304.json is excluded by !**/.sqlx/**
  • .sqlx/query-210d2b138dfaed6974dad7ee5cdbf00b90b65abddda29eab93d816e453371bf4.json is excluded by !**/.sqlx/**
  • .sqlx/query-5d5cf55d5b9dc0b138684b01620b30dc5908a951020368af47cbbe6ca1052e79.json is excluded by !**/.sqlx/**
  • .sqlx/query-8b1e8ff98b7b6cfc0d18fc32bcba5f41392f2f716d7c3ba7c7d50044981a322e.json is excluded by !**/.sqlx/**
  • .sqlx/query-edb000afe8b8359996a3623dd495bcc6b3b39fe98fb76bcbccb176ba9e06aaef.json is excluded by !**/.sqlx/**
  • apps/web/src/lib/service-clients/service-auth/generated/schemas/index.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-auth/generated/schemas/team.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
  • apps/web/src/lib/service-clients/service-auth/generated/schemas/toggleNonAdminInvitesResponse.ts is excluded by !**/generated/**, !apps/web/src/lib/service-clients/**/generated/**
📒 Files selected for processing (16)
  • apps/web/src/features/settings/Team.tsx
  • apps/web/src/lib/queries/team/teams.ts
  • apps/web/src/lib/service-clients/service-auth/client.ts
  • crates/macro_db_client/migrations/20260720145727_add_team_allow_non_admin_invites.sql
  • crates/teams/src/domain/model.rs
  • crates/teams/src/domain/team_repo.rs
  • crates/teams/src/domain/team_service.rs
  • crates/teams/src/domain/team_service/test.rs
  • crates/teams/src/inbound/axum_router/create_team.rs
  • crates/teams/src/inbound/axum_router/invite_to_team.rs
  • crates/teams/src/inbound/axum_router/mod.rs
  • crates/teams/src/inbound/axum_router/test.rs
  • crates/teams/src/inbound/axum_router/toggle_non_admin_invites.rs
  • crates/teams/src/outbound/team_repo.rs
  • crates/teams/src/outbound/team_repo/test.rs
  • services/authentication_service/src/api/swagger.rs

claude added 3 commits July 20, 2026 16:41
- Regenerate service-auth openapi.json + generated client.ts for the new
  /team/non-admin-invites/toggle endpoint and Team.allow_non_admin_invites
  field (CI gen-api check)
- Delegate InviteToTeamError's domain-error arm to InviteUsersToTeamError's
  IntoResponse impl instead of duplicating the variant mappings
- Use query_scalar! for the single-bool allow_non_admin_invites queries
The Team type's new required field broke the create-team optimistic
update literal; new teams default to allowing member invites.
Comment thread crates/teams/src/outbound/team_repo.rs

@dev-rb dev-rb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frontend lgtm

@whutchinson98
whutchinson98 merged commit af70306 into main Jul 20, 2026
27 checks passed
@whutchinson98
whutchinson98 deleted the claude/july-7th-performance-r2j30u branch July 20, 2026 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants