Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/invite-email-case-insensitive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: fix
---

Org member invites now match emails case-insensitively, so an invite whose email casing differs from the invitee's account email can be accepted. Re-inviting an already-invited email now resends the invite instead of failing.
24 changes: 19 additions & 5 deletions apps/webapp/app/models/member.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,12 @@ export async function inviteMembers({
const existingMembers = await prisma.orgMember.findMany({
where: {
organizationId: org.id,
user: { email: { in: [...uniqueEmails] } },
user: {
email: {
in: [...uniqueEmails],
mode: "insensitive",
},
},
},
select: { user: { select: { email: true } } },
});
Expand Down Expand Up @@ -203,7 +208,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,
},
Expand Down Expand Up @@ -562,7 +567,7 @@ export async function acceptInvite({
await prisma.orgMemberInvite.delete({
where: {
id: inviteId,
email: user.email,
email: { equals: user.email, mode: "insensitive" },
},
});
} catch (error) {
Expand All @@ -573,6 +578,15 @@ export async function acceptInvite({
}
}

// Consume any case-variant duplicate invites for this org (rows created
// before invite emails were lowercased)
await prisma.orgMemberInvite.deleteMany({
where: {
organizationId: invite.organizationId,
email: { equals: user.email, mode: "insensitive" },
},
});

const remainingInvites = await getUsersInvites({ email: user.email });

if (invite.rbacRoleId) {
Expand Down Expand Up @@ -605,7 +619,7 @@ export async function declineInvite({
const declinedInvite = await tx.orgMemberInvite.delete({
where: {
id: inviteId,
email: user.email,
email: { equals: user.email, mode: "insensitive" },
},
include: {
organization: true,
Expand All @@ -615,7 +629,7 @@ export async function declineInvite({
//2. check for other invites
const remainingInvites = await tx.orgMemberInvite.findMany({
where: {
email: user.email,
email: { equals: user.email, mode: "insensitive" },
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,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(),
});

Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/routes/invite-accept.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down