@budibase/worker 1.2.57 → 1.2.58-alpha.2

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.
Files changed (42) hide show
  1. package/package.json +7 -6
  2. package/scripts/jestSetup.js +6 -0
  3. package/src/api/controllers/global/auth.ts +14 -1
  4. package/src/api/controllers/global/users.ts +6 -8
  5. package/src/api/controllers/system/accounts.ts +21 -0
  6. package/src/api/{index.js → index.ts} +12 -16
  7. package/src/api/routes/global/configs.js +1 -0
  8. package/src/api/routes/{tests/auth.spec.js → global/tests/auth.spec.ts} +32 -56
  9. package/src/api/routes/{tests/configs.spec.js → global/tests/configs.spec.ts} +76 -47
  10. package/src/api/routes/{tests/email.spec.js → global/tests/email.spec.ts} +7 -15
  11. package/src/api/routes/{tests/realEmail.spec.js → global/tests/realEmail.spec.ts} +20 -21
  12. package/src/api/routes/{tests/self.spec.js → global/tests/self.spec.ts} +10 -18
  13. package/src/api/routes/global/tests/users.spec.ts +464 -0
  14. package/src/api/routes/index.js +2 -0
  15. package/src/api/routes/system/accounts.ts +19 -0
  16. package/src/api/routes/system/tests/accounts.spec.ts +57 -0
  17. package/src/{environment.js → environment.ts} +10 -9
  18. package/src/index.ts +6 -4
  19. package/src/sdk/accounts/accounts.ts +53 -0
  20. package/src/sdk/accounts/index.ts +1 -0
  21. package/src/sdk/index.ts +1 -0
  22. package/src/sdk/users/users.ts +217 -77
  23. package/src/tests/TestConfiguration.ts +273 -0
  24. package/src/tests/api/accounts.ts +28 -0
  25. package/src/tests/api/auth.ts +48 -0
  26. package/src/tests/api/configs.ts +40 -0
  27. package/src/tests/api/email.ts +24 -0
  28. package/src/tests/api/index.ts +25 -0
  29. package/src/tests/api/self.ts +21 -0
  30. package/src/tests/api/users.ts +95 -0
  31. package/src/tests/index.ts +14 -0
  32. package/src/tests/mocks/index.ts +7 -0
  33. package/src/tests/structures/accounts.ts +24 -0
  34. package/src/tests/structures/index.ts +20 -0
  35. package/src/tests/structures/users.ts +12 -4
  36. package/src/utilities/redis.js +1 -0
  37. package/scripts/load/users.js +0 -97
  38. package/src/api/routes/tests/users.spec.js +0 -390
  39. package/src/tests/TestConfiguration.js +0 -231
  40. package/src/tests/index.js +0 -12
  41. package/src/tests/mocks/index.js +0 -5
  42. package/src/tests/structures/index.js +0 -14
@@ -0,0 +1,57 @@
1
+ import { accounts } from "../../../../sdk"
2
+ import { TestConfiguration, structures, API } from "../../../../tests"
3
+ import { v4 as uuid } from "uuid"
4
+
5
+ describe("accounts", () => {
6
+ const config = new TestConfiguration()
7
+ const api = new API(config)
8
+
9
+ beforeAll(async () => {
10
+ await config.beforeAll()
11
+ })
12
+
13
+ afterAll(async () => {
14
+ await config.afterAll()
15
+ })
16
+
17
+ beforeEach(() => {
18
+ jest.clearAllMocks()
19
+ })
20
+
21
+ describe("metadata", () => {
22
+ describe("saveMetadata", () => {
23
+ it("saves account metadata", async () => {
24
+ let account = structures.accounts.account()
25
+
26
+ const response = await api.accounts.saveMetadata(account)
27
+
28
+ const id = accounts.formatAccountMetadataId(account.accountId)
29
+ const metadata = await accounts.getMetadata(id)
30
+ expect(response).toStrictEqual(metadata)
31
+ })
32
+ })
33
+
34
+ describe("destroyMetadata", () => {
35
+ it("destroys account metadata", async () => {
36
+ const account = structures.accounts.account()
37
+ await api.accounts.saveMetadata(account)
38
+
39
+ await api.accounts.destroyMetadata(account.accountId)
40
+
41
+ const deleted = await accounts.getMetadata(account.accountId)
42
+ expect(deleted).toBe(undefined)
43
+ })
44
+
45
+ it("destroys account metadata that does not exist", async () => {
46
+ const id = uuid()
47
+
48
+ const response = await api.accounts.destroyMetadata(id)
49
+
50
+ expect(response.status).toBe(404)
51
+ expect(response.body.message).toBe(
52
+ `id=${accounts.formatAccountMetadataId(id)} does not exist`
53
+ )
54
+ })
55
+ })
56
+ })
57
+ })
@@ -20,13 +20,13 @@ if (!LOADED && isDev() && !isTest()) {
20
20
  LOADED = true
21
21
  }
22
22
 
23
- function parseIntSafe(number) {
23
+ function parseIntSafe(number: any) {
24
24
  if (number) {
25
25
  return parseInt(number)
26
26
  }
27
27
  }
28
28
 
29
- module.exports = {
29
+ const env = {
30
30
  // auth
31
31
  MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
32
32
  MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
@@ -47,7 +47,7 @@ module.exports = {
47
47
  CLUSTER_PORT: process.env.CLUSTER_PORT,
48
48
  // flags
49
49
  NODE_ENV: process.env.NODE_ENV,
50
- SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
50
+ SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
51
51
  LOG_LEVEL: process.env.LOG_LEVEL,
52
52
  MULTI_TENANCY: process.env.MULTI_TENANCY,
53
53
  DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
@@ -62,7 +62,7 @@ module.exports = {
62
62
  // other
63
63
  CHECKLIST_CACHE_TTL: parseIntSafe(process.env.CHECKLIST_CACHE_TTL) || 3600,
64
64
  SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD,
65
- _set(key, value) {
65
+ _set(key: any, value: any) {
66
66
  process.env[key] = value
67
67
  module.exports[key] = value
68
68
  },
@@ -74,16 +74,17 @@ module.exports = {
74
74
  }
75
75
 
76
76
  // if some var haven't been set, define them
77
- if (!module.exports.APPS_URL) {
78
- module.exports.APPS_URL = isDev()
79
- ? "http://localhost:4001"
80
- : "http://app-service:4002"
77
+ if (!env.APPS_URL) {
78
+ env.APPS_URL = isDev() ? "http://localhost:4001" : "http://app-service:4002"
81
79
  }
82
80
 
83
81
  // clean up any environment variable edge cases
84
82
  for (let [key, value] of Object.entries(module.exports)) {
85
83
  // handle the edge case of "0" to disable an environment variable
86
84
  if (value === "0") {
87
- module.exports[key] = 0
85
+ // @ts-ignore
86
+ env[key] = 0
88
87
  }
89
88
  }
89
+
90
+ export = env
package/src/index.ts CHANGED
@@ -71,9 +71,7 @@ server.on("close", async () => {
71
71
  return
72
72
  }
73
73
  shuttingDown = true
74
- if (!env.isTest()) {
75
- console.log("Server Closed")
76
- }
74
+ console.log("Server Closed")
77
75
  await redis.shutdown()
78
76
  await events.shutdown()
79
77
  if (!env.isTest()) {
@@ -86,7 +84,7 @@ const shutdown = () => {
86
84
  server.destroy()
87
85
  }
88
86
 
89
- module.exports = server.listen(parseInt(env.PORT || 4002), async () => {
87
+ export = server.listen(parseInt(env.PORT || 4002), async () => {
90
88
  console.log(`Worker running on ${JSON.stringify(server.address())}`)
91
89
  await redis.init()
92
90
  })
@@ -100,3 +98,7 @@ process.on("uncaughtException", err => {
100
98
  process.on("SIGTERM", () => {
101
99
  shutdown()
102
100
  })
101
+
102
+ process.on("SIGINT", () => {
103
+ shutdown()
104
+ })
@@ -0,0 +1,53 @@
1
+ import { AccountMetadata } from "@budibase/types"
2
+ import {
3
+ db,
4
+ StaticDatabases,
5
+ HTTPError,
6
+ DocumentType,
7
+ SEPARATOR,
8
+ } from "@budibase/backend-core"
9
+
10
+ export const formatAccountMetadataId = (accountId: string) => {
11
+ return `${DocumentType.ACCOUNT_METADATA}${SEPARATOR}${accountId}`
12
+ }
13
+
14
+ export const saveMetadata = async (
15
+ metadata: AccountMetadata
16
+ ): Promise<AccountMetadata> => {
17
+ return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
18
+ const existing = await getMetadata(metadata._id!)
19
+ if (existing) {
20
+ metadata._rev = existing._rev
21
+ }
22
+ const res = await db.put(metadata)
23
+ metadata._rev = res.rev
24
+ return metadata
25
+ })
26
+ }
27
+
28
+ export const getMetadata = async (
29
+ accountId: string
30
+ ): Promise<AccountMetadata | undefined> => {
31
+ return db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
32
+ try {
33
+ return await db.get(accountId)
34
+ } catch (e: any) {
35
+ if (e.status === 404) {
36
+ // do nothing
37
+ return
38
+ } else {
39
+ throw e
40
+ }
41
+ }
42
+ })
43
+ }
44
+
45
+ export const destroyMetadata = async (accountId: string) => {
46
+ await db.doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => {
47
+ const metadata = await getMetadata(accountId)
48
+ if (!metadata) {
49
+ throw new HTTPError(`id=${accountId} does not exist`, 404)
50
+ }
51
+ await db.remove(accountId, metadata._rev)
52
+ })
53
+ }
@@ -0,0 +1 @@
1
+ export * from "./accounts"
package/src/sdk/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * as users from "./users"
2
+ export * as accounts from "./accounts"
@@ -14,8 +14,23 @@ import {
14
14
  HTTPError,
15
15
  accounts,
16
16
  migrations,
17
+ StaticDatabases,
18
+ ViewName,
17
19
  } from "@budibase/backend-core"
18
- import { MigrationType, User } from "@budibase/types"
20
+ import {
21
+ MigrationType,
22
+ PlatformUserByEmail,
23
+ User,
24
+ Account,
25
+ BulkCreateUsersResponse,
26
+ CreateUserResponse,
27
+ BulkDeleteUsersResponse,
28
+ CloudAccount,
29
+ AllDocsResponse,
30
+ RowResponse,
31
+ BulkDocsResponse,
32
+ AccountMetadata,
33
+ } from "@budibase/types"
19
34
  import { groups as groupUtils } from "@budibase/pro"
20
35
 
21
36
  const PAGE_LIMIT = 8
@@ -98,7 +113,6 @@ export const getUser = async (userId: string) => {
98
113
  interface SaveUserOpts {
99
114
  hashPassword?: boolean
100
115
  requirePassword?: boolean
101
- bulkCreate?: boolean
102
116
  }
103
117
 
104
118
  const buildUser = async (
@@ -109,7 +123,7 @@ const buildUser = async (
109
123
  },
110
124
  tenantId: string,
111
125
  dbUser?: any
112
- ) => {
126
+ ): Promise<User> => {
113
127
  let { password, _id } = user
114
128
 
115
129
  let hashedPassword
@@ -143,62 +157,63 @@ const buildUser = async (
143
157
  return user
144
158
  }
145
159
 
160
+ const validateUniqueUser = async (email: string, tenantId: string) => {
161
+ // check budibase users in other tenants
162
+ if (env.MULTI_TENANCY) {
163
+ const tenantUser = await tenancy.getTenantUser(email)
164
+ if (tenantUser != null && tenantUser.tenantId !== tenantId) {
165
+ throw `Unavailable`
166
+ }
167
+ }
168
+
169
+ // check root account users in account portal
170
+ if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
171
+ const account = await accounts.getAccount(email)
172
+ if (account && account.verified && account.tenantId !== tenantId) {
173
+ throw `Unavailable`
174
+ }
175
+ }
176
+ }
177
+
146
178
  export const save = async (
147
- user: any,
179
+ user: User,
148
180
  opts: SaveUserOpts = {
149
181
  hashPassword: true,
150
182
  requirePassword: true,
151
- bulkCreate: false,
152
183
  }
153
- ) => {
184
+ ): Promise<CreateUserResponse> => {
154
185
  const tenantId = tenancy.getTenantId()
155
186
  const db = tenancy.getGlobalDB()
156
187
  let { email, _id } = user
157
- // make sure another user isn't using the same email
158
- let dbUser: any
159
- if (opts.bulkCreate) {
160
- dbUser = null
161
- } else if (email) {
162
- // check budibase users inside the tenant
163
- dbUser = await usersCore.getGlobalUserByEmail(email)
164
- if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) {
165
- throw `Email address ${email} already in use.`
166
- }
167
188
 
168
- // check budibase users in other tenants
169
- if (env.MULTI_TENANCY) {
170
- const tenantUser = await tenancy.getTenantUser(email)
171
- if (tenantUser != null && tenantUser.tenantId !== tenantId) {
172
- throw `Email address ${email} already in use.`
173
- }
189
+ let dbUser: User | undefined
190
+ if (_id) {
191
+ // try to get existing user from db
192
+ dbUser = (await db.get(_id)) as User
193
+ if (email && dbUser.email !== email) {
194
+ throw "Email address cannot be changed"
174
195
  }
175
-
176
- // check root account users in account portal
177
- if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
178
- const account = await accounts.getAccount(email)
179
- if (account && account.verified && account.tenantId !== tenantId) {
180
- throw `Email address ${email} already in use.`
181
- }
196
+ email = dbUser.email
197
+ } else if (email) {
198
+ // no id was specified - load from email instead
199
+ dbUser = await usersCore.getGlobalUserByEmail(email)
200
+ if (dbUser && dbUser._id !== _id) {
201
+ throw `Unavailable`
182
202
  }
183
- } else if (_id) {
184
- dbUser = await db.get(_id)
203
+ } else {
204
+ throw new Error("_id or email is required")
185
205
  }
186
206
 
207
+ await validateUniqueUser(email, tenantId)
208
+
187
209
  let builtUser = await buildUser(user, opts, tenantId, dbUser)
188
210
 
189
211
  // make sure we set the _id field for a new user
190
212
  if (!_id) {
191
- _id = builtUser._id
213
+ _id = builtUser._id!
192
214
  }
193
215
 
194
216
  try {
195
- const putOpts = {
196
- password: builtUser.password,
197
- ...user,
198
- }
199
- if (opts.bulkCreate) {
200
- return putOpts
201
- }
202
217
  // save the user to db
203
218
  let response
204
219
  const putUserFn = () => {
@@ -247,29 +262,87 @@ export const addTenant = async (
247
262
  }
248
263
  }
249
264
 
265
+ const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
266
+ return dbUtils.queryGlobalView(ViewName.USER_BY_EMAIL, {
267
+ keys: emails,
268
+ include_docs: true,
269
+ arrayResponse: true,
270
+ })
271
+ }
272
+
273
+ const getExistingPlatformUsers = async (
274
+ emails: string[]
275
+ ): Promise<PlatformUserByEmail[]> => {
276
+ return dbUtils.doWithDB(
277
+ StaticDatabases.PLATFORM_INFO.name,
278
+ async (infoDb: any) => {
279
+ const response: AllDocsResponse<PlatformUserByEmail> =
280
+ await infoDb.allDocs({
281
+ keys: emails,
282
+ include_docs: true,
283
+ })
284
+ return response.rows
285
+ .filter(row => row.doc && (row.error !== "not_found") !== null)
286
+ .map((row: any) => row.doc)
287
+ }
288
+ )
289
+ }
290
+
291
+ const getExistingAccounts = async (
292
+ emails: string[]
293
+ ): Promise<AccountMetadata[]> => {
294
+ return dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, {
295
+ keys: emails,
296
+ include_docs: true,
297
+ arrayResponse: true,
298
+ })
299
+ }
300
+
301
+ /**
302
+ * Apply a system-wide search on emails:
303
+ * - in tenant
304
+ * - cross tenant
305
+ * - accounts
306
+ * return an array of emails that match the supplied emails.
307
+ */
308
+ const searchExistingEmails = async (emails: string[]) => {
309
+ let matchedEmails: string[] = []
310
+
311
+ const existingTenantUsers = await getExistingTenantUsers(emails)
312
+ matchedEmails.push(...existingTenantUsers.map(user => user.email))
313
+
314
+ const existingPlatformUsers = await getExistingPlatformUsers(emails)
315
+ matchedEmails.push(...existingPlatformUsers.map(user => user._id!))
316
+
317
+ const existingAccounts = await getExistingAccounts(emails)
318
+ matchedEmails.push(...existingAccounts.map(account => account.email))
319
+
320
+ return [...new Set(matchedEmails)]
321
+ }
322
+
250
323
  export const bulkCreate = async (
251
324
  newUsersRequested: User[],
252
325
  groups: string[]
253
- ) => {
326
+ ): Promise<BulkCreateUsersResponse> => {
254
327
  const db = tenancy.getGlobalDB()
255
328
  const tenantId = tenancy.getTenantId()
256
329
 
257
330
  let usersToSave: any[] = []
258
331
  let newUsers: any[] = []
259
332
 
260
- const allUsers = await db.allDocs(
261
- dbUtils.getGlobalUserParams(null, {
262
- include_docs: true,
263
- })
264
- )
265
- let mapped = allUsers.rows.map((row: any) => row.id)
333
+ const emails = newUsersRequested.map((user: User) => user.email)
334
+ const existingEmails = await searchExistingEmails(emails)
335
+ const unsuccessful: { email: string; reason: string }[] = []
266
336
 
267
- const currentUserEmails = mapped.map((x: any) => x.email) || []
268
337
  for (const newUser of newUsersRequested) {
269
338
  if (
270
339
  newUsers.find((x: any) => x.email === newUser.email) ||
271
- currentUserEmails.includes(newUser.email)
340
+ existingEmails.includes(newUser.email)
272
341
  ) {
342
+ unsuccessful.push({
343
+ email: newUser.email,
344
+ reason: `Unavailable`,
345
+ })
273
346
  continue
274
347
  }
275
348
  newUser.userGroups = groups
@@ -307,63 +380,130 @@ export const bulkCreate = async (
307
380
  await apps.syncUserInApps(user._id)
308
381
  }
309
382
 
310
- return usersToBulkSave.map(user => {
383
+ const saved = usersToBulkSave.map(user => {
311
384
  return {
312
385
  _id: user._id,
313
386
  email: user.email,
314
387
  }
315
388
  })
389
+
390
+ return {
391
+ successful: saved,
392
+ unsuccessful,
393
+ }
316
394
  }
317
395
 
318
- export const bulkDelete = async (userIds: any) => {
396
+ /**
397
+ * For the given user id's, return the account holder if it is in the ids.
398
+ */
399
+ const getAccountHolderFromUserIds = async (
400
+ userIds: string[]
401
+ ): Promise<CloudAccount | undefined> => {
402
+ if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
403
+ const tenantId = tenancy.getTenantId()
404
+ const account = await accounts.getAccountByTenantId(tenantId)
405
+ if (!account) {
406
+ throw new Error(`Account not found for tenantId=${tenantId}`)
407
+ }
408
+
409
+ const budibaseUserId = account.budibaseUserId
410
+ if (userIds.includes(budibaseUserId)) {
411
+ return account
412
+ }
413
+ }
414
+ }
415
+
416
+ export const bulkDelete = async (
417
+ userIds: string[]
418
+ ): Promise<BulkDeleteUsersResponse> => {
319
419
  const db = tenancy.getGlobalDB()
320
420
 
421
+ const response: BulkDeleteUsersResponse = {
422
+ successful: [],
423
+ unsuccessful: [],
424
+ }
425
+
426
+ // remove the account holder from the delete request if present
427
+ const account = await getAccountHolderFromUserIds(userIds)
428
+ if (account) {
429
+ userIds = userIds.filter(u => u !== account.budibaseUserId)
430
+ // mark user as unsuccessful
431
+ response.unsuccessful.push({
432
+ _id: account.budibaseUserId,
433
+ email: account.email,
434
+ reason: "Account holder cannot be deleted",
435
+ })
436
+ }
437
+
321
438
  let groupsToModify: any = {}
322
439
  let builderCount = 0
440
+
323
441
  // Get users and delete
324
- let usersToDelete = (
325
- await db.allDocs({
326
- include_docs: true,
327
- keys: userIds,
328
- })
329
- ).rows.map((user: any) => {
330
- // if we find a user that has an associated group, add it to
331
- // an array so we can easily use allDocs on them later.
332
- // This prevents us having to re-loop over all the users
333
- if (user.doc.userGroups) {
334
- for (let groupId of user.doc.userGroups) {
335
- if (!Object.keys(groupsToModify).includes(groupId)) {
336
- groupsToModify[groupId] = [user.id]
337
- } else {
338
- groupsToModify[groupId] = [...groupsToModify[groupId], user.id]
442
+ const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
443
+ include_docs: true,
444
+ keys: userIds,
445
+ })
446
+ const usersToDelete: User[] = allDocsResponse.rows.map(
447
+ (user: RowResponse<User>) => {
448
+ // if we find a user that has an associated group, add it to
449
+ // an array so we can easily use allDocs on them later.
450
+ // This prevents us having to re-loop over all the users
451
+ if (user.doc.userGroups) {
452
+ for (let groupId of user.doc.userGroups) {
453
+ if (!Object.keys(groupsToModify).includes(groupId)) {
454
+ groupsToModify[groupId] = [user.id]
455
+ } else {
456
+ groupsToModify[groupId] = [...groupsToModify[groupId], user.id]
457
+ }
339
458
  }
340
459
  }
341
- }
342
460
 
343
- // Also figure out how many builders are being deleted
344
- if (eventHelpers.isAddingBuilder(user.doc, null)) {
345
- builderCount++
346
- }
461
+ // Also figure out how many builders are being deleted
462
+ if (eventHelpers.isAddingBuilder(user.doc, null)) {
463
+ builderCount++
464
+ }
347
465
 
348
- return user.doc
349
- })
466
+ return user.doc
467
+ }
468
+ )
350
469
 
351
- const response = await db.bulkDocs(
352
- usersToDelete.map((user: any) => ({
470
+ // Delete from DB
471
+ const dbResponse: BulkDocsResponse = await db.bulkDocs(
472
+ usersToDelete.map(user => ({
353
473
  ...user,
354
474
  _deleted: true,
355
475
  }))
356
476
  )
357
477
 
478
+ // Deletion post processing
358
479
  await groupUtils.bulkDeleteGroupUsers(groupsToModify)
359
-
360
- //Deletion post processing
361
480
  for (let user of usersToDelete) {
362
481
  await bulkDeleteProcessing(user)
363
482
  }
364
-
365
483
  await quotas.removeDevelopers(builderCount)
366
484
 
485
+ // Build Response
486
+ // index users by id
487
+ const userIndex: { [key: string]: User } = {}
488
+ usersToDelete.reduce((prev, current) => {
489
+ prev[current._id!] = current
490
+ return prev
491
+ }, userIndex)
492
+
493
+ // add the successful and unsuccessful users to response
494
+ dbResponse.forEach(item => {
495
+ const email = userIndex[item.id].email
496
+ if (item.ok) {
497
+ response.successful.push({ _id: item.id, email })
498
+ } else {
499
+ response.unsuccessful.push({
500
+ _id: item.id,
501
+ email,
502
+ reason: "Database error",
503
+ })
504
+ }
505
+ })
506
+
367
507
  return response
368
508
  }
369
509