@labdigital/commercetools-mock 1.11.0 → 2.1.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 +213 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.js +213 -37
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/constants.ts +2 -4
- package/src/ctMock.ts +90 -50
- package/src/exceptions.ts +4 -0
- package/src/index.test.ts +54 -20
- package/src/lib/password.ts +7 -0
- package/src/lib/proxy.ts +4 -4
- package/src/oauth/server.ts +137 -3
- package/src/oauth/store.ts +13 -0
- package/src/repositories/customer.ts +30 -8
- package/src/services/my-customer.ts +2 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@labdigital/commercetools-mock",
|
|
3
3
|
"author": "Michael van Tellingen",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "2.1.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
|
-
"node": ">=
|
|
22
|
+
"node": ">=18",
|
|
23
23
|
"pnpm": ">=8.6.5"
|
|
24
24
|
},
|
|
25
25
|
"packageManager": "pnpm@8.6.5",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"express": "^4.18.2",
|
|
40
40
|
"lodash.isequal": "^4.5.0",
|
|
41
41
|
"morgan": "^1.10.0",
|
|
42
|
-
"
|
|
42
|
+
"msw": "^2.0.0",
|
|
43
43
|
"supertest": "^6.3.3",
|
|
44
44
|
"uuid": "^9.0.0"
|
|
45
45
|
},
|
package/src/constants.ts
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
export const DEFAULT_API_HOSTNAME =
|
|
2
|
-
|
|
3
|
-
export const DEFAULT_AUTH_HOSTNAME =
|
|
4
|
-
/^https:\/\/auth\..*?\.commercetools.com:443$/
|
|
1
|
+
export const DEFAULT_API_HOSTNAME = 'https://api.*.commercetools.com'
|
|
2
|
+
export const DEFAULT_AUTH_HOSTNAME = 'https://auth.*.commercetools.com'
|
package/src/ctMock.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import nock from 'nock'
|
|
2
1
|
import express, { NextFunction, Request, Response } from 'express'
|
|
3
2
|
import supertest from 'supertest'
|
|
4
3
|
import morgan from 'morgan'
|
|
4
|
+
import { setupServer, SetupServer } from 'msw/node'
|
|
5
|
+
import { http, HttpResponse } from 'msw'
|
|
5
6
|
import { AbstractStorage, InMemoryStorage } from './storage/index.js'
|
|
6
7
|
import { Services } from './types.js'
|
|
7
8
|
import { CommercetoolsError } from './exceptions.js'
|
|
@@ -36,16 +37,15 @@ const DEFAULT_OPTIONS: CommercetoolsMockOptions = {
|
|
|
36
37
|
silent: false,
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
const _globalListeners: SetupServer[] = []
|
|
41
|
+
|
|
39
42
|
export class CommercetoolsMock {
|
|
40
43
|
public app: express.Express
|
|
41
44
|
public options: CommercetoolsMockOptions
|
|
42
45
|
|
|
43
46
|
private _storage: AbstractStorage
|
|
44
47
|
private _oauth2: OAuth2Server
|
|
45
|
-
private
|
|
46
|
-
auth: nock.Scope | undefined
|
|
47
|
-
api: nock.Scope | undefined
|
|
48
|
-
} = { auth: undefined, api: undefined }
|
|
48
|
+
private _mswServer: SetupServer | undefined = undefined
|
|
49
49
|
private _services: Services | null
|
|
50
50
|
private _repositories: RepositoryMap | null
|
|
51
51
|
private _projectService?: ProjectService
|
|
@@ -67,19 +67,17 @@ export class CommercetoolsMock {
|
|
|
67
67
|
|
|
68
68
|
start() {
|
|
69
69
|
// Order is important here when the hostnames match
|
|
70
|
-
this.
|
|
71
|
-
this.
|
|
70
|
+
this.clear()
|
|
71
|
+
this.startServer()
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
stop() {
|
|
75
|
-
this.
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
this._nockScopes.api?.persist(false)
|
|
79
|
-
this._nockScopes.api = undefined
|
|
75
|
+
this._mswServer?.close()
|
|
76
|
+
this._mswServer = undefined
|
|
80
77
|
}
|
|
81
78
|
|
|
82
79
|
clear() {
|
|
80
|
+
this._mswServer?.resetHandlers()
|
|
83
81
|
this._storage.clear()
|
|
84
82
|
}
|
|
85
83
|
|
|
@@ -108,6 +106,7 @@ export class CommercetoolsMock {
|
|
|
108
106
|
|
|
109
107
|
private createApp(options?: AppOptions): express.Express {
|
|
110
108
|
this._repositories = createRepositories(this._storage)
|
|
109
|
+
this._oauth2.setCustomerRepository(this._repositories.customer)
|
|
111
110
|
|
|
112
111
|
const app = express()
|
|
113
112
|
|
|
@@ -141,6 +140,14 @@ export class CommercetoolsMock {
|
|
|
141
140
|
|
|
142
141
|
app.use((err: Error, req: Request, resp: Response, next: NextFunction) => {
|
|
143
142
|
if (err instanceof CommercetoolsError) {
|
|
143
|
+
if (err.errors?.length > 0) {
|
|
144
|
+
return resp.status(err.statusCode).send({
|
|
145
|
+
statusCode: err.statusCode,
|
|
146
|
+
message: err.message,
|
|
147
|
+
errors: err.errors,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
144
151
|
return resp.status(err.statusCode).send({
|
|
145
152
|
statusCode: err.statusCode,
|
|
146
153
|
message: err.message,
|
|
@@ -157,48 +164,81 @@ export class CommercetoolsMock {
|
|
|
157
164
|
return app
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
private
|
|
161
|
-
|
|
167
|
+
private startServer() {
|
|
168
|
+
// Check if there are any other servers running
|
|
169
|
+
if (_globalListeners.length > 0) {
|
|
170
|
+
if (this._mswServer !== undefined) {
|
|
171
|
+
throw new Error('Server already started')
|
|
172
|
+
} else {
|
|
173
|
+
console.warn("Server wasn't stopped properly, clearing")
|
|
174
|
+
_globalListeners.forEach((listener) => listener.close())
|
|
175
|
+
}
|
|
176
|
+
}
|
|
162
177
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
.
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
.
|
|
176
|
-
|
|
178
|
+
const app = this.app
|
|
179
|
+
this._mswServer = setupServer(
|
|
180
|
+
http.post(`${this.options.authHost}/oauth/*`, async ({ request }) => {
|
|
181
|
+
const text = await request.text()
|
|
182
|
+
const url = new URL(request.url)
|
|
183
|
+
const res = await supertest(app)
|
|
184
|
+
.post(url.pathname + '?' + url.searchParams.toString())
|
|
185
|
+
.set(copyHeaders(request.headers))
|
|
186
|
+
.send(text)
|
|
187
|
+
|
|
188
|
+
return new HttpResponse(res.text, {
|
|
189
|
+
status: res.status,
|
|
190
|
+
headers: res.headers,
|
|
191
|
+
})
|
|
192
|
+
}),
|
|
193
|
+
http.get(`${this.options.apiHost}/*`, async ({ request }) => {
|
|
194
|
+
const body = await request.text()
|
|
195
|
+
const url = new URL(request.url)
|
|
196
|
+
const res = await supertest(app)
|
|
197
|
+
.get(url.pathname + '?' + url.searchParams.toString())
|
|
198
|
+
.set(copyHeaders(request.headers))
|
|
177
199
|
.send(body)
|
|
178
|
-
return
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
200
|
+
return new HttpResponse(res.text, {
|
|
201
|
+
status: res.status,
|
|
202
|
+
headers: res.headers,
|
|
203
|
+
})
|
|
204
|
+
}),
|
|
205
|
+
http.post(`${this.options.apiHost}/*`, async ({ request }) => {
|
|
206
|
+
const body = await request.text()
|
|
207
|
+
const url = new URL(request.url)
|
|
208
|
+
const res = await supertest(app)
|
|
209
|
+
.post(url.pathname + '?' + url.searchParams.toString())
|
|
210
|
+
.set(copyHeaders(request.headers))
|
|
211
|
+
.send(body)
|
|
212
|
+
return new HttpResponse(res.text, {
|
|
213
|
+
status: res.status,
|
|
214
|
+
headers: res.headers,
|
|
215
|
+
})
|
|
216
|
+
}),
|
|
217
|
+
http.delete(`${this.options.apiHost}/*`, async ({ request }) => {
|
|
218
|
+
const body = await request.text()
|
|
219
|
+
const url = new URL(request.url)
|
|
220
|
+
const res = await supertest(app)
|
|
221
|
+
.delete(url.pathname + '?' + url.searchParams.toString())
|
|
222
|
+
.set(copyHeaders(request.headers))
|
|
185
223
|
.send(body)
|
|
186
|
-
return [response.status, response.body]
|
|
187
|
-
})
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private mockAuthHost() {
|
|
191
|
-
const app = this.app
|
|
192
224
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const response = await supertest(app)
|
|
198
|
-
.post(uri + '?' + body)
|
|
199
|
-
.set(copyHeaders(this.req.headers))
|
|
200
|
-
.send()
|
|
201
|
-
return [response.status, response.body]
|
|
225
|
+
return new HttpResponse(res.text, {
|
|
226
|
+
status: res.status,
|
|
227
|
+
headers: res.headers,
|
|
228
|
+
})
|
|
202
229
|
})
|
|
230
|
+
)
|
|
231
|
+
this._mswServer.listen({
|
|
232
|
+
// We need to allow requests done by supertest
|
|
233
|
+
onUnhandledRequest: (request, print) => {
|
|
234
|
+
const url = new URL(request.url)
|
|
235
|
+
if (url.hostname === '127.0.0.1') {
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
print.error()
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
_globalListeners.push(this._mswServer)
|
|
203
243
|
}
|
|
204
244
|
}
|
package/src/exceptions.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
export abstract class BaseError {
|
|
2
2
|
abstract message: string
|
|
3
|
+
abstract errors?: BaseError[]
|
|
3
4
|
}
|
|
4
5
|
|
|
5
6
|
export class CommercetoolsError<T extends BaseError> extends Error {
|
|
6
7
|
info: T
|
|
7
8
|
statusCode: number
|
|
8
9
|
|
|
10
|
+
errors: BaseError[]
|
|
11
|
+
|
|
9
12
|
constructor(info: T, statusCode = 400) {
|
|
10
13
|
super(info.message)
|
|
11
14
|
this.info = info
|
|
12
15
|
this.statusCode = statusCode || 500
|
|
16
|
+
this.errors = info.errors ?? []
|
|
13
17
|
}
|
|
14
18
|
}
|
|
15
19
|
|
package/src/index.test.ts
CHANGED
|
@@ -1,27 +1,63 @@
|
|
|
1
1
|
import { type InvalidTokenError } from '@commercetools/platform-sdk'
|
|
2
2
|
import { CommercetoolsMock } from './index.js'
|
|
3
|
-
import {
|
|
4
|
-
import nock from 'nock'
|
|
3
|
+
import { expect, test } from 'vitest'
|
|
5
4
|
import got from 'got'
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
test('node:fetch client', async () => {
|
|
7
|
+
const ctMock = new CommercetoolsMock({
|
|
8
|
+
enableAuthentication: true,
|
|
9
|
+
validateCredentials: true,
|
|
10
|
+
apiHost: 'https://localhost',
|
|
11
|
+
authHost: 'https://localhost:8080',
|
|
12
|
+
})
|
|
13
|
+
ctMock.start()
|
|
14
|
+
|
|
15
|
+
const authHeader = 'Basic ' + Buffer.from('foo:bar').toString('base64')
|
|
16
|
+
let response = await fetch('https://localhost:8080/oauth/token', {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
20
|
+
Authorization: authHeader,
|
|
21
|
+
},
|
|
22
|
+
body: new URLSearchParams({
|
|
23
|
+
grant_type: 'client_credentials',
|
|
24
|
+
scope: 'manage_project:commercetools-node-mock',
|
|
25
|
+
}),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const authBody = await response.json()
|
|
29
|
+
expect(response.status).toBe(200)
|
|
30
|
+
|
|
31
|
+
const token = authBody.access_token
|
|
32
|
+
response = await fetch('https://localhost/my-project/orders', {
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `Bearer ${token}`,
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const body = await response.json()
|
|
39
|
+
expect(response.status).toBe(200)
|
|
40
|
+
expect(body).toStrictEqual({
|
|
41
|
+
count: 0,
|
|
42
|
+
total: 0,
|
|
43
|
+
offset: 0,
|
|
44
|
+
limit: 20,
|
|
45
|
+
results: [],
|
|
46
|
+
})
|
|
47
|
+
ctMock.stop()
|
|
14
48
|
})
|
|
15
49
|
|
|
16
|
-
test('
|
|
50
|
+
test('got client', async () => {
|
|
17
51
|
const ctMock = new CommercetoolsMock({
|
|
18
52
|
enableAuthentication: true,
|
|
19
53
|
validateCredentials: true,
|
|
54
|
+
apiHost: 'https://localhost',
|
|
55
|
+
authHost: 'https://localhost:8080',
|
|
20
56
|
})
|
|
21
57
|
ctMock.start()
|
|
22
58
|
|
|
23
59
|
let response = await got.post<{ access_token: string }>(
|
|
24
|
-
'https://
|
|
60
|
+
'https://localhost:8080/oauth/token',
|
|
25
61
|
{
|
|
26
62
|
searchParams: {
|
|
27
63
|
grant_type: 'client_credentials',
|
|
@@ -36,15 +72,12 @@ test('Default mock endpoints', async () => {
|
|
|
36
72
|
|
|
37
73
|
const token = response.body.access_token
|
|
38
74
|
expect(response.body.access_token).toBeDefined()
|
|
39
|
-
response = await got.get(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
responseType: 'json',
|
|
46
|
-
}
|
|
47
|
-
)
|
|
75
|
+
response = await got.get('https://localhost/my-project/orders', {
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${token}`,
|
|
78
|
+
},
|
|
79
|
+
responseType: 'json',
|
|
80
|
+
})
|
|
48
81
|
expect(response.statusCode).toBe(200)
|
|
49
82
|
expect(response.body).toStrictEqual({
|
|
50
83
|
count: 0,
|
|
@@ -192,6 +225,7 @@ test('apiHost mock proxy: querystring', async () => {
|
|
|
192
225
|
expand: 'custom.type',
|
|
193
226
|
},
|
|
194
227
|
})
|
|
228
|
+
|
|
195
229
|
expect(response.statusCode).toBe(200)
|
|
196
230
|
expect(response.body).toStrictEqual({
|
|
197
231
|
count: 0,
|
package/src/lib/proxy.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
export const copyHeaders = (headers:
|
|
2
|
-
const validHeaders = ['accept', 'host', 'authorization']
|
|
1
|
+
export const copyHeaders = (headers: Headers) => {
|
|
2
|
+
const validHeaders = ['accept', 'host', 'authorization', 'content-type']
|
|
3
3
|
const result: Record<string, string> = {}
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
for (const [key, value] of headers.entries()) {
|
|
6
6
|
if (validHeaders.includes(key.toLowerCase())) {
|
|
7
7
|
result[key] = value
|
|
8
8
|
}
|
|
9
|
-
}
|
|
9
|
+
}
|
|
10
10
|
|
|
11
11
|
return result
|
|
12
12
|
}
|
package/src/oauth/server.ts
CHANGED
|
@@ -10,18 +10,45 @@ import { CommercetoolsError, InvalidRequestError } from '../exceptions.js'
|
|
|
10
10
|
import { InvalidClientError, UnsupportedGrantType } from './errors.js'
|
|
11
11
|
import { OAuth2Store } from './store.js'
|
|
12
12
|
import { getBearerToken } from './helpers.js'
|
|
13
|
+
import { CustomerRepository } from '../repositories/customer.js'
|
|
14
|
+
import { hashPassword } from '../lib/password.js'
|
|
15
|
+
|
|
16
|
+
type AuthRequest = Request & {
|
|
17
|
+
credentials: {
|
|
18
|
+
clientId: string
|
|
19
|
+
clientSecret: string
|
|
20
|
+
}
|
|
21
|
+
}
|
|
13
22
|
|
|
14
23
|
export class OAuth2Server {
|
|
15
24
|
store: OAuth2Store
|
|
25
|
+
private customerRepository: CustomerRepository
|
|
16
26
|
|
|
17
27
|
constructor(options: { enabled: boolean; validate: boolean }) {
|
|
18
28
|
this.store = new OAuth2Store(options.validate)
|
|
19
29
|
}
|
|
20
30
|
|
|
31
|
+
setCustomerRepository(repository: CustomerRepository) {
|
|
32
|
+
this.customerRepository = repository
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
createRouter() {
|
|
22
36
|
const router = express.Router()
|
|
23
37
|
router.use(bodyParser.urlencoded({ extended: true }))
|
|
38
|
+
router.use(this.validateClientCredentials.bind(this))
|
|
24
39
|
router.post('/token', this.tokenHandler.bind(this))
|
|
40
|
+
router.post(
|
|
41
|
+
'/:projectKey/customers/token',
|
|
42
|
+
this.customerTokenHandler.bind(this)
|
|
43
|
+
)
|
|
44
|
+
router.post(
|
|
45
|
+
'/:projectKey/in-store/key=:storeKey/customers/token',
|
|
46
|
+
this.inStoreCustomerTokenHandler.bind(this)
|
|
47
|
+
)
|
|
48
|
+
router.post(
|
|
49
|
+
'/:projectKey/anonymous/token',
|
|
50
|
+
this.anonymousTokenHandler.bind(this)
|
|
51
|
+
)
|
|
25
52
|
return router
|
|
26
53
|
}
|
|
27
54
|
|
|
@@ -56,7 +83,12 @@ export class OAuth2Server {
|
|
|
56
83
|
next()
|
|
57
84
|
}
|
|
58
85
|
}
|
|
59
|
-
|
|
86
|
+
|
|
87
|
+
async validateClientCredentials(
|
|
88
|
+
request: AuthRequest,
|
|
89
|
+
response: Response,
|
|
90
|
+
next: NextFunction
|
|
91
|
+
) {
|
|
60
92
|
const authHeader = request.header('Authorization')
|
|
61
93
|
if (!authHeader) {
|
|
62
94
|
return next(
|
|
@@ -84,6 +116,19 @@ export class OAuth2Server {
|
|
|
84
116
|
)
|
|
85
117
|
}
|
|
86
118
|
|
|
119
|
+
request.credentials = {
|
|
120
|
+
clientId: credentials.name,
|
|
121
|
+
clientSecret: credentials.pass,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
next()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async tokenHandler(
|
|
128
|
+
request: AuthRequest,
|
|
129
|
+
response: Response,
|
|
130
|
+
next: NextFunction
|
|
131
|
+
) {
|
|
87
132
|
const grantType = request.query.grant_type || request.body.grant_type
|
|
88
133
|
if (!grantType) {
|
|
89
134
|
return next(
|
|
@@ -99,8 +144,15 @@ export class OAuth2Server {
|
|
|
99
144
|
|
|
100
145
|
if (grantType === 'client_credentials') {
|
|
101
146
|
const token = this.store.getClientToken(
|
|
102
|
-
credentials.
|
|
103
|
-
credentials.
|
|
147
|
+
request.credentials.clientId,
|
|
148
|
+
request.credentials.clientSecret,
|
|
149
|
+
request.query.scope?.toString()
|
|
150
|
+
)
|
|
151
|
+
return response.status(200).send(token)
|
|
152
|
+
} else if (grantType === 'refresh_token') {
|
|
153
|
+
const token = this.store.getClientToken(
|
|
154
|
+
request.credentials.clientId,
|
|
155
|
+
request.credentials.clientSecret,
|
|
104
156
|
request.query.scope?.toString()
|
|
105
157
|
)
|
|
106
158
|
return response.status(200).send(token)
|
|
@@ -116,4 +168,86 @@ export class OAuth2Server {
|
|
|
116
168
|
)
|
|
117
169
|
}
|
|
118
170
|
}
|
|
171
|
+
async customerTokenHandler(
|
|
172
|
+
request: AuthRequest,
|
|
173
|
+
response: Response,
|
|
174
|
+
next: NextFunction
|
|
175
|
+
) {
|
|
176
|
+
const grantType = request.query.grant_type || request.body.grant_type
|
|
177
|
+
if (!grantType) {
|
|
178
|
+
return next(
|
|
179
|
+
new CommercetoolsError<InvalidRequestError>(
|
|
180
|
+
{
|
|
181
|
+
code: 'invalid_request',
|
|
182
|
+
message: 'Missing required parameter: grant_type.',
|
|
183
|
+
},
|
|
184
|
+
400
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (grantType === 'password') {
|
|
190
|
+
const username = request.query.username || request.body.username
|
|
191
|
+
const password = hashPassword(
|
|
192
|
+
request.query.password || request.body.password
|
|
193
|
+
)
|
|
194
|
+
const scope =
|
|
195
|
+
request.query.scope?.toString() || request.body.scope?.toString()
|
|
196
|
+
|
|
197
|
+
const result = this.customerRepository.query(
|
|
198
|
+
{ projectKey: request.params.projectKey },
|
|
199
|
+
{
|
|
200
|
+
where: [`email = "${username}"`, `password = "${password}"`],
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if (result.count === 0) {
|
|
205
|
+
return next(
|
|
206
|
+
new CommercetoolsError<any>(
|
|
207
|
+
{
|
|
208
|
+
code: 'invalid_customer_account_credentials',
|
|
209
|
+
message: 'Customer account with the given credentials not found.',
|
|
210
|
+
},
|
|
211
|
+
400
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const customer = result.results[0]
|
|
217
|
+
const token = this.store.getCustomerToken(scope, customer.id)
|
|
218
|
+
return response.status(200).send(token)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async inStoreCustomerTokenHandler(
|
|
223
|
+
request: Request,
|
|
224
|
+
response: Response,
|
|
225
|
+
next: NextFunction
|
|
226
|
+
) {
|
|
227
|
+
return next(
|
|
228
|
+
new CommercetoolsError<InvalidClientError>(
|
|
229
|
+
{
|
|
230
|
+
code: 'invalid_client',
|
|
231
|
+
message: 'Not implemented yet in commercetools-mock',
|
|
232
|
+
},
|
|
233
|
+
401
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async anonymousTokenHandler(
|
|
239
|
+
request: Request,
|
|
240
|
+
response: Response,
|
|
241
|
+
next: NextFunction
|
|
242
|
+
) {
|
|
243
|
+
return next(
|
|
244
|
+
new CommercetoolsError<InvalidClientError>(
|
|
245
|
+
{
|
|
246
|
+
code: 'invalid_client',
|
|
247
|
+
message: 'Not implemented yet in commercetools-mock',
|
|
248
|
+
},
|
|
249
|
+
401
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
}
|
|
119
253
|
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -26,6 +26,19 @@ export class OAuth2Store {
|
|
|
26
26
|
return token
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
getCustomerToken(scope: string, customerId: string) {
|
|
30
|
+
const token: Token = {
|
|
31
|
+
access_token: randomBytes(16).toString('base64'),
|
|
32
|
+
token_type: 'Bearer',
|
|
33
|
+
expires_in: 172800,
|
|
34
|
+
scope: scope
|
|
35
|
+
? `${scope} custome_id:${customerId}`
|
|
36
|
+
: `customer_id: ${customerId}`,
|
|
37
|
+
}
|
|
38
|
+
this.tokens.push(token)
|
|
39
|
+
return token
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
validateToken(token: string) {
|
|
30
43
|
if (!this.validate) return true
|
|
31
44
|
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
CustomerDraft,
|
|
5
5
|
CustomerSetAuthenticationModeAction,
|
|
6
6
|
CustomerSetCustomFieldAction,
|
|
7
|
+
DuplicateFieldError,
|
|
7
8
|
InvalidInputError,
|
|
8
9
|
InvalidJsonInputError,
|
|
9
10
|
} from '@commercetools/platform-sdk'
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
AbstractResourceRepository,
|
|
15
16
|
type RepositoryContext,
|
|
16
17
|
} from './abstract.js'
|
|
18
|
+
import { hashPassword } from '../lib/password.js'
|
|
17
19
|
|
|
18
20
|
export class CustomerRepository extends AbstractResourceRepository<'customer'> {
|
|
19
21
|
getTypeId() {
|
|
@@ -21,13 +23,32 @@ export class CustomerRepository extends AbstractResourceRepository<'customer'> {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
create(context: RepositoryContext, draft: CustomerDraft): Customer {
|
|
26
|
+
// Check uniqueness
|
|
27
|
+
const results = this._storage.query(context.projectKey, this.getTypeId(), {
|
|
28
|
+
where: [`email="${draft.email.toLocaleLowerCase()}"`],
|
|
29
|
+
})
|
|
30
|
+
if (results.count > 0) {
|
|
31
|
+
throw new CommercetoolsError<any>({
|
|
32
|
+
code: 'CustomerAlreadyExists',
|
|
33
|
+
statusCode: 400,
|
|
34
|
+
message:
|
|
35
|
+
'There is already an existing customer with the provided email.',
|
|
36
|
+
errors: [
|
|
37
|
+
{
|
|
38
|
+
code: 'DuplicateField',
|
|
39
|
+
message: `Customer with email '${draft.email}' already exists.`,
|
|
40
|
+
duplicateValue: draft.email,
|
|
41
|
+
field: 'email',
|
|
42
|
+
} as DuplicateFieldError,
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
24
47
|
const resource: Customer = {
|
|
25
48
|
...getBaseResourceProperties(),
|
|
26
49
|
authenticationMode: draft.authenticationMode || 'Password',
|
|
27
|
-
email: draft.email,
|
|
28
|
-
password: draft.password
|
|
29
|
-
? Buffer.from(draft.password).toString('base64')
|
|
30
|
-
: undefined,
|
|
50
|
+
email: draft.email.toLowerCase(),
|
|
51
|
+
password: draft.password ? hashPassword(draft.password) : undefined,
|
|
31
52
|
isEmailVerified: draft.isEmailVerified || false,
|
|
32
53
|
addresses: [],
|
|
33
54
|
}
|
|
@@ -36,11 +57,14 @@ export class CustomerRepository extends AbstractResourceRepository<'customer'> {
|
|
|
36
57
|
}
|
|
37
58
|
|
|
38
59
|
getMe(context: RepositoryContext): Customer | undefined {
|
|
60
|
+
// grab the first customer you can find for now. In the future we should
|
|
61
|
+
// use the customer id from the scope of the token
|
|
39
62
|
const results = this._storage.query(
|
|
40
63
|
context.projectKey,
|
|
41
64
|
this.getTypeId(),
|
|
42
65
|
{}
|
|
43
|
-
)
|
|
66
|
+
)
|
|
67
|
+
|
|
44
68
|
if (results.count > 0) {
|
|
45
69
|
return results.results[0] as Customer
|
|
46
70
|
}
|
|
@@ -76,9 +100,7 @@ export class CustomerRepository extends AbstractResourceRepository<'customer'> {
|
|
|
76
100
|
return
|
|
77
101
|
}
|
|
78
102
|
if (authMode === 'Password') {
|
|
79
|
-
resource.password = password
|
|
80
|
-
? Buffer.from(password).toString('base64')
|
|
81
|
-
: undefined
|
|
103
|
+
resource.password = password ? hashPassword(password) : undefined
|
|
82
104
|
return
|
|
83
105
|
}
|
|
84
106
|
throw new CommercetoolsError<InvalidJsonInputError>(
|
|
@@ -2,6 +2,7 @@ import { Request, Response, Router } from 'express'
|
|
|
2
2
|
import { CustomerRepository } from '../repositories/customer.js'
|
|
3
3
|
import { getRepositoryContext } from '../repositories/helpers.js'
|
|
4
4
|
import AbstractService from './abstract.js'
|
|
5
|
+
import { hashPassword } from '../lib/password.js'
|
|
5
6
|
|
|
6
7
|
export class MyCustomerService extends AbstractService {
|
|
7
8
|
public repository: CustomerRepository
|
|
@@ -51,7 +52,7 @@ export class MyCustomerService extends AbstractService {
|
|
|
51
52
|
|
|
52
53
|
signIn(request: Request, response: Response) {
|
|
53
54
|
const { email, password } = request.body
|
|
54
|
-
const encodedPassword =
|
|
55
|
+
const encodedPassword = hashPassword(password)
|
|
55
56
|
|
|
56
57
|
const result = this.repository.query(getRepositoryContext(request), {
|
|
57
58
|
where: [`email = "${email}"`, `password = "${encodedPassword}"`],
|