@labdigital/commercetools-mock 2.23.1 → 2.26.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 +82 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/password.ts +15 -0
- package/src/repositories/customer/actions.ts +18 -0
- package/src/repositories/customer/index.ts +30 -1
- package/src/repositories/my-customer.ts +41 -1
- package/src/services/customer.test.ts +40 -0
- package/src/services/customer.ts +9 -0
- package/src/services/my-customer.test.ts +47 -0
- package/src/services/my-customer.ts +10 -0
package/package.json
CHANGED
package/src/lib/password.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Customer } from "@commercetools/platform-sdk";
|
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
3
|
|
|
4
4
|
const PWRESET_SECRET = "pwreset";
|
|
5
|
+
const EMAIL_VERIFY_SECRET = "emailverifysecret";
|
|
5
6
|
|
|
6
7
|
export const validatePassword = (
|
|
7
8
|
clearPassword: string,
|
|
@@ -16,6 +17,11 @@ export const createPasswordResetToken = (customer: Customer) =>
|
|
|
16
17
|
"base64",
|
|
17
18
|
);
|
|
18
19
|
|
|
20
|
+
export const createEmailVerifyToken = (customer: Customer) =>
|
|
21
|
+
Buffer.from(`${customer.id}:${EMAIL_VERIFY_SECRET}:${uuidv4()}`).toString(
|
|
22
|
+
"base64",
|
|
23
|
+
);
|
|
24
|
+
|
|
19
25
|
export const validatePasswordResetToken = (token: string) => {
|
|
20
26
|
const items = Buffer.from(token, "base64").toString("utf-8").split(":");
|
|
21
27
|
const [customerId, secret] = items;
|
|
@@ -25,3 +31,12 @@ export const validatePasswordResetToken = (token: string) => {
|
|
|
25
31
|
|
|
26
32
|
return customerId;
|
|
27
33
|
};
|
|
34
|
+
export const validateEmailVerifyToken = (token: string) => {
|
|
35
|
+
const items = Buffer.from(token, "base64").toString("utf-8").split(":");
|
|
36
|
+
const [customerId, secret] = items;
|
|
37
|
+
if (secret !== EMAIL_VERIFY_SECRET) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return customerId;
|
|
42
|
+
};
|
|
@@ -6,9 +6,11 @@ import type {
|
|
|
6
6
|
CustomerSetCompanyNameAction,
|
|
7
7
|
CustomerSetCustomFieldAction,
|
|
8
8
|
CustomerSetCustomerNumberAction,
|
|
9
|
+
CustomerSetExternalIdAction,
|
|
9
10
|
CustomerSetFirstNameAction,
|
|
10
11
|
CustomerSetKeyAction,
|
|
11
12
|
CustomerSetLastNameAction,
|
|
13
|
+
CustomerSetLocaleAction,
|
|
12
14
|
CustomerSetSalutationAction,
|
|
13
15
|
CustomerSetVatIdAction,
|
|
14
16
|
CustomerUpdateAction,
|
|
@@ -141,6 +143,14 @@ export class CustomerUpdateHandler
|
|
|
141
143
|
resource.custom.fields[name] = value;
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
setExternalId(
|
|
147
|
+
_context: RepositoryContext,
|
|
148
|
+
resource: Writable<Customer>,
|
|
149
|
+
{ externalId }: CustomerSetExternalIdAction,
|
|
150
|
+
) {
|
|
151
|
+
resource.externalId = externalId;
|
|
152
|
+
}
|
|
153
|
+
|
|
144
154
|
setFirstName(
|
|
145
155
|
_context: RepositoryContext,
|
|
146
156
|
resource: Writable<Customer>,
|
|
@@ -165,6 +175,14 @@ export class CustomerUpdateHandler
|
|
|
165
175
|
resource.lastName = lastName;
|
|
166
176
|
}
|
|
167
177
|
|
|
178
|
+
setLocale(
|
|
179
|
+
_context: RepositoryContext,
|
|
180
|
+
resource: Writable<Customer>,
|
|
181
|
+
{ locale }: CustomerSetLocaleAction,
|
|
182
|
+
) {
|
|
183
|
+
resource.locale = locale;
|
|
184
|
+
}
|
|
185
|
+
|
|
168
186
|
setSalutation(
|
|
169
187
|
_context: RepositoryContext,
|
|
170
188
|
resource: Writable<Customer>,
|
|
@@ -7,7 +7,11 @@ import type {
|
|
|
7
7
|
} from "@commercetools/platform-sdk";
|
|
8
8
|
import { CommercetoolsError } from "~src/exceptions";
|
|
9
9
|
import { generateRandomString, getBaseResourceProperties } from "~src/helpers";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createEmailVerifyToken,
|
|
12
|
+
createPasswordResetToken,
|
|
13
|
+
hashPassword,
|
|
14
|
+
} from "~src/lib/password";
|
|
11
15
|
import { AbstractStorage } from "~src/storage/abstract";
|
|
12
16
|
import {
|
|
13
17
|
AbstractResourceRepository,
|
|
@@ -110,4 +114,29 @@ export class CustomerRepository extends AbstractResourceRepository<"customer"> {
|
|
|
110
114
|
value: token,
|
|
111
115
|
};
|
|
112
116
|
}
|
|
117
|
+
|
|
118
|
+
verifyEmailToken(context: RepositoryContext, id: string): CustomerToken {
|
|
119
|
+
const results = this._storage.query(context.projectKey, this.getTypeId(), {
|
|
120
|
+
where: [`id="${id.toLocaleLowerCase()}"`],
|
|
121
|
+
});
|
|
122
|
+
if (results.count === 0) {
|
|
123
|
+
throw new CommercetoolsError<ResourceNotFoundError>({
|
|
124
|
+
code: "ResourceNotFound",
|
|
125
|
+
message: `The Customer with ID '${id}' was not found.`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const expiresAt = new Date(Date.now() + 30 * 60);
|
|
129
|
+
const customer = results.results[0] as Customer;
|
|
130
|
+
const rest = getBaseResourceProperties();
|
|
131
|
+
|
|
132
|
+
const token = createEmailVerifyToken(customer);
|
|
133
|
+
return {
|
|
134
|
+
id: rest.id,
|
|
135
|
+
createdAt: rest.createdAt,
|
|
136
|
+
lastModifiedAt: rest.lastModifiedAt,
|
|
137
|
+
customerId: customer.id,
|
|
138
|
+
expiresAt: expiresAt.toISOString(),
|
|
139
|
+
value: token,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
113
142
|
}
|
|
@@ -2,11 +2,16 @@ import {
|
|
|
2
2
|
Customer,
|
|
3
3
|
InvalidCurrentPasswordError,
|
|
4
4
|
MyCustomerChangePassword,
|
|
5
|
+
MyCustomerEmailVerify,
|
|
5
6
|
ResourceNotFoundError,
|
|
6
7
|
type MyCustomerResetPassword,
|
|
7
8
|
} from "@commercetools/platform-sdk";
|
|
8
9
|
import { CommercetoolsError } from "~src/exceptions";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
hashPassword,
|
|
12
|
+
validateEmailVerifyToken,
|
|
13
|
+
validatePasswordResetToken,
|
|
14
|
+
} from "../lib/password";
|
|
10
15
|
import { Writable } from "../types";
|
|
11
16
|
import { type RepositoryContext } from "./abstract";
|
|
12
17
|
import { CustomerRepository } from "./customer";
|
|
@@ -45,6 +50,41 @@ export class MyCustomerRepository extends CustomerRepository {
|
|
|
45
50
|
return customer;
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
confirmEmail(
|
|
54
|
+
context: RepositoryContext,
|
|
55
|
+
resetPassword: MyCustomerEmailVerify,
|
|
56
|
+
) {
|
|
57
|
+
const { tokenValue } = resetPassword;
|
|
58
|
+
|
|
59
|
+
const customerId = validateEmailVerifyToken(tokenValue);
|
|
60
|
+
if (!customerId) {
|
|
61
|
+
throw new CommercetoolsError<ResourceNotFoundError>({
|
|
62
|
+
code: "ResourceNotFound",
|
|
63
|
+
message: `The Customer with ID 'Token(${tokenValue})' was not found.`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const customer = this._storage.get(
|
|
68
|
+
context.projectKey,
|
|
69
|
+
"customer",
|
|
70
|
+
customerId,
|
|
71
|
+
) as Writable<Customer> | undefined;
|
|
72
|
+
|
|
73
|
+
if (!customer) {
|
|
74
|
+
throw new CommercetoolsError<ResourceNotFoundError>({
|
|
75
|
+
code: "ResourceNotFound",
|
|
76
|
+
message: `The Customer with ID 'Token(${tokenValue})' was not found.`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
customer.isEmailVerified = true;
|
|
81
|
+
customer.version += 1;
|
|
82
|
+
|
|
83
|
+
// Update storage
|
|
84
|
+
this._storage.add(context.projectKey, "customer", customer);
|
|
85
|
+
return customer;
|
|
86
|
+
}
|
|
87
|
+
|
|
48
88
|
deleteMe(context: RepositoryContext): Customer | undefined {
|
|
49
89
|
// grab the first customer you can find for now. In the future we should
|
|
50
90
|
// use the customer id from the scope of the token
|
|
@@ -177,6 +177,26 @@ describe("Customer Update Actions", () => {
|
|
|
177
177
|
expect(response.body.custom.fields.isValidCouponCode).toBe(false);
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
test("setExternalId", async () => {
|
|
181
|
+
assert(customer, "customer not created");
|
|
182
|
+
|
|
183
|
+
customer = {
|
|
184
|
+
...customer,
|
|
185
|
+
firstName: "John",
|
|
186
|
+
};
|
|
187
|
+
ctMock.project("dummy").add("customer", customer);
|
|
188
|
+
|
|
189
|
+
const response = await supertest(ctMock.app)
|
|
190
|
+
.post(`/dummy/customers/${customer.id}`)
|
|
191
|
+
.send({
|
|
192
|
+
version: 1,
|
|
193
|
+
actions: [{ action: "setExternalId", externalId: "123-xx-123" }],
|
|
194
|
+
});
|
|
195
|
+
expect(response.status).toBe(200);
|
|
196
|
+
expect(response.body.version).toBe(2);
|
|
197
|
+
expect(response.body.externalId).toBe("123-xx-123");
|
|
198
|
+
});
|
|
199
|
+
|
|
180
200
|
test("setFirstName", async () => {
|
|
181
201
|
assert(customer, "customer not created");
|
|
182
202
|
|
|
@@ -217,6 +237,26 @@ describe("Customer Update Actions", () => {
|
|
|
217
237
|
expect(response.body.lastName).toBe("Smith");
|
|
218
238
|
});
|
|
219
239
|
|
|
240
|
+
test("setLocale", async () => {
|
|
241
|
+
assert(customer, "customer not created");
|
|
242
|
+
|
|
243
|
+
customer = {
|
|
244
|
+
...customer,
|
|
245
|
+
salutation: "Mr.",
|
|
246
|
+
};
|
|
247
|
+
ctMock.project("dummy").add("customer", customer);
|
|
248
|
+
|
|
249
|
+
const response = await supertest(ctMock.app)
|
|
250
|
+
.post(`/dummy/customers/${customer.id}`)
|
|
251
|
+
.send({
|
|
252
|
+
version: 1,
|
|
253
|
+
actions: [{ action: "setLocale", locale: "de-DE" }],
|
|
254
|
+
});
|
|
255
|
+
expect(response.status).toBe(200);
|
|
256
|
+
expect(response.body.version).toBe(2);
|
|
257
|
+
expect(response.body.locale).toBe("de-DE");
|
|
258
|
+
});
|
|
259
|
+
|
|
220
260
|
test("setSalutation", async () => {
|
|
221
261
|
assert(customer, "customer not created");
|
|
222
262
|
|
package/src/services/customer.ts
CHANGED
|
@@ -39,5 +39,14 @@ export class CustomerService extends AbstractService {
|
|
|
39
39
|
);
|
|
40
40
|
return response.status(200).send(token);
|
|
41
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
|
+
});
|
|
42
51
|
}
|
|
43
52
|
}
|
|
@@ -224,6 +224,53 @@ describe("/me", () => {
|
|
|
224
224
|
});
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
+
test("verify email flow", async () => {
|
|
228
|
+
const customer = {
|
|
229
|
+
...getBaseResourceProperties(),
|
|
230
|
+
id: "customer-uuid",
|
|
231
|
+
email: "user@example.com",
|
|
232
|
+
password: hashPassword("p4ssw0rd"),
|
|
233
|
+
addresses: [],
|
|
234
|
+
isEmailVerified: false,
|
|
235
|
+
authenticationMode: "Password", //default in Commercetools
|
|
236
|
+
version: 1,
|
|
237
|
+
};
|
|
238
|
+
ctMock.project("dummy").add("customer", customer);
|
|
239
|
+
|
|
240
|
+
const token = await supertest(ctMock.app)
|
|
241
|
+
.post("/dummy/customers/email-token")
|
|
242
|
+
.send({
|
|
243
|
+
id: "customer-uuid",
|
|
244
|
+
})
|
|
245
|
+
.then((response) => response.body as CustomerToken);
|
|
246
|
+
|
|
247
|
+
const response = await supertest(ctMock.app)
|
|
248
|
+
.post("/dummy/me/email/confirm")
|
|
249
|
+
.send({
|
|
250
|
+
tokenValue: token.value,
|
|
251
|
+
});
|
|
252
|
+
expect(response.status).toBe(200);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("fail verify email flow", async () => {
|
|
256
|
+
const response = await supertest(ctMock.app)
|
|
257
|
+
.post("/dummy/me/email/confirm")
|
|
258
|
+
.send({
|
|
259
|
+
tokenValue: "invalid-token",
|
|
260
|
+
});
|
|
261
|
+
expect(response.status).toBe(400);
|
|
262
|
+
expect(response.body).toEqual({
|
|
263
|
+
message: `The Customer with ID 'Token(invalid-token)' was not found.`,
|
|
264
|
+
statusCode: 400,
|
|
265
|
+
errors: [
|
|
266
|
+
{
|
|
267
|
+
code: "ResourceNotFound",
|
|
268
|
+
message: `The Customer with ID 'Token(invalid-token)' was not found.`,
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
227
274
|
test("setCustomField", async () => {
|
|
228
275
|
const response = await supertest(ctMock.app)
|
|
229
276
|
.post(`/dummy/me`)
|
|
@@ -35,6 +35,7 @@ export class MyCustomerService extends AbstractService {
|
|
|
35
35
|
router.post("/login", this.signIn.bind(this));
|
|
36
36
|
router.post("/password", this.changePassword.bind(this));
|
|
37
37
|
router.post("/password/reset", this.resetPassword.bind(this));
|
|
38
|
+
router.post("/email/confirm", this.emailConfirm.bind(this));
|
|
38
39
|
|
|
39
40
|
parent.use(`/${basePath}`, router);
|
|
40
41
|
}
|
|
@@ -105,6 +106,15 @@ export class MyCustomerService extends AbstractService {
|
|
|
105
106
|
return response.status(200).send(customer);
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
emailConfirm(request: Request, response: Response) {
|
|
110
|
+
const customer = this.repository.confirmEmail(
|
|
111
|
+
getRepositoryContext(request),
|
|
112
|
+
request.body,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return response.status(200).send(customer);
|
|
116
|
+
}
|
|
117
|
+
|
|
108
118
|
signIn(request: Request, response: Response) {
|
|
109
119
|
const { email, password } = request.body;
|
|
110
120
|
const encodedPassword = hashPassword(password);
|