@@ -135,11 +135,25 @@ export async function inviteMembers({
135135 const existingMembers = await prisma . orgMember . findMany ( {
136136 where : {
137137 organizationId : org . id ,
138- user : { email : { in : boundedIn ( [ ...uniqueEmails ] ) } } ,
138+ user : { email : { in : boundedIn ( [ ...uniqueEmails ] ) , mode : "insensitive" } } ,
139139 } ,
140140 select : { user : { select : { email : true } } } ,
141141 } ) ;
142- const existingMemberEmails = new Set ( existingMembers . map ( ( member ) => member . user . email ) ) ;
142+ // Compare folded: stored account emails and invite rows can both carry
143+ // casing the caller didn't type, and the unique org+email constraint the
144+ // P2002 below relies on is itself case-sensitive.
145+ const existingMemberEmails = new Set (
146+ existingMembers . map ( ( member ) => member . user . email . toLowerCase ( ) )
147+ ) ;
148+
149+ const pendingInvites = await prisma . orgMemberInvite . findMany ( {
150+ where : {
151+ organizationId : org . id ,
152+ email : { in : boundedIn ( [ ...uniqueEmails ] ) , mode : "insensitive" } ,
153+ } ,
154+ select : { email : true } ,
155+ } ) ;
156+ const pendingInviteEmails = new Set ( pendingInvites . map ( ( invite ) => invite . email . toLowerCase ( ) ) ) ;
143157
144158 // Create one invite per unique email and return ONLY the invites actually
145159 // created by this call. A P2002 means the email is already invited to this org
@@ -153,15 +167,26 @@ export async function inviteMembers({
153167 const alreadyInvited : string [ ] = [ ] ;
154168
155169 for ( const email of uniqueEmails ) {
156- if ( existingMemberEmails . has ( email ) ) {
170+ const folded = email . toLowerCase ( ) ;
171+
172+ if ( existingMemberEmails . has ( folded ) ) {
157173 alreadyMembers . push ( email ) ;
158174 continue ;
159175 }
160176
177+ if ( pendingInviteEmails . has ( folded ) ) {
178+ alreadyInvited . push ( email ) ;
179+ continue ;
180+ }
181+
161182 try {
162183 const invite = await prisma . orgMemberInvite . create ( {
163184 data : {
164- email,
185+ // Store folded. The @@unique ([organizationId, email]) backstop is
186+ // case-sensitive, so it only catches a concurrent duplicate if every
187+ // row for an address is written the same way — this keeps that true
188+ // regardless of what a caller passes in.
189+ email : folded ,
165190 token : tokenGenerator ( ) ,
166191 organizationId : org . id ,
167192 inviterId : userId ,
@@ -204,7 +229,7 @@ export async function getInviteFromToken({ token }: { token: string }) {
204229export async function getUsersInvites ( { email } : { email : string } ) {
205230 return await prisma . orgMemberInvite . findMany ( {
206231 where : {
207- email,
232+ email : { equals : email , mode : "insensitive" } ,
208233 organization : {
209234 deletedAt : null ,
210235 } ,
@@ -467,7 +492,7 @@ export async function acceptInvite({
467492 const invite = await prisma . orgMemberInvite . findFirst ( {
468493 where : {
469494 id : inviteId ,
470- email : user . email ,
495+ email : { equals : user . email , mode : "insensitive" } ,
471496 organization : {
472497 deletedAt : null ,
473498 } ,
@@ -563,7 +588,7 @@ export async function acceptInvite({
563588 await prisma . orgMemberInvite . delete ( {
564589 where : {
565590 id : inviteId ,
566- email : user . email ,
591+ email : { equals : user . email , mode : "insensitive" } ,
567592 } ,
568593 } ) ;
569594 } catch ( error ) {
@@ -606,7 +631,7 @@ export async function declineInvite({
606631 const declinedInvite = await tx . orgMemberInvite . delete ( {
607632 where : {
608633 id : inviteId ,
609- email : user . email ,
634+ email : { equals : user . email , mode : "insensitive" } ,
610635 } ,
611636 include : {
612637 organization : true ,
@@ -616,7 +641,7 @@ export async function declineInvite({
616641 //2. check for other invites
617642 const remainingInvites = await tx . orgMemberInvite . findMany ( {
618643 where : {
619- email : user . email ,
644+ email : { equals : user . email , mode : "insensitive" } ,
620645 } ,
621646 } ) ;
622647
0 commit comments