ソースを参照

feat(core): Verify admin-created Customers if password supplied

Relates to #171
Michael Bromley 6 年 前
コミット
9931e2574f

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

@@ -132,7 +132,7 @@ describe('Authorization & permissions', () => {
             it('can create', async () => {
                 await assertRequestAllowed(
                     gql`
-                        mutation CreateCustomer($input: CreateCustomerInput!) {
+                        mutation CanCreateCustomer($input: CreateCustomerInput!) {
                             createCustomer(input: $input) {
                                 id
                             }

+ 85 - 5
packages/core/e2e/customer.e2e-spec.ts

@@ -1,11 +1,18 @@
+import { OnModuleInit } from '@nestjs/common';
 import { omit } from '@vendure/common/lib/omit';
 import gql from 'graphql-tag';
 import path from 'path';
 
+import { EventBus } from '../src/event-bus/event-bus';
+import { EventBusModule } from '../src/event-bus/event-bus.module';
+import { AccountRegistrationEvent } from '../src/event-bus/events/account-registration-event';
+import { VendurePlugin } from '../src/plugin/vendure-plugin';
+
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import { CUSTOMER_FRAGMENT } from './graphql/fragments';
 import {
     CreateAddress,
+    CreateCustomer,
     DeleteCustomer,
     DeleteCustomerAddress,
     DeletionResult,
@@ -23,6 +30,7 @@ import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 
 // tslint:disable:no-non-null-assertion
+let sendEmailFn: jest.Mock;
 
 describe('Customer resolver', () => {
     const adminClient = new TestAdminClient();
@@ -33,10 +41,15 @@ describe('Customer resolver', () => {
     let thirdCustomer: GetCustomerList.Items;
 
     beforeAll(async () => {
-        const token = await server.init({
-            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-            customerCount: 5,
-        });
+        const token = await server.init(
+            {
+                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+                customerCount: 5,
+            },
+            {
+                plugins: [TestEmailPlugin],
+            },
+        );
         await adminClient.init();
     }, TEST_SETUP_TIMEOUT_MS);
 
@@ -303,9 +316,51 @@ describe('Customer resolver', () => {
         });
     });
 
+    describe('creation', () => {
+        it('triggers verification event if no password supplied', async () => {
+            sendEmailFn = jest.fn();
+            const { createCustomer } = await adminClient.query<
+                CreateCustomer.Mutation,
+                CreateCustomer.Variables
+            >(CREATE_CUSTOMER, {
+                input: {
+                    emailAddress: 'test1@test.com',
+                    firstName: 'New',
+                    lastName: 'Customer',
+                },
+            });
+
+            expect(createCustomer.user!.verified).toBe(false);
+            expect(sendEmailFn).toHaveBeenCalledTimes(1);
+            expect(sendEmailFn.mock.calls[0][0] instanceof AccountRegistrationEvent).toBe(true);
+            expect(sendEmailFn.mock.calls[0][0].user.identifier).toBe('test1@test.com');
+        });
+
+        it('creates a verified Customer', async () => {
+            sendEmailFn = jest.fn();
+            const { createCustomer } = await adminClient.query<
+                CreateCustomer.Mutation,
+                CreateCustomer.Variables
+            >(CREATE_CUSTOMER, {
+                input: {
+                    emailAddress: 'test2@test.com',
+                    firstName: 'New',
+                    lastName: 'Customer',
+                },
+                password: 'test',
+            });
+
+            expect(createCustomer.user!.verified).toBe(true);
+            expect(sendEmailFn).toHaveBeenCalledTimes(0);
+        });
+    });
+
     describe('deletion', () => {
         it('deletes a customer', async () => {
-            const result = await adminClient.query<DeleteCustomer.Mutation, DeleteCustomer.Variables>(DELETE_CUSTOMER, { id: thirdCustomer.id });
+            const result = await adminClient.query<DeleteCustomer.Mutation, DeleteCustomer.Variables>(
+                DELETE_CUSTOMER,
+                { id: thirdCustomer.id },
+            );
 
             expect(result.deleteCustomer).toEqual({ result: DeletionResult.DELETED });
         });
@@ -406,6 +461,15 @@ const GET_CUSTOMER_ORDERS = gql`
     }
 `;
 
+export const CREATE_CUSTOMER = gql`
+    mutation CreateCustomer($input: CreateCustomerInput!, $password: String) {
+        createCustomer(input: $input, password: $password) {
+            ...Customer
+        }
+    }
+    ${CUSTOMER_FRAGMENT}
+`;
+
 export const UPDATE_CUSTOMER = gql`
     mutation UpdateCustomer($input: UpdateCustomerInput!) {
         updateCustomer(input: $input) {
@@ -422,3 +486,19 @@ const DELETE_CUSTOMER = gql`
         }
     }
 `;
+
+/**
+ * This mock plugin simulates an EmailPlugin which would send emails
+ * on the registration & password reset events.
+ */
+@VendurePlugin({
+    imports: [EventBusModule],
+})
+class TestEmailPlugin implements OnModuleInit {
+    constructor(private eventBus: EventBus) {}
+    onModuleInit() {
+        this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
+            sendEmailFn(event);
+        });
+    }
+}

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

@@ -3350,11 +3350,11 @@ export type UpdateAdministratorMutation = { __typename?: 'Mutation' } & {
     updateAdministrator: { __typename?: 'Administrator' } & AdministratorFragment;
 };
 
-export type CreateCustomerMutationVariables = {
+export type CanCreateCustomerMutationVariables = {
     input: CreateCustomerInput;
 };
 
-export type CreateCustomerMutation = { __typename?: 'Mutation' } & {
+export type CanCreateCustomerMutation = { __typename?: 'Mutation' } & {
     createCustomer: { __typename?: 'Customer' } & Pick<Customer, 'id'>;
 };
 
@@ -3595,6 +3595,15 @@ export type GetCustomerOrdersQuery = { __typename?: 'Query' } & {
     >;
 };
 
+export type CreateCustomerMutationVariables = {
+    input: CreateCustomerInput;
+    password?: Maybe<Scalars['String']>;
+};
+
+export type CreateCustomerMutation = { __typename?: 'Mutation' } & {
+    createCustomer: { __typename?: 'Customer' } & CustomerFragment;
+};
+
 export type UpdateCustomerMutationVariables = {
     input: UpdateCustomerInput;
 };
@@ -4962,10 +4971,10 @@ export namespace UpdateAdministrator {
     export type UpdateAdministrator = AdministratorFragment;
 }
 
-export namespace CreateCustomer {
-    export type Variables = CreateCustomerMutationVariables;
-    export type Mutation = CreateCustomerMutation;
-    export type CreateCustomer = CreateCustomerMutation['createCustomer'];
+export namespace CanCreateCustomer {
+    export type Variables = CanCreateCustomerMutationVariables;
+    export type Mutation = CanCreateCustomerMutation;
+    export type CreateCustomer = CanCreateCustomerMutation['createCustomer'];
 }
 
 export namespace GetCustomerCount {
@@ -5136,6 +5145,12 @@ export namespace GetCustomerOrders {
     export type Items = NonNullable<(NonNullable<GetCustomerOrdersQuery['customer']>)['orders']['items'][0]>;
 }
 
+export namespace CreateCustomer {
+    export type Variables = CreateCustomerMutationVariables;
+    export type Mutation = CreateCustomerMutation;
+    export type CreateCustomer = CustomerFragment;
+}
+
 export namespace UpdateCustomer {
     export type Variables = UpdateCustomerMutationVariables;
     export type Mutation = UpdateCustomerMutation;

+ 5 - 2
packages/core/src/api/resolvers/admin/customer.resolver.ts

@@ -39,9 +39,12 @@ export class CustomerResolver {
 
     @Mutation()
     @Allow(Permission.CreateCustomer)
-    async createCustomer(@Args() args: MutationCreateCustomerArgs): Promise<Customer> {
+    async createCustomer(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationCreateCustomerArgs,
+    ): Promise<Customer> {
         const { input, password } = args;
-        return this.customerService.create(input, password || undefined);
+        return this.customerService.create(ctx, input, password || undefined);
     }
 
     @Mutation()

+ 16 - 4
packages/core/src/service/services/customer.service.ts

@@ -80,7 +80,7 @@ export class CustomerService {
             });
     }
 
-    async create(input: CreateCustomerInput, password?: string): Promise<Customer> {
+    async create(ctx: RequestContext, input: CreateCustomerInput, password?: string): Promise<Customer> {
         input.emailAddress = normalizeEmailAddress(input.emailAddress);
         const customer = new Customer(input);
 
@@ -93,9 +93,17 @@ export class CustomerService {
         if (existing) {
             throw new InternalServerError(`error.email-address-must-be-unique`);
         }
+        customer.user = await this.userService.createCustomerUser(input.emailAddress, password);
 
-        if (password) {
-            customer.user = await this.userService.createCustomerUser(input.emailAddress, password);
+        if (password && password !== '') {
+            if (customer.user.verificationToken) {
+                customer.user = await this.userService.verifyUserByToken(
+                    customer.user.verificationToken,
+                    password,
+                );
+            }
+        } else {
+            this.eventBus.publish(new AccountRegistrationEvent(ctx, customer.user));
         }
         return this.connection.getRepository(Customer).save(customer);
     }
@@ -168,7 +176,11 @@ export class CustomerService {
         }
     }
 
-    async requestUpdateEmailAddress(ctx: RequestContext, userId: ID, newEmailAddress: string): Promise<boolean> {
+    async requestUpdateEmailAddress(
+        ctx: RequestContext,
+        userId: ID,
+        newEmailAddress: string,
+    ): Promise<boolean> {
         const userWithConflictingIdentifier = await this.userService.getUserByEmailAddress(newEmailAddress);
         if (userWithConflictingIdentifier) {
             throw new UserInputError('error.email-address-not-available');