feat: Invite a contributor as a member (pitch #3806, scope D)#3825
Merged
Anty0 merged 1 commit intoJul 25, 2026
Merged
Conversation
Contributor
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Anty0
force-pushed
the
jirikuchynka/community-contributors
branch
from
July 25, 2026 14:29
34b9f46 to
70a1fe0
Compare
Anty0
force-pushed
the
jirikuchynka/invite-contributor
branch
from
July 25, 2026 14:51
67f5f35 to
84c61e1
Compare
Anty0
force-pushed
the
jirikuchynka/community-contributors
branch
from
July 25, 2026 15:58
70a1fe0 to
37ab36d
Compare
Anty0
force-pushed
the
jirikuchynka/invite-contributor
branch
from
July 25, 2026 17:01
84c61e1 to
fc107fc
Compare
Anty0
force-pushed
the
jirikuchynka/invite-contributor
branch
from
July 25, 2026 17:33
fc107fc to
2956471
Compare
Anty0
merged commit Jul 25, 2026
39b8ada
into
jirikuchynka/community-contributors
105 of 110 checks passed
Anty0
added a commit
that referenced
this pull request
Jul 26, 2026
Part of pitch #3806 (Community translation v1.1 — Contributors), **scope D**. Targets the integration branch (PR #3819), not `main`. ## What Adds an **"Invite as member"** button to a Community-tab row on the project Members page. Clicking it invites that contributor to the project **by user id** — the server resolves their address, sends the invitation mail, and never returns it to the org. - New endpoint `PUT /v2/projects/{projectId}/invite-contributor` (`MEMBERS_EDIT`); the body carries `userId` + permission fields, **no email**. - **Email-free by construction.** The address is stored server-side (needed to send the mail and bind acceptance) but is masked at read time by `Invitation.visibleEmail` — the single home of the rule — which **every** assembler that serializes an invitee address reads (Team-tab pending list *and* the unauthenticated `invitation_info` endpoint). Pitch hard-constraint 1: no contributor email reaches the org before acceptance. - **Anti-oracle guard.** Only *visible contributors* of the project may be invited (reusing the Community tab's member / deleted / disabled exclusions, via a single shared `VISIBLE_CONTRIBUTOR` query). Every other id — nonexistent, deleted, disabled, member, org member, or a contributor of another project — returns one **byte-identical 404**, so the endpoint leaks neither account existence nor state. - **No permission-less membership.** `type = NONE` is rejected server-side (`CANNOT_INVITE_CONTRIBUTOR_WITH_NO_PERMISSION`), not merely hidden in the picker. - Scoped/advanced invites route through the EE service exactly like the email invite; `agencyId` stays null (a non-null agency would short-circuit acceptance binding). - Acceptance is bound to the invitee (`email = user.username`); a different user gets `INVITATION_EMAIL_MISMATCH`. ## Frontend - `members.edit`-gated **"Invite as member"** button on the contributor row, opening the existing `PermissionsModal` picker with `hideNone` (matching the email dialog). The permission→body mapping is factored into a shared helper used by both invite hooks. ## Tests - Backend (`InviteContributorControllerTest`, `InviteContributorEeTest`): email stored + hidden, model + public-endpoint email null, `MEMBERS_EDIT` gate, uniform-404 for every non-visible-contributor id, re-invite dedup, NONE rejection, EE scoped routing + granted languages, nameless-contributor path, acceptance binding + granted membership. - Frontend: vitest over the shared permission-body helper (all role/scope/language branches, incl. scope-based filtering); an E2E case that invites a contributor from the Community tab (typed `data-cy` selectors), asserts the request succeeds, that a pending invitation appears, and that the `NONE` role is absent from the picker. ## Notes - Accepted, documented limitations (see the plan's Risks): an already-invited contributor stays listed with a live button until they accept (a second click fails safely with a no-email error), and acceptance is email-bound rather than user-FK-bound (a full user-FK model is deferred to a follow-up across all invitation kinds).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of pitch #3806 (Community translation v1.1 — Contributors), scope D. Targets the integration branch (PR #3819), not
main.What
Adds an "Invite as member" button to a Community-tab row on the project Members page. Clicking it invites that contributor to the project by user id — the server resolves their address, sends the invitation mail, and never returns it to the org.
PUT /v2/projects/{projectId}/invite-contributor(MEMBERS_EDIT); the body carriesuserId+ permission fields, no email.Invitation.visibleEmail— the single home of the rule — which every assembler that serializes an invitee address reads (Team-tab pending list and the unauthenticatedinvitation_infoendpoint). Pitch hard-constraint 1: no contributor email reaches the org before acceptance.VISIBLE_CONTRIBUTORquery). Every other id — nonexistent, deleted, disabled, member, org member, or a contributor of another project — returns one byte-identical 404, so the endpoint leaks neither account existence nor state.type = NONEis rejected server-side (CANNOT_INVITE_CONTRIBUTOR_WITH_NO_PERMISSION), not merely hidden in the picker.agencyIdstays null (a non-null agency would short-circuit acceptance binding).email = user.username); a different user getsINVITATION_EMAIL_MISMATCH.Frontend
members.edit-gated "Invite as member" button on the contributor row, opening the existingPermissionsModalpicker withhideNone(matching the email dialog). The permission→body mapping is factored into a shared helper used by both invite hooks.Tests
InviteContributorControllerTest,InviteContributorEeTest): email stored + hidden, model + public-endpoint email null,MEMBERS_EDITgate, uniform-404 for every non-visible-contributor id, re-invite dedup, NONE rejection, EE scoped routing + granted languages, nameless-contributor path, acceptance binding + granted membership.data-cyselectors), asserts the request succeeds, that a pending invitation appears, and that theNONErole is absent from the picker.Notes