@labdigital/commercetools-mock 2.26.0 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@labdigital/commercetools-mock",
3
- "version": "2.26.0",
3
+ "version": "2.27.0",
4
4
  "license": "MIT",
5
5
  "author": "Michael van Tellingen",
6
6
  "type": "module",
@@ -12,10 +12,10 @@ export const validatePassword = (
12
12
  export const hashPassword = (clearPassword: string) =>
13
13
  Buffer.from(clearPassword).toString("base64");
14
14
 
15
- export const createPasswordResetToken = (customer: Customer) =>
16
- Buffer.from(`${customer.id}:${PWRESET_SECRET}:${uuidv4()}`).toString(
17
- "base64",
18
- );
15
+ export const createPasswordResetToken = (customer: Customer, expiresAt: Date) =>
16
+ Buffer.from(
17
+ `${customer.id}:${PWRESET_SECRET}:${expiresAt.getTime()}`,
18
+ ).toString("base64");
19
19
 
20
20
  export const createEmailVerifyToken = (customer: Customer) =>
21
21
  Buffer.from(`${customer.id}:${EMAIL_VERIFY_SECRET}:${uuidv4()}`).toString(
@@ -24,13 +24,20 @@ export const createEmailVerifyToken = (customer: Customer) =>
24
24
 
25
25
  export const validatePasswordResetToken = (token: string) => {
26
26
  const items = Buffer.from(token, "base64").toString("utf-8").split(":");
27
- const [customerId, secret] = items;
27
+ const [customerId, secret, time] = items;
28
+
28
29
  if (secret !== PWRESET_SECRET) {
29
30
  return undefined;
30
31
  }
31
32
 
33
+ // Check if the token is expired
34
+ if (parseInt(time) < new Date().getTime()) {
35
+ return undefined;
36
+ }
37
+
32
38
  return customerId;
33
39
  };
40
+
34
41
  export const validateEmailVerifyToken = (token: string) => {
35
42
  const items = Buffer.from(token, "base64").toString("utf-8").split(":");
36
43
  const [customerId, secret] = items;
@@ -5,6 +5,7 @@ import type {
5
5
  CustomerSetAuthenticationModeAction,
6
6
  CustomerSetCompanyNameAction,
7
7
  CustomerSetCustomFieldAction,
8
+ CustomerSetCustomTypeAction,
8
9
  CustomerSetCustomerNumberAction,
9
10
  CustomerSetExternalIdAction,
10
11
  CustomerSetFirstNameAction,
@@ -25,7 +26,7 @@ import {
25
26
  UpdateHandlerInterface,
26
27
  type RepositoryContext,
27
28
  } from "../abstract";
28
- import { createAddress } from "../helpers";
29
+ import { createAddress, createCustomFields } from "../helpers";
29
30
 
30
31
  export class CustomerUpdateHandler
31
32
  extends AbstractUpdateHandler
@@ -143,6 +144,22 @@ export class CustomerUpdateHandler
143
144
  resource.custom.fields[name] = value;
144
145
  }
145
146
 
147
+ setCustomType(
148
+ context: RepositoryContext,
149
+ resource: Writable<Customer>,
150
+ { type, fields }: CustomerSetCustomTypeAction,
151
+ ) {
152
+ if (type) {
153
+ resource.custom = createCustomFields(
154
+ { type, fields },
155
+ context.projectKey,
156
+ this._storage,
157
+ );
158
+ } else {
159
+ resource.custom = undefined;
160
+ }
161
+ }
162
+
146
163
  setExternalId(
147
164
  _context: RepositoryContext,
148
165
  resource: Writable<Customer>,
@@ -1,8 +1,11 @@
1
1
  import type {
2
2
  Customer,
3
+ CustomerCreatePasswordResetToken,
3
4
  CustomerDraft,
5
+ CustomerResetPassword,
4
6
  CustomerToken,
5
7
  DuplicateFieldError,
8
+ MyCustomerResetPassword,
6
9
  ResourceNotFoundError,
7
10
  } from "@commercetools/platform-sdk";
8
11
  import { CommercetoolsError } from "~src/exceptions";
@@ -11,8 +14,10 @@ import {
11
14
  createEmailVerifyToken,
12
15
  createPasswordResetToken,
13
16
  hashPassword,
17
+ validatePasswordResetToken,
14
18
  } from "~src/lib/password";
15
19
  import { AbstractStorage } from "~src/storage/abstract";
20
+ import { Writable } from "~src/types";
16
21
  import {
17
22
  AbstractResourceRepository,
18
23
  type RepositoryContext,
@@ -90,21 +95,28 @@ export class CustomerRepository extends AbstractResourceRepository<"customer"> {
90
95
  return this.saveNew(context, resource);
91
96
  }
92
97
 
93
- passwordResetToken(context: RepositoryContext, email: string): CustomerToken {
98
+ passwordResetToken(
99
+ context: RepositoryContext,
100
+ request: CustomerCreatePasswordResetToken,
101
+ ): CustomerToken {
94
102
  const results = this._storage.query(context.projectKey, this.getTypeId(), {
95
- where: [`email="${email.toLocaleLowerCase()}"`],
103
+ where: [`email="${request.email.toLocaleLowerCase()}"`],
96
104
  });
97
105
  if (results.count === 0) {
98
106
  throw new CommercetoolsError<ResourceNotFoundError>({
99
107
  code: "ResourceNotFound",
100
- message: `The Customer with ID '${email}' was not found.`,
108
+ message: `The Customer with ID '${request.email}' was not found.`,
101
109
  });
102
110
  }
103
- const expiresAt = new Date(Date.now() + 30 * 60);
111
+
112
+ const ttlMinutes = request.ttlMinutes ?? 34560; // 34560 is CT default
113
+
114
+ const expiresAt = new Date(new Date().getTime() + ttlMinutes * 60 * 1000);
104
115
  const customer = results.results[0] as Customer;
105
116
  const rest = getBaseResourceProperties();
106
117
 
107
- const token = createPasswordResetToken(customer);
118
+ const token = createPasswordResetToken(customer, expiresAt);
119
+
108
120
  return {
109
121
  id: rest.id,
110
122
  createdAt: rest.createdAt,
@@ -115,6 +127,41 @@ export class CustomerRepository extends AbstractResourceRepository<"customer"> {
115
127
  };
116
128
  }
117
129
 
130
+ passwordReset(
131
+ context: RepositoryContext,
132
+ resetPassword: CustomerResetPassword | MyCustomerResetPassword,
133
+ ) {
134
+ const { newPassword, tokenValue } = resetPassword;
135
+
136
+ const customerId = validatePasswordResetToken(tokenValue);
137
+ if (!customerId) {
138
+ throw new CommercetoolsError<ResourceNotFoundError>({
139
+ code: "ResourceNotFound",
140
+ message: `The Customer with ID 'Token(${tokenValue})' was not found.`,
141
+ });
142
+ }
143
+
144
+ const customer = this._storage.get(
145
+ context.projectKey,
146
+ "customer",
147
+ customerId,
148
+ ) as Writable<Customer> | undefined;
149
+
150
+ if (!customer) {
151
+ throw new CommercetoolsError<ResourceNotFoundError>({
152
+ code: "ResourceNotFound",
153
+ message: `The Customer with ID 'Token(${tokenValue})' was not found.`,
154
+ });
155
+ }
156
+
157
+ customer.password = hashPassword(newPassword);
158
+ customer.version += 1;
159
+
160
+ // Update storage
161
+ this._storage.add(context.projectKey, "customer", customer);
162
+ return customer;
163
+ }
164
+
118
165
  verifyEmailToken(context: RepositoryContext, id: string): CustomerToken {
119
166
  const results = this._storage.query(context.projectKey, this.getTypeId(), {
120
167
  where: [`id="${id.toLocaleLowerCase()}"`],
@@ -4,14 +4,9 @@ import {
4
4
  MyCustomerChangePassword,
5
5
  MyCustomerEmailVerify,
6
6
  ResourceNotFoundError,
7
- type MyCustomerResetPassword,
8
7
  } from "@commercetools/platform-sdk";
9
8
  import { CommercetoolsError } from "~src/exceptions";
10
- import {
11
- hashPassword,
12
- validateEmailVerifyToken,
13
- validatePasswordResetToken,
14
- } from "../lib/password";
9
+ import { hashPassword, validateEmailVerifyToken } from "../lib/password";
15
10
  import { Writable } from "../types";
16
11
  import { type RepositoryContext } from "./abstract";
17
12
  import { CustomerRepository } from "./customer";
@@ -116,39 +111,4 @@ export class MyCustomerRepository extends CustomerRepository {
116
111
 
117
112
  return;
118
113
  }
119
-
120
- resetPassword(
121
- context: RepositoryContext,
122
- resetPassword: MyCustomerResetPassword,
123
- ) {
124
- const { newPassword, tokenValue } = resetPassword;
125
-
126
- const customerId = validatePasswordResetToken(tokenValue);
127
- if (!customerId) {
128
- throw new CommercetoolsError<ResourceNotFoundError>({
129
- code: "ResourceNotFound",
130
- message: `The Customer with ID 'Token(${tokenValue})' was not found.`,
131
- });
132
- }
133
-
134
- const customer = this._storage.get(
135
- context.projectKey,
136
- "customer",
137
- customerId,
138
- ) as Writable<Customer> | undefined;
139
-
140
- if (!customer) {
141
- throw new CommercetoolsError<ResourceNotFoundError>({
142
- code: "ResourceNotFound",
143
- message: `The Customer with ID 'Token(${tokenValue})' was not found.`,
144
- });
145
- }
146
-
147
- customer.password = hashPassword(newPassword);
148
- customer.version += 1;
149
-
150
- // Update storage
151
- this._storage.add(context.projectKey, "customer", customer);
152
- return customer;
153
- }
154
114
  }
@@ -1,11 +1,13 @@
1
- import { Customer } from "@commercetools/platform-sdk";
1
+ import { Customer, CustomerToken } from "@commercetools/platform-sdk";
2
2
  import assert from "assert";
3
3
  import supertest from "supertest";
4
4
  import { afterEach, beforeEach, describe, expect, test } from "vitest";
5
+ import { hashPassword } from "~src/lib/password";
5
6
  import { CommercetoolsMock, getBaseResourceProperties } from "../index";
6
7
 
8
+ const ctMock = new CommercetoolsMock();
9
+
7
10
  describe("Customer Update Actions", () => {
8
- const ctMock = new CommercetoolsMock();
9
11
  let customer: Customer | undefined;
10
12
 
11
13
  beforeEach(async () => {
@@ -447,3 +449,61 @@ describe("Customer Update Actions", () => {
447
449
  expect(response.body.key).toBe("C001");
448
450
  });
449
451
  });
452
+
453
+ describe("Customer Password Reset", () => {
454
+ afterEach(() => {
455
+ ctMock.clear();
456
+ });
457
+
458
+ beforeEach(() => {
459
+ ctMock.project("dummy").add("customer", {
460
+ id: "123",
461
+ createdAt: "2021-03-18T14:00:00.000Z",
462
+ version: 2,
463
+ lastModifiedAt: "2021-03-18T14:00:00.000Z",
464
+ email: "foo@example.org",
465
+ password: hashPassword("p4ssw0rd"),
466
+ addresses: [],
467
+ isEmailVerified: true,
468
+ authenticationMode: "password",
469
+ custom: { type: { typeId: "type", id: "" }, fields: {} },
470
+ });
471
+ });
472
+
473
+ test("reset password flow", async () => {
474
+ const token = await supertest(ctMock.app)
475
+ .post("/dummy/customers/password-token")
476
+ .send({
477
+ email: "foo@example.org",
478
+ })
479
+ .then((response) => response.body as CustomerToken);
480
+
481
+ const response = await supertest(ctMock.app)
482
+ .post("/dummy/customers/password/reset")
483
+ .send({
484
+ tokenValue: token.value,
485
+ newPassword: "somethingNew",
486
+ });
487
+ expect(response.status).toBe(200);
488
+ });
489
+
490
+ test("fail reset password flow", async () => {
491
+ const response = await supertest(ctMock.app)
492
+ .post("/dummy/customers/password/reset")
493
+ .send({
494
+ tokenValue: "invalid-token",
495
+ newPassword: "somethingNew",
496
+ });
497
+ expect(response.status).toBe(400);
498
+ expect(response.body).toEqual({
499
+ message: `The Customer with ID 'Token(invalid-token)' was not found.`,
500
+ statusCode: 400,
501
+ errors: [
502
+ {
503
+ code: "ResourceNotFound",
504
+ message: `The Customer with ID 'Token(invalid-token)' was not found.`,
505
+ },
506
+ ],
507
+ });
508
+ });
509
+ });
@@ -16,6 +16,12 @@ export class CustomerService extends AbstractService {
16
16
  return "customers";
17
17
  }
18
18
 
19
+ extraRoutes(parent: Router) {
20
+ parent.post("/password-token", this.passwordResetToken.bind(this));
21
+ parent.post("/password/reset", this.passwordReset.bind(this));
22
+ parent.post("/email-token", this.confirmEmailToken.bind(this));
23
+ }
24
+
19
25
  post(request: Request, response: Response) {
20
26
  const draft = request.body;
21
27
  const resource = this.repository.create(
@@ -30,23 +36,30 @@ export class CustomerService extends AbstractService {
30
36
  return response.status(this.createStatusCode).send(result);
31
37
  }
32
38
 
33
- extraRoutes(parent: Router) {
34
- parent.post("/password-token", (request, response) => {
35
- const email = request.body.email;
36
- const token = this.repository.passwordResetToken(
37
- getRepositoryContext(request),
38
- email,
39
- );
40
- return response.status(200).send(token);
41
- });
42
-
43
- parent.post("/email-token", (request, response) => {
44
- const id = request.body.id;
45
- const token = this.repository.verifyEmailToken(
46
- getRepositoryContext(request),
47
- id,
48
- );
49
- return response.status(200).send(token);
50
- });
39
+ passwordResetToken(request: Request, response: Response) {
40
+ const customer = this.repository.passwordResetToken(
41
+ getRepositoryContext(request),
42
+ request.body,
43
+ );
44
+
45
+ return response.status(200).send(customer);
46
+ }
47
+
48
+ passwordReset(request: Request, response: Response) {
49
+ const customer = this.repository.passwordReset(
50
+ getRepositoryContext(request),
51
+ request.body,
52
+ );
53
+
54
+ return response.status(200).send(customer);
55
+ }
56
+
57
+ confirmEmailToken(request: Request, response: Response) {
58
+ const id = request.body.id;
59
+ const token = this.repository.verifyEmailToken(
60
+ getRepositoryContext(request),
61
+ id,
62
+ );
63
+ return response.status(200).send(token);
51
64
  }
52
65
  }
@@ -98,7 +98,7 @@ export class MyCustomerService extends AbstractService {
98
98
  }
99
99
 
100
100
  resetPassword(request: Request, response: Response) {
101
- const customer = this.repository.resetPassword(
101
+ const customer = this.repository.passwordReset(
102
102
  getRepositoryContext(request),
103
103
  request.body,
104
104
  );