1
0
Эх сурвалжийг харах

feat(core): Normalize email addresses for native auth

Fixes #1515
Michael Bromley 2 жил өмнө
parent
commit
ad7eab8b19

+ 1 - 1
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -8286,7 +8286,7 @@ export type GetCustomerListQuery = {
             lastName: string;
             emailAddress: string;
             phoneNumber?: string | null;
-            user?: { id: string; verified: boolean } | null;
+            user?: { id: string; identifier: string; verified: boolean } | null;
         }>;
     };
 };

+ 1 - 0
packages/core/e2e/graphql/shared-definitions.ts

@@ -137,6 +137,7 @@ export const GET_CUSTOMER_LIST = gql`
                 phoneNumber
                 user {
                     id
+                    identifier
                     verified
                 }
             }

+ 77 - 1
packages/core/e2e/shop-auth.e2e-spec.ts

@@ -1050,7 +1050,7 @@ describe('Expiring tokens', () => {
 });
 
 describe('Registration without email verification', () => {
-    const { server, shopClient } = createTestEnvironment(
+    const { server, shopClient, adminClient } = createTestEnvironment(
         mergeConfig(testConfig(), {
             plugins: [TestEmailPlugin as any],
             authOptions: {
@@ -1066,6 +1066,7 @@ describe('Registration without email verification', () => {
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
             customerCount: 1,
         });
+        await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
 
     beforeEach(() => {
@@ -1127,6 +1128,81 @@ describe('Registration without email verification', () => {
         );
         expect(result.me.identifier).toBe(userEmailAddress);
     });
+
+    it('can login case insensitive', async () => {
+        await shopClient.asUserWithCredentials(userEmailAddress.toUpperCase(), 'test');
+
+        const result = await shopClient.query(
+            gql`
+                query GetMe {
+                    me {
+                        identifier
+                    }
+                }
+            `,
+        );
+        expect(result.me.identifier).toBe(userEmailAddress);
+    });
+
+    it('normalizes customer & user email addresses', async () => {
+        const input: RegisterCustomerInput = {
+            firstName: 'Bobbington',
+            lastName: 'Jarrolds',
+            emailAddress: 'BOBBINGTON.J@Test.com',
+            password: 'test',
+        };
+        const { registerCustomerAccount } = await shopClient.query<
+            CodegenShop.RegisterMutation,
+            CodegenShop.RegisterMutationVariables
+        >(REGISTER_ACCOUNT, {
+            input,
+        });
+        successErrorGuard.assertSuccess(registerCustomerAccount);
+
+        const { customers } = await adminClient.query<
+            Codegen.GetCustomerListQuery,
+            Codegen.GetCustomerListQueryVariables
+        >(GET_CUSTOMER_LIST, {
+            options: {
+                filter: {
+                    firstName: { eq: 'Bobbington' },
+                },
+            },
+        });
+
+        expect(customers.items[0].emailAddress).toBe('bobbington.j@test.com');
+        expect(customers.items[0].user?.identifier).toBe('bobbington.j@test.com');
+    });
+
+    it('registering with same email address with different casing does not create new user', async () => {
+        const input: RegisterCustomerInput = {
+            firstName: 'Glen',
+            lastName: 'Beardsley',
+            emailAddress: userEmailAddress.toUpperCase(),
+            password: 'test',
+        };
+        const { registerCustomerAccount } = await shopClient.query<
+            CodegenShop.RegisterMutation,
+            CodegenShop.RegisterMutationVariables
+        >(REGISTER_ACCOUNT, {
+            input,
+        });
+        successErrorGuard.assertSuccess(registerCustomerAccount);
+
+        const { customers } = await adminClient.query<
+            Codegen.GetCustomerListQuery,
+            Codegen.GetCustomerListQueryVariables
+        >(GET_CUSTOMER_LIST, {
+            options: {
+                filter: {
+                    firstName: { eq: 'Glen' },
+                },
+            },
+        });
+
+        expect(customers.items[0].emailAddress).toBe(userEmailAddress);
+        expect(customers.items[0].user?.identifier).toBe(userEmailAddress);
+    });
 });
 
 describe('Updating email address without email verification', () => {

+ 2 - 1
packages/core/src/service/services/administrator.service.ts

@@ -10,7 +10,7 @@ import { In, IsNull } from 'typeorm';
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/index';
 import { EntityNotFoundError, InternalServerError, UserInputError } from '../../common/error/errors';
-import { idsAreEqual } from '../../common/index';
+import { idsAreEqual, normalizeEmailAddress } from '../../common/index';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ConfigService } from '../../config';
 import { TransactionalConnection } from '../../connection/transactional-connection';
@@ -127,6 +127,7 @@ export class AdministratorService {
     async create(ctx: RequestContext, input: CreateAdministratorInput): Promise<Administrator> {
         await this.checkActiveUserCanGrantRoles(ctx, input.roleIds);
         const administrator = new Administrator(input);
+        administrator.emailAddress = normalizeEmailAddress(input.emailAddress);
         administrator.user = await this.userService.createAdminUser(ctx, input.emailAddress, input.password);
         let createdAdministrator = await this.connection
             .getRepository(ctx, Administrator)

+ 8 - 5
packages/core/src/service/services/user.service.ts

@@ -17,6 +17,7 @@ import {
     VerificationTokenExpiredError,
     VerificationTokenInvalidError,
 } from '../../common/error/generated-graphql-shop-errors';
+import { normalizeEmailAddress } from '../../common/index';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { NativeAuthenticationMethod } from '../../entity/authentication-method/native-authentication-method.entity';
@@ -72,7 +73,9 @@ export class UserService {
             .leftJoinAndSelect('user.roles', 'roles')
             .leftJoinAndSelect('roles.channels', 'channels')
             .leftJoinAndSelect('user.authenticationMethods', 'authenticationMethods')
-            .where('user.identifier = :identifier', { identifier: emailAddress })
+            .where('LOWER(user.identifier) = :identifier', {
+                identifier: normalizeEmailAddress(emailAddress),
+            })
             .andWhere('user.deletedAt IS NULL')
             .getOne()
             .then(result => result ?? undefined);
@@ -88,7 +91,7 @@ export class UserService {
         password?: string,
     ): Promise<User | PasswordValidationError> {
         const user = new User();
-        user.identifier = identifier;
+        user.identifier = normalizeEmailAddress(identifier);
         const customerRole = await this.roleService.getCustomerRole(ctx);
         user.roles = [customerRole];
         const addNativeAuthResult = await this.addNativeAuthenticationMethod(ctx, user, identifier, password);
@@ -138,7 +141,7 @@ export class UserService {
         } else {
             authenticationMethod.passwordHash = '';
         }
-        authenticationMethod.identifier = identifier;
+        authenticationMethod.identifier = normalizeEmailAddress(identifier);
         authenticationMethod.user = user;
         await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(authenticationMethod);
         user.authenticationMethods = [...(user.authenticationMethods ?? []), authenticationMethod];
@@ -151,14 +154,14 @@ export class UserService {
      */
     async createAdminUser(ctx: RequestContext, identifier: string, password: string): Promise<User> {
         const user = new User({
-            identifier,
+            identifier: normalizeEmailAddress(identifier),
             verified: true,
         });
         const authenticationMethod = await this.connection
             .getRepository(ctx, NativeAuthenticationMethod)
             .save(
                 new NativeAuthenticationMethod({
-                    identifier,
+                    identifier: normalizeEmailAddress(identifier),
                     passwordHash: await this.passwordCipher.hash(password),
                 }),
             );