Entering an email address that has no ContextForge account into the create-team form fails with "User not found". The team is created without that person and no invitation is sent. The user sees the toast "Team {name} created, but some members could not be added" with the address listed against that error. Manage team members behaves the same way.
There is no way to invite anyone from the UI, whether or not they already have an account. Every membership path in the client is a direct add that requires an existing user.
Steps to reproduce:
- Go to Settings > Teams and choose Create team.
- Add a member row with an email address that has no account.
- Submit.
Expected: the address is invited, an invitation is created, and an invitation email is sent if SMTP is configured.
Actual: the team is created without them, the toast above appears, and no invitation exists.
Cause: the client never calls either invitation route. client/src/hooks/useTeamForm.ts calls createTeam() without a members array, then loops addTeamMember() over the rows with Promise.allSettled. client/src/hooks/useTeamMembersForm.ts:149 does the same on save. addTeamMember is POST /teams/{id}/members, which looks the address up in EmailUser and raises UserNotFoundError when there is no row, returning 404.
Both backend paths needed here already exist and are unused by the client:
- POST /teams accepts a members array, adds the emails that resolve to existing users, and creates plus emails an invitation for the ones that do not. It writes the team, the memberships and the invitations in one transaction and reports back as members_added and invitations_sent.
- POST /teams/{team_id}/invitations invites one person to a team that already exists.
Suggested fix: pass members in the createTeam payload rather than looping addTeamMember afterwards, and use POST /teams/{team_id}/invitations for the Manage team members dialog. The create form already collects the email and role per row, which is the shape POST /teams expects.
This also fixes a second problem in the same code. Team creation is currently two steps, so a failure partway leaves a created team and a warning toast rather than failing cleanly. Using the seeding path makes it one transaction.
Acceptance criteria:
- Adding an email with no account to the create-team form creates an invitation for that address instead of reporting "User not found".
- Adding an email with no account in Manage team members creates an invitation.
- Adding an email that does have an account still adds them as a member directly, unchanged.
- Team creation succeeds or fails as a unit, with no partial-success toast.
- The response distinguishes who was added from who was invited, and the UI says which happened for each address.
Note that an invited address still cannot complete acceptance if they have never signed up, because accept_invitation requires an existing user with email_verified_at set. That is #5535 and is not fixed here. This issue is about the client sending an invitation at all.
Related: #5535, #5536, #6010.
Entering an email address that has no ContextForge account into the create-team form fails with "User not found". The team is created without that person and no invitation is sent. The user sees the toast "Team {name} created, but some members could not be added" with the address listed against that error. Manage team members behaves the same way.
There is no way to invite anyone from the UI, whether or not they already have an account. Every membership path in the client is a direct add that requires an existing user.
Steps to reproduce:
Expected: the address is invited, an invitation is created, and an invitation email is sent if SMTP is configured.
Actual: the team is created without them, the toast above appears, and no invitation exists.
Cause: the client never calls either invitation route. client/src/hooks/useTeamForm.ts calls createTeam() without a members array, then loops addTeamMember() over the rows with Promise.allSettled. client/src/hooks/useTeamMembersForm.ts:149 does the same on save. addTeamMember is POST /teams/{id}/members, which looks the address up in EmailUser and raises UserNotFoundError when there is no row, returning 404.
Both backend paths needed here already exist and are unused by the client:
Suggested fix: pass members in the createTeam payload rather than looping addTeamMember afterwards, and use POST /teams/{team_id}/invitations for the Manage team members dialog. The create form already collects the email and role per row, which is the shape POST /teams expects.
This also fixes a second problem in the same code. Team creation is currently two steps, so a failure partway leaves a created team and a warning toast rather than failing cleanly. Using the seeding path makes it one transaction.
Acceptance criteria:
Note that an invited address still cannot complete acceptance if they have never signed up, because accept_invitation requires an existing user with email_verified_at set. That is #5535 and is not fixed here. This issue is about the client sending an invitation at all.
Related: #5535, #5536, #6010.