Browse Source

fix(core): Handle user verification edge case

Fixes #1659
Michael Bromley 3 years ago
parent
commit
1640ea72bc

+ 89 - 0
packages/core/e2e/shop-auth.e2e-spec.ts

@@ -29,6 +29,8 @@ import {
     GetCustomer,
     GetCustomerHistory,
     GetCustomerList,
+    GetCustomerListQuery,
+    GetCustomerListQueryVariables,
     HistoryEntryType,
     Permission,
 } from './graphql/generated-e2e-admin-types';
@@ -73,6 +75,7 @@ let sendEmailFn: jest.Mock;
 })
 class TestEmailPlugin implements OnModuleInit {
     constructor(private eventBus: EventBus) {}
+
     onModuleInit() {
         this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
             sendEmailFn(event);
@@ -616,6 +619,92 @@ describe('Shop auth & accounts', () => {
         });
     });
 
+    // https://github.com/vendure-ecommerce/vendure/issues/1659
+    describe('password reset before verification', () => {
+        const password = 'password';
+        const emailAddress = 'test3@test.com';
+        let verificationToken: string;
+        let passwordResetToken: string;
+        let newCustomerId: string;
+
+        beforeEach(() => {
+            sendEmailFn = jest.fn();
+        });
+
+        it('register a new account without password', async () => {
+            const verificationTokenPromise = getVerificationTokenPromise();
+            const input: RegisterCustomerInput = {
+                firstName: 'Bobby',
+                lastName: 'Tester',
+                phoneNumber: '123456',
+                emailAddress,
+            };
+            const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
+                REGISTER_ACCOUNT,
+                { input },
+            );
+            successErrorGuard.assertSuccess(registerCustomerAccount);
+            verificationToken = await verificationTokenPromise;
+
+            const { customers } = await adminClient.query<
+                GetCustomerListQuery,
+                GetCustomerListQueryVariables
+            >(GET_CUSTOMER_LIST, {
+                options: {
+                    filter: {
+                        emailAddress: { eq: emailAddress },
+                    },
+                },
+            });
+
+            expect(customers.items[0].user?.verified).toBe(false);
+            newCustomerId = customers.items[0].id;
+        });
+
+        it('requestPasswordReset', async () => {
+            const passwordResetTokenPromise = getPasswordResetTokenPromise();
+            const { requestPasswordReset } = await shopClient.query<
+                RequestPasswordReset.Mutation,
+                RequestPasswordReset.Variables
+            >(REQUEST_PASSWORD_RESET, {
+                identifier: emailAddress,
+            });
+            successErrorGuard.assertSuccess(requestPasswordReset);
+
+            await waitForSendEmailFn();
+            passwordResetToken = await passwordResetTokenPromise;
+            expect(requestPasswordReset.success).toBe(true);
+            expect(sendEmailFn).toHaveBeenCalled();
+            expect(passwordResetToken).toBeDefined();
+        });
+
+        it('resetPassword also performs verification', async () => {
+            const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
+                RESET_PASSWORD,
+                {
+                    token: passwordResetToken,
+                    password: 'newPassword',
+                },
+            );
+            currentUserErrorGuard.assertSuccess(resetPassword);
+
+            expect(resetPassword.identifier).toBe(emailAddress);
+            const { customer } = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(
+                GET_CUSTOMER,
+                {
+                    id: newCustomerId,
+                },
+            );
+
+            expect(customer?.user?.verified).toBe(true);
+        });
+
+        it('can log in with new password', async () => {
+            const loginResult = await shopClient.asUserWithCredentials(emailAddress, 'newPassword');
+            expect(loginResult.identifier).toBe(emailAddress);
+        });
+    });
+
     describe('updating emailAddress', () => {
         let emailUpdateToken: string;
         let customer: GetCustomer.Customer;

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

@@ -260,6 +260,14 @@ export class UserService {
             nativeAuthMethod.passwordHash = await this.passwordCipher.hash(password);
             nativeAuthMethod.passwordResetToken = null;
             await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(nativeAuthMethod);
+            if (user.verified === false && this.configService.authOptions.requireVerification) {
+                // This code path represents an edge-case in which the Customer creates an account,
+                // but prior to verifying their email address, they start the password reset flow.
+                // Since the password reset flow makes the exact same guarantee as the email verification
+                // flow (i.e. the person controls the specified email account), we can also consider it
+                // a verification.
+                user.verified = true;
+            }
             return this.connection.getRepository(ctx, User).save(user);
         } else {
             return new PasswordResetTokenExpiredError();