Skip to content

Commit 9ce880b

Browse files
committed
fix(webapp): return existing invite on re-invite regardless of original inviter
With skipDuplicates, an email already invited by a different org member is skipped on re-insert; scoping the follow-up findMany to the current inviter then dropped it from the response, so the caller saw an empty invite list for an email that is in fact invited. Scope the lookup to the org + emails instead. Adds a cross-inviter re-invite test.
1 parent cd0814e commit 9ce880b

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

apps/webapp/app/models/member.server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,14 @@ export async function inviteMembers({
119119
skipDuplicates: true,
120120
});
121121

122+
// Return the invite for each submitted email scoped to the org, NOT to the
123+
// current inviter: with skipDuplicates an email already invited by someone
124+
// else is skipped, and filtering by inviterId here would drop it from the
125+
// result — leaving the caller with a misleading empty list for an email that
126+
// is in fact invited.
122127
return await prisma.orgMemberInvite.findMany({
123128
where: {
124129
organizationId: org.id,
125-
inviterId: userId,
126130
email: {
127131
in: emails,
128132
},

apps/webapp/test/auth-api.e2e.full.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3073,6 +3073,40 @@ describe("API", () => {
30733073
expect(second.status).toBe(201);
30743074
expect(second.status).not.toBe(500);
30753075
});
3076+
3077+
it("re-inviting an email another org member already invited still returns it", async () => {
3078+
const server = getTestServer();
3079+
const { organization, pat } = await seedTestUserProject(server.prisma);
3080+
3081+
// A different user invited this email first; skipDuplicates will skip the
3082+
// re-insert, so the response must still surface the existing invite rather
3083+
// than an empty list scoped to the current caller.
3084+
const otherUser = await server.prisma.user.create({
3085+
data: {
3086+
email: `other_${organization.id}@example.com`,
3087+
authenticationMethod: "MAGIC_LINK",
3088+
},
3089+
});
3090+
const sharedEmail = `shared_${organization.id}@example.com`;
3091+
await server.prisma.orgMemberInvite.create({
3092+
data: {
3093+
email: sharedEmail,
3094+
token: `tok_${Math.random().toString(36).slice(2)}`,
3095+
organizationId: organization.id,
3096+
inviterId: otherUser.id,
3097+
role: "MEMBER",
3098+
},
3099+
});
3100+
3101+
const res = await server.webapp.fetch(`/api/v1/orgs/${organization.id}/invites`, {
3102+
method: "POST",
3103+
headers: { Authorization: `Bearer ${pat.token}`, "Content-Type": "application/json" },
3104+
body: JSON.stringify({ emails: [sharedEmail] }),
3105+
});
3106+
expect(res.status).toBe(201);
3107+
const body = (await res.json()) as { invites: { email: string }[] };
3108+
expect(body.invites.map((i) => i.email)).toContain(sharedEmail);
3109+
});
30763110
});
30773111

30783112
// Org creation via the management API is gated behind

0 commit comments

Comments
 (0)