From 52df3ebbbba5b922e753709358da7d4ac1802b52 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Wed, 17 Apr 2024 17:55:54 +0800 Subject: [PATCH] fix(core,console): invitee emails should be case insensitive (#5730) * fix(core,console): invitee email checks should be case insensitive * test: add integration test * chore: add changeset --- .changeset/afraid-stingrays-perform.md | 5 +++++ .../TenantMembers/InviteEmailsInput/hooks.ts | 7 ++++--- packages/core/src/queries/organization/index.ts | 4 ++-- .../api/organization/organization-invitation.get.test.ts | 7 ++++--- 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .changeset/afraid-stingrays-perform.md diff --git a/.changeset/afraid-stingrays-perform.md b/.changeset/afraid-stingrays-perform.md new file mode 100644 index 000000000..2bfa7f270 --- /dev/null +++ b/.changeset/afraid-stingrays-perform.md @@ -0,0 +1,5 @@ +--- +"@logto/core": patch +--- + +Bug fix: organization invitation APIs should handle invitee emails case insensitively diff --git a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/hooks.ts b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/hooks.ts index f57aca982..e10338ab3 100644 --- a/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/hooks.ts +++ b/packages/console/src/pages/TenantSettings/TenantMembers/InviteEmailsInput/hooks.ts @@ -44,15 +44,16 @@ const useEmailInputUtils = () => { const validEmails = new Set(); const existingMemberEmails = new Set( - existingMembers.map(({ primaryEmail }) => primaryEmail ?? '').filter(Boolean) + existingMembers.map(({ primaryEmail }) => primaryEmail?.toLowerCase() ?? '').filter(Boolean) ); const existingInvitationEmails = new Set( existingInvitations .filter(({ status }) => status === OrganizationInvitationStatus.Pending) - .map(({ invitee }) => invitee) + .map(({ invitee }) => invitee.toLowerCase()) ); - for (const email of emails) { + for (const userInputEmail of emails) { + const email = userInputEmail.toLowerCase(); if (!emailRegEx.test(email)) { invalidEmails.add(email); } diff --git a/packages/core/src/queries/organization/index.ts b/packages/core/src/queries/organization/index.ts index 266728d4b..573c82411 100644 --- a/packages/core/src/queries/organization/index.ts +++ b/packages/core/src/queries/organization/index.ts @@ -165,7 +165,7 @@ class OrganizationInvitationsQueries extends SchemaQueries< return sql`and ${fields.organizationId} = ${id}`; })} ${conditionalSql(invitee, (email) => { - return sql`and ${fields.invitee} = ${email}`; + return sql`and lower(${fields.invitee}) = lower(${email})`; })} `); } @@ -220,7 +220,7 @@ class OrganizationInvitationsQueries extends SchemaQueries< return sql`and ${fields.inviterId} = ${id}`; })} ${conditionalSql(invitee, (email) => { - return sql`and ${fields.invitee} = ${email}`; + return sql`and lower(${fields.invitee}) = lower(${email})`; })} group by ${fields.id} ${conditionalSql(this.orderBy, ({ field, order }) => { diff --git a/packages/integration-tests/src/tests/api/organization/organization-invitation.get.test.ts b/packages/integration-tests/src/tests/api/organization/organization-invitation.get.test.ts index e59e3c4c1..16b6bb30e 100644 --- a/packages/integration-tests/src/tests/api/organization/organization-invitation.get.test.ts +++ b/packages/integration-tests/src/tests/api/organization/organization-invitation.get.test.ts @@ -61,18 +61,19 @@ describe('organization invitation creation', () => { await Promise.all([deleteUser(inviter.id), deleteUser(inviter2.id)]); }); - it('should be able to get invitations by invitee', async () => { + it('should be able to get invitations by invitee email (case insensitive)', async () => { const organization = await organizationApi.create({ name: 'test' }); const invitee = `${randomId()}@example.com`; await invitationApi.create({ organizationId: organization.id, - invitee, + // Deliberately use uppercase email when creating the invitation + invitee: invitee.toUpperCase(), expiresAt: Date.now() + 1_000_000, }); const invitations = await invitationApi.getList(new URLSearchParams({ invitee })); expect(invitations.length).toBe(1); - expect(invitations[0]?.invitee).toBe(invitee); + expect(invitations[0]?.invitee.toLocaleLowerCase()).toBe(invitee.toLowerCase()); }); it('should have no pagination', async () => {