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,