@labdigital/commercetools-mock 2.0.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@labdigital/commercetools-mock",
3
3
  "author": "Michael van Tellingen",
4
- "version": "2.0.0",
4
+ "version": "2.1.0",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",
package/src/ctMock.ts CHANGED
@@ -106,6 +106,7 @@ export class CommercetoolsMock {
106
106
 
107
107
  private createApp(options?: AppOptions): express.Express {
108
108
  this._repositories = createRepositories(this._storage)
109
+ this._oauth2.setCustomerRepository(this._repositories.customer)
109
110
 
110
111
  const app = express()
111
112
 
@@ -139,6 +140,14 @@ export class CommercetoolsMock {
139
140
 
140
141
  app.use((err: Error, req: Request, resp: Response, next: NextFunction) => {
141
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
+
142
151
  return resp.status(err.statusCode).send({
143
152
  statusCode: err.statusCode,
144
153
  message: err.message,
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
 
@@ -0,0 +1,7 @@
1
+ export const validatePassword = (
2
+ clearPassword: string,
3
+ hashedPassword: string
4
+ ) => hashPassword(clearPassword) === hashedPassword
5
+
6
+ export const hashPassword = (clearPassword: string) =>
7
+ Buffer.from(clearPassword).toString('base64')
@@ -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
- async tokenHandler(request: Request, response: Response, next: NextFunction) {
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.name,
103
- credentials.pass,
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
  }
@@ -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
- ) // grab the first customer you can find
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 = Buffer.from(password).toString('base64')
55
+ const encodedPassword = hashPassword(password)
55
56
 
56
57
  const result = this.repository.query(getRepositoryContext(request), {
57
58
  where: [`email = "${email}"`, `password = "${encodedPassword}"`],