From 36485affc2a67eb2838d4fc6a887e01a833818c7 Mon Sep 17 00:00:00 2001 From: isshaddad Date: Fri, 5 Jun 2026 16:50:34 -0400 Subject: [PATCH] fix(webapp): match org invite emails case-insensitively --- .server-changes/invite-email-case-insensitive.md | 12 ++++++++++++ apps/webapp/app/models/member.server.ts | 10 +++++----- .../_app.orgs.$organizationSlug.invite/route.tsx | 2 +- apps/webapp/app/routes/invite-accept.tsx | 2 +- 4 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 .server-changes/invite-email-case-insensitive.md diff --git a/.server-changes/invite-email-case-insensitive.md b/.server-changes/invite-email-case-insensitive.md new file mode 100644 index 000000000..60b4df0d1 --- /dev/null +++ b/.server-changes/invite-email-case-insensitive.md @@ -0,0 +1,12 @@ +--- +area: webapp +type: fix +--- + +Org member invites now match emails case-insensitively. Previously an invite +created with different casing than the invitee's account email (e.g. +"Andreas@example.com" vs "andreas@example.com") could never be accepted — +the accept route compared emails strictly and the pending-invite lookups +were exact-match. Invite emails are now lowercased on creation, and all +invite-by-email lookups (accept, decline, pending list) match +case-insensitively so existing mixed-case invite rows still work. diff --git a/apps/webapp/app/models/member.server.ts b/apps/webapp/app/models/member.server.ts index b88fc7e11..bf5421d45 100644 --- a/apps/webapp/app/models/member.server.ts +++ b/apps/webapp/app/models/member.server.ts @@ -156,7 +156,7 @@ export async function getInviteFromToken({ token }: { token: string }) { export async function getUsersInvites({ email }: { email: string }) { return await prisma.orgMemberInvite.findMany({ where: { - email, + email: { equals: email, mode: "insensitive" }, organization: { deletedAt: null, }, @@ -180,7 +180,7 @@ export async function acceptInvite({ const invite = await tx.orgMemberInvite.delete({ where: { id: inviteId, - email: user.email, + email: { equals: user.email, mode: "insensitive" }, }, include: { organization: { @@ -215,7 +215,7 @@ export async function acceptInvite({ // 4. Check for other invites const remainingInvites = await tx.orgMemberInvite.findMany({ where: { - email: user.email, + email: { equals: user.email, mode: "insensitive" }, }, }); @@ -259,7 +259,7 @@ export async function declineInvite({ const declinedInvite = await prisma.orgMemberInvite.delete({ where: { id: inviteId, - email: user.email, + email: { equals: user.email, mode: "insensitive" }, }, include: { organization: true, @@ -269,7 +269,7 @@ export async function declineInvite({ //2. check for other invites const remainingInvites = await prisma.orgMemberInvite.findMany({ where: { - email: user.email, + email: { equals: user.email, mode: "insensitive" }, }, }); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx index f77c19ffb..89cfb9513 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx @@ -149,7 +149,7 @@ const schema = z.object({ } return [""]; - }, z.string().email().array().nonempty("At least one email is required")), + }, z.string().trim().toLowerCase().email().array().nonempty("At least one email is required")), rbacRoleId: z.string().optional(), }); diff --git a/apps/webapp/app/routes/invite-accept.tsx b/apps/webapp/app/routes/invite-accept.tsx index 592384b95..b777ea393 100644 --- a/apps/webapp/app/routes/invite-accept.tsx +++ b/apps/webapp/app/routes/invite-accept.tsx @@ -34,7 +34,7 @@ export async function loader({ request }: LoaderFunctionArgs) { ); } - if (invite.email !== user.email) { + if (invite.email.toLowerCase() !== user.email.toLowerCase()) { return redirectWithErrorMessage( "/", request,