@labdigital/commercetools-mock 2.26.1 → 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/dist/index.cjs +65 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +65 -52
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/password.ts +12 -5
- package/src/repositories/customer/index.ts +52 -5
- package/src/repositories/my-customer.ts +1 -41
- package/src/services/customer.test.ts +62 -2
- package/src/services/customer.ts +31 -18
- package/src/services/my-customer.ts +1 -1
package/package.json
CHANGED
package/src/lib/password.ts
CHANGED
|
@@ -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(
|
|
17
|
-
|
|
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;
|
|
@@ -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(
|
|
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
|
-
|
|
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
|
+
});
|
package/src/services/customer.ts
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
101
|
+
const customer = this.repository.passwordReset(
|
|
102
102
|
getRepositoryContext(request),
|
|
103
103
|
request.body,
|
|
104
104
|
);
|