Просмотр исходного кода

feat(server): Implement account registration without verification

Michael Bromley 7 лет назад
Родитель
Сommit
00ca4d4017

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
schema.json


+ 85 - 2
server/e2e/auth.e2e-spec.ts

@@ -154,6 +154,19 @@ describe('Authorization & permissions', () => {
             sendEmailFn = jest.fn();
         });
 
+        it(
+            'errors if a password is provided',
+            assertThrowsWithMessage(async () => {
+                const input: RegisterCustomerInput = {
+                    firstName: 'Sofia',
+                    lastName: 'Green',
+                    emailAddress: 'sofia.green@test.com',
+                    password: 'test',
+                };
+                const result = await client.query(REGISTER_ACCOUNT, { input });
+            }, 'Do not provide a password when `authOptions.requireVerification` is set to "true"'),
+        );
+
         it('register a new account', async () => {
             const verificationTokenPromise = getVerificationTokenPromise();
             const input: RegisterCustomerInput = {
@@ -384,11 +397,11 @@ describe('Expiring registration token', () => {
                 lastName: 'Wallace',
                 emailAddress: 'barry.wallace@test.com',
             };
-            const result1 = await client.query(REGISTER_ACCOUNT, { input });
+            const result = await client.query(REGISTER_ACCOUNT, { input });
 
             const verificationToken = await verificationTokenPromise;
 
-            expect(result1.registerCustomerAccount).toBe(true);
+            expect(result.registerCustomerAccount).toBe(true);
             expect(sendEmailFn).toHaveBeenCalledTimes(1);
             expect(verificationToken).toBeDefined();
 
@@ -402,6 +415,76 @@ describe('Expiring registration token', () => {
     );
 });
 
+describe('Registration without email verification', () => {
+    const client = new TestClient();
+    const server = new TestServer();
+    const userEmailAddress = 'glen.beardsley@test.com';
+
+    beforeAll(async () => {
+        const token = await server.init(
+            {
+                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
+                customerCount: 1,
+            },
+            {
+                emailOptions,
+                authOptions: {
+                    requireVerification: false,
+                },
+            },
+        );
+        await client.init();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    beforeEach(() => {
+        sendEmailFn = jest.fn();
+    });
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it(
+        'errors if no password is provided',
+        assertThrowsWithMessage(async () => {
+            const input: RegisterCustomerInput = {
+                firstName: 'Glen',
+                lastName: 'Beardsley',
+                emailAddress: userEmailAddress,
+            };
+            const result = await client.query(REGISTER_ACCOUNT, { input });
+        }, 'A password must be provided when `authOptions.requireVerification` is set to "false"'),
+    );
+
+    it('register a new account with password', async () => {
+        const input: RegisterCustomerInput = {
+            firstName: 'Glen',
+            lastName: 'Beardsley',
+            emailAddress: userEmailAddress,
+            password: 'test',
+        };
+        const result = await client.query(REGISTER_ACCOUNT, { input });
+
+        expect(result.registerCustomerAccount).toBe(true);
+        expect(sendEmailFn).not.toHaveBeenCalled();
+    });
+
+    it('can login after registering', async () => {
+        await client.asUserWithCredentials(userEmailAddress, 'test');
+
+        const result = await client.query(
+            gql`
+                query {
+                    me {
+                        identifier
+                    }
+                }
+            `,
+        );
+        expect(result.me.identifier).toBe(userEmailAddress);
+    });
+});
+
 function getVerificationTokenPromise(): Promise<string> {
     return new Promise<string>(resolve => {
         sendEmailFn.mockImplementation(ctx => {

+ 3 - 2
server/src/api/types/auth.api.graphql

@@ -7,9 +7,9 @@ type Mutation {
     logout: Boolean!
     "Register a Customer account with the given credentials"
     registerCustomerAccount(input: RegisterCustomerInput!): Boolean!
-    "Verify a Customer email address with the token sent to that address"
+    "Verify a Customer email address with the token sent to that address. Only applicable if `authOptions.requireVerification` is set to true."
     verifyCustomerAccount(token: String!, password: String!): LoginResult!
-    "Regenerate and send a verification token for a new Customer registration"
+    "Regenerate and send a verification token for a new Customer registration. Only applicable if `authOptions.requireVerification` is set to true."
     refreshCustomerVerification(emailAddress: String!): Boolean!
 }
 
@@ -28,4 +28,5 @@ input RegisterCustomerInput {
     title: String
     firstName: String
     lastName: String
+    password: String
 }

+ 4 - 0
server/src/config/vendure-config.ts

@@ -89,6 +89,10 @@ export interface AuthOptions {
      * @description
      * Determines whether new User accounts require verification of their email address.
      *
+     * If set to "true", when registering via the `registerCustomerAccount` mutation, one should *not* set the
+     * `password` property - doing so will result in an error. Instead, the password is set at a later stage
+     * (once the email with the verification token has been opened) via the `verifyCustomerAccount` mutation.
+     *
      * @defaut true
      */
     requireVerification?: boolean;

+ 3 - 1
server/src/i18n/messages/en.json

@@ -12,6 +12,7 @@
     "entity-with-id-not-found": "No { entityName } with the id '{ id }' could be found",
     "forbidden": "You are not currently authorized to perform this action",
     "invalid-sort-field": "The sort field '{ fieldName }' is invalid. Valid fields are: { validFields }",
+    "missing-password-on-registration": "A password must be provided when `authOptions.requireVerification` is set to \"false\"",
     "no-search-plugin-configured": "No search plugin has been configured",
     "no-valid-channel-specified": "No valid channel was specified (ensure the 'vendure-token' header was specified in the request)",
     "order-contents-may-only-be-modified-in-addingitems-state": "Order contents may only be modified when in the \"AddingItems\" state",
@@ -20,7 +21,8 @@
     "payment-may-only-be-added-in-arrangingpayment-state": "A Payment may only be added when Order is in \"ArrangingPayment\" state",
     "verification-token-has-expired": "Verification token has expired. Use refreshCustomerVerification to send a new token.",
     "verification-token-not-recognized": "Verification token not recognized",
-    "unauthorized": "The credentials did not match. Please check and try again"
+    "unauthorized": "The credentials did not match. Please check and try again",
+    "unexpected-password-on-registration": "Do not provide a password when `authOptions.requireVerification` is set to \"true\""
   },
   "message": {
     "country-used-in-addresses": "The selected Country cannot be deleted as it is used in {count, plural, one {1 Address} other {# Addresses}}",

+ 13 - 2
server/src/service/services/customer.service.ts

@@ -13,9 +13,10 @@ import {
 } from '../../../../shared/generated-types';
 import { ID, PaginatedList } from '../../../../shared/shared-types';
 import { RequestContext } from '../../api/common/request-context';
-import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
+import { EntityNotFoundError, InternalServerError, UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual, normalizeEmailAddress } from '../../common/utils';
+import { ConfigService } from '../../config/config.service';
 import { Address } from '../../entity/address/address.entity';
 import { Customer } from '../../entity/customer/customer.entity';
 import { EventBus } from '../../event-bus/event-bus';
@@ -32,6 +33,7 @@ import { UserService } from './user.service';
 export class CustomerService {
     constructor(
         @InjectConnection() private connection: Connection,
+        private configService: ConfigService,
         private userService: UserService,
         private countryService: CountryService,
         private listQueryBuilder: ListQueryBuilder,
@@ -95,6 +97,15 @@ export class CustomerService {
     }
 
     async registerCustomerAccount(ctx: RequestContext, input: RegisterCustomerInput): Promise<boolean> {
+        if (this.configService.authOptions.requireVerification) {
+            if (input.password) {
+                throw new UserInputError(`error.unexpected-password-on-registration`);
+            }
+        } else {
+            if (!input.password) {
+                throw new UserInputError(`error.missing-password-on-registration`);
+            }
+        }
         let user = await this.userService.getUserByEmailAddress(input.emailAddress);
         if (user && user.verified) {
             // If the user has already been verified, do nothing
@@ -107,7 +118,7 @@ export class CustomerService {
             lastName: input.lastName || '',
         });
         if (!user) {
-            user = await this.userService.createCustomerUser(input.emailAddress);
+            user = await this.userService.createCustomerUser(input.emailAddress, input.password || undefined);
         } else if (!user.verified) {
             user = await this.userService.setVerificationToken(user);
         }

+ 5 - 3
shared/generated-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-02-18T13:01:33+01:00
+// Generated in 2019-02-18T15:05:52+01:00
 export type Maybe<T> = T | null;
 
 
@@ -566,6 +566,8 @@ export interface RegisterCustomerInput {
   firstName?: Maybe<string>;
   
   lastName?: Maybe<string>;
+  
+  password?: Maybe<string>;
 }
 
 export interface CreateChannelInput {
@@ -5516,9 +5518,9 @@ export interface Mutation {
   logout: boolean;
   /** Register a Customer account with the given credentials */
   registerCustomerAccount: boolean;
-  /** Verify a Customer email address with the token sent to that address */
+  /** Verify a Customer email address with the token sent to that address. Only applicable if `authOptions.requireVerification` is set to true. */
   verifyCustomerAccount: LoginResult;
-  /** Regenerate and send a verification token for a new Customer registration */
+  /** Regenerate and send a verification token for a new Customer registration. Only applicable if `authOptions.requireVerification` is set to true. */
   refreshCustomerVerification: boolean;
   /** Create a new Channel */
   createChannel: Channel;

Некоторые файлы не были показаны из-за большого количества измененных файлов