Browse Source

feat(server): Add updateCustomerPassword mutation & tests

Michael Bromley 7 years ago
parent
commit
2c26d30d07

File diff suppressed because it is too large
+ 0 - 0
schema-shop.json


+ 41 - 0
schema.json

@@ -16207,6 +16207,47 @@
             },
             "isDeprecated": false,
             "deprecationReason": null
+          },
+          {
+            "name": "updateCustomerPassword",
+            "description": null,
+            "args": [
+              {
+                "name": "currentPassword",
+                "description": null,
+                "type": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "SCALAR",
+                    "name": "String",
+                    "ofType": null
+                  }
+                },
+                "defaultValue": null
+              },
+              {
+                "name": "newPassword",
+                "description": null,
+                "type": {
+                  "kind": "NON_NULL",
+                  "name": null,
+                  "ofType": {
+                    "kind": "SCALAR",
+                    "name": "String",
+                    "ofType": null
+                  }
+                },
+                "defaultValue": null
+              }
+            ],
+            "type": {
+              "kind": "SCALAR",
+              "name": "Boolean",
+              "ofType": null
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
           }
         ],
         "inputFields": null,

+ 23 - 0
server/e2e/shop-customer.e2e-spec.ts

@@ -162,6 +162,23 @@ describe('Shop customers', () => {
                 await shopClient.query(DELETE_ADDRESS, { id: 'T_2' });
             }, 'You are not currently authorized to perform this action'),
         );
+
+        it(
+            'updatePassword fails with incorrect current password',
+            assertThrowsWithMessage(async () => {
+                await shopClient.query(UPDATE_PASSWORD, { old: 'wrong', new: 'test2' });
+            }, 'The credentials did not match. Please check and try again'),
+        );
+
+        it('updatePassword works', async () => {
+            const response = await shopClient.query(UPDATE_PASSWORD, { old: 'test', new: 'test2' });
+
+            expect(response.updateCustomerPassword).toBe(true);
+
+            // Log out and log in with new password
+            const loginResult = await shopClient.asUserWithCredentials(customer.emailAddress, 'test2');
+            expect(loginResult.user.identifier).toBe(customer.emailAddress);
+        });
     });
 });
 
@@ -202,3 +219,9 @@ const UPDATE_CUSTOMER = gql`
     }
     ${CUSTOMER_FRAGMENT}
 `;
+
+const UPDATE_PASSWORD = gql`
+    mutation UpdatePassword($old: String!, $new: String!) {
+        updateCustomerPassword(currentPassword: $old, newPassword: $new)
+    }
+`;

+ 1 - 0
server/mock-data/simple-graphql-client.ts

@@ -173,6 +173,7 @@ export class SimpleGraphQLClient {
                 password,
             },
         );
+        return result.login;
     }
 
     async asSuperAdmin() {

+ 20 - 2
server/src/api/resolvers/base/base-auth.resolver.ts

@@ -1,11 +1,10 @@
 import { Request, Response } from 'express';
 
 import { LoginMutationArgs, LoginResult } from '../../../../../shared/generated-types';
+import { InternalServerError } from '../../../common/error/errors';
 import { ConfigService } from '../../../config/config.service';
 import { User } from '../../../entity/user/user.entity';
 import { AuthService } from '../../../service/services/auth.service';
-import { ChannelService } from '../../../service/services/channel.service';
-import { CustomerService } from '../../../service/services/customer.service';
 import { UserService } from '../../../service/services/user.service';
 import { extractAuthToken } from '../../common/extract-auth-token';
 import { RequestContext } from '../../common/request-context';
@@ -78,6 +77,25 @@ export class BaseAuthResolver {
         };
     }
 
+    /**
+     * Updates the password of an existing User.
+     */
+    protected async updatePassword(
+        ctx: RequestContext,
+        currentPassword: string,
+        newPassword: string,
+    ): Promise<boolean> {
+        const { activeUserId } = ctx;
+        if (!activeUserId) {
+            throw new InternalServerError(`error.no-active-user-id`);
+        }
+        const user = await this.userService.getUserById(activeUserId);
+        if (!user) {
+            throw new InternalServerError(`error.no-active-user-id`);
+        }
+        return this.userService.updatePassword(user, currentPassword, newPassword);
+    }
+
     /**
      * Exposes a subset of the User properties which we want to expose to the public API.
      */

+ 10 - 0
server/src/api/resolvers/shop/shop-auth.resolver.ts

@@ -7,6 +7,7 @@ import {
     Permission,
     RefreshCustomerVerificationMutationArgs,
     RegisterCustomerAccountMutationArgs,
+    UpdateCustomerPasswordMutationArgs,
     VerifyCustomerAccountMutationArgs,
 } from '../../../../../shared/generated-shop-types';
 import { VerificationTokenError } from '../../../common/error/errors';
@@ -99,4 +100,13 @@ export class ShopAuthResolver extends BaseAuthResolver {
     ) {
         return this.customerService.refreshVerificationToken(ctx, args.emailAddress).then(() => true);
     }
+
+    @Mutation()
+    @Allow(Permission.Owner)
+    async updateCustomerPassword(
+        @Ctx() ctx: RequestContext,
+        @Args() args: UpdateCustomerPasswordMutationArgs,
+    ): Promise<boolean> {
+        return super.updatePassword(ctx, args.currentPassword, args.newPassword);
+    }
 }

+ 2 - 0
server/src/api/schema/shop-api/shop.api.graphql

@@ -41,6 +41,8 @@ type Mutation {
     deleteCustomerAddress(id: ID!): Boolean!
     "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!
+    "Update the password of the active Customer"
+    updateCustomerPassword(currentPassword: String!, newPassword: String!): Boolean
 }
 
 input RegisterCustomerInput {

+ 11 - 1
server/src/service/services/user.service.ts

@@ -3,7 +3,7 @@ import { InjectConnection } from '@nestjs/typeorm';
 import { Connection } from 'typeorm';
 
 import { ID } from '../../../../shared/shared-types';
-import { VerificationTokenExpiredError } from '../../common/error/errors';
+import { UnauthorizedError, VerificationTokenExpiredError } from '../../common/error/errors';
 import { ConfigService } from '../../config/config.service';
 import { User } from '../../entity/user/user.entity';
 import { PasswordCiper } from '../helpers/password-cipher/password-ciper';
@@ -86,4 +86,14 @@ export class UserService {
             }
         }
     }
+
+    async updatePassword(user: User, currentPassword: string, newPassword: string): Promise<boolean> {
+        const matches = await this.passwordCipher.check(currentPassword, user.passwordHash);
+        if (!matches) {
+            throw new UnauthorizedError();
+        }
+        user.passwordHash = await this.passwordCipher.hash(newPassword);
+        await this.connection.getRepository(User).save(user);
+        return true;
+    }
 }

+ 8 - 1
shared/generated-shop-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-02-25T17:30:36+01:00
+// Generated in 2019-02-26T09:46:27+01:00
 export type Maybe<T> = T | null;
 
 export interface OrderListOptions {
@@ -1523,6 +1523,8 @@ export interface Mutation {
     deleteCustomerAddress: boolean;
     /** Verify a Customer email address with the token sent to that address. Only applicable if `authOptions.requireVerification` is set to true. */
     verifyCustomerAccount: LoginResult;
+
+    updateCustomerPassword?: Maybe<boolean>;
 }
 
 export interface LoginResult {
@@ -1769,3 +1771,8 @@ export interface VerifyCustomerAccountMutationArgs {
 
     password: string;
 }
+export interface UpdateCustomerPasswordMutationArgs {
+    currentPassword: string;
+
+    newPassword: string;
+}

+ 1 - 1
shared/generated-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-02-25T17:30:37+01:00
+// Generated in 2019-02-26T09:46:29+01:00
 export type Maybe<T> = T | null;
 
 

Some files were not shown because too many files changed in this diff