@budibase/backend-core 2.11.37 → 2.11.39

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 (92) hide show
  1. package/dist/index.js +324 -276
  2. package/dist/index.js.map +3 -3
  3. package/dist/index.js.meta.json +1 -1
  4. package/dist/package.json +4 -4
  5. package/dist/plugins.js.map +1 -1
  6. package/dist/plugins.js.meta.json +1 -1
  7. package/dist/src/cache/appMetadata.d.ts +5 -5
  8. package/dist/src/cache/appMetadata.js +5 -5
  9. package/dist/src/cache/user.d.ts +5 -5
  10. package/dist/src/cache/user.js +5 -5
  11. package/dist/src/cache/writethrough.d.ts +1 -1
  12. package/dist/src/cache/writethrough.js +2 -2
  13. package/dist/src/cache/writethrough.js.map +1 -1
  14. package/dist/src/configs/configs.d.ts +1 -1
  15. package/dist/src/configs/configs.js +1 -1
  16. package/dist/src/context/mainContext.d.ts +1 -1
  17. package/dist/src/context/mainContext.js +1 -1
  18. package/dist/src/db/Replication.d.ts +4 -4
  19. package/dist/src/db/Replication.js +4 -4
  20. package/dist/src/db/lucene.d.ts +8 -8
  21. package/dist/src/db/lucene.js +12 -12
  22. package/dist/src/db/utils.d.ts +1 -1
  23. package/dist/src/db/utils.js +1 -1
  24. package/dist/src/docIds/conversions.d.ts +1 -1
  25. package/dist/src/docIds/conversions.js +1 -1
  26. package/dist/src/docIds/ids.d.ts +11 -11
  27. package/dist/src/docIds/ids.js +11 -11
  28. package/dist/src/docIds/params.d.ts +8 -8
  29. package/dist/src/docIds/params.js +8 -8
  30. package/dist/src/helpers.d.ts +2 -2
  31. package/dist/src/helpers.js +2 -2
  32. package/dist/src/middleware/passport/local.d.ts +4 -4
  33. package/dist/src/middleware/passport/local.js +4 -4
  34. package/dist/src/middleware/passport/sso/oidc.js +11 -11
  35. package/dist/src/middleware/passport/utils.d.ts +3 -3
  36. package/dist/src/middleware/passport/utils.js +3 -3
  37. package/dist/src/objectStore/buckets/app.d.ts +3 -3
  38. package/dist/src/objectStore/buckets/app.js +3 -3
  39. package/dist/src/objectStore/objectStore.d.ts +3 -3
  40. package/dist/src/objectStore/objectStore.js +3 -3
  41. package/dist/src/queue/inMemoryQueue.d.ts +6 -6
  42. package/dist/src/queue/inMemoryQueue.js +9 -9
  43. package/dist/src/redis/redis.js +1 -1
  44. package/dist/src/security/permissions.d.ts +2 -2
  45. package/dist/src/security/permissions.js +2 -2
  46. package/dist/src/security/roles.d.ts +6 -6
  47. package/dist/src/security/roles.js +6 -6
  48. package/dist/src/users/db.d.ts +1 -1
  49. package/dist/src/users/db.js +12 -5
  50. package/dist/src/users/db.js.map +1 -1
  51. package/dist/src/users/users.d.ts +2 -1
  52. package/dist/src/users/users.js +20 -2
  53. package/dist/src/users/users.js.map +1 -1
  54. package/dist/src/users/utils.d.ts +1 -0
  55. package/dist/src/users/utils.js +2 -1
  56. package/dist/src/users/utils.js.map +1 -1
  57. package/dist/src/utils/utils.d.ts +11 -11
  58. package/dist/src/utils/utils.js +11 -11
  59. package/dist/tests/core/utilities/structures/licenses.js +13 -0
  60. package/dist/tests/core/utilities/structures/licenses.js.map +1 -1
  61. package/dist/tests/core/utilities/structures/quotas.d.ts +1 -1
  62. package/dist/tests/core/utilities/structures/quotas.js +3 -2
  63. package/dist/tests/core/utilities/structures/quotas.js.map +1 -1
  64. package/package.json +4 -4
  65. package/src/cache/appMetadata.ts +5 -5
  66. package/src/cache/user.ts +5 -5
  67. package/src/cache/writethrough.ts +2 -2
  68. package/src/configs/configs.ts +1 -1
  69. package/src/context/mainContext.ts +1 -1
  70. package/src/db/Replication.ts +4 -4
  71. package/src/db/lucene.ts +12 -12
  72. package/src/db/utils.ts +1 -1
  73. package/src/docIds/conversions.ts +1 -1
  74. package/src/docIds/ids.ts +11 -11
  75. package/src/docIds/params.ts +8 -8
  76. package/src/helpers.ts +2 -2
  77. package/src/middleware/passport/local.ts +4 -4
  78. package/src/middleware/passport/sso/oidc.ts +11 -11
  79. package/src/middleware/passport/utils.ts +3 -3
  80. package/src/objectStore/buckets/app.ts +3 -3
  81. package/src/objectStore/objectStore.ts +3 -3
  82. package/src/queue/inMemoryQueue.ts +9 -9
  83. package/src/redis/redis.ts +1 -1
  84. package/src/security/permissions.ts +2 -2
  85. package/src/security/roles.ts +6 -6
  86. package/src/users/db.ts +63 -48
  87. package/src/users/users.ts +16 -2
  88. package/src/users/utils.ts +1 -0
  89. package/src/utils/utils.ts +11 -11
  90. package/tests/core/users/users.spec.js +54 -0
  91. package/tests/core/utilities/structures/licenses.ts +13 -0
  92. package/tests/core/utilities/structures/quotas.ts +3 -2
package/src/users/db.ts CHANGED
@@ -21,17 +21,21 @@ import {
21
21
  User,
22
22
  UserStatus,
23
23
  UserGroup,
24
- ContextUser,
25
24
  } from "@budibase/types"
26
25
  import {
27
26
  getAccountHolderFromUserIds,
28
27
  isAdmin,
28
+ isCreator,
29
29
  validateUniqueUser,
30
30
  } from "./utils"
31
31
  import { searchExistingEmails } from "./lookup"
32
32
  import { hash } from "../utils"
33
33
 
34
- type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any>
34
+ type QuotaUpdateFn = (
35
+ change: number,
36
+ creatorsChange: number,
37
+ cb?: () => Promise<any>
38
+ ) => Promise<any>
35
39
  type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
36
40
  type FeatureFn = () => Promise<Boolean>
37
41
  type GroupGetFn = (ids: string[]) => Promise<UserGroup[]>
@@ -135,7 +139,7 @@ export class UserDB {
135
139
  if (!fullUser.roles) {
136
140
  fullUser.roles = {}
137
141
  }
138
- // add the active status to a user if its not provided
142
+ // add the active status to a user if it's not provided
139
143
  if (fullUser.status == null) {
140
144
  fullUser.status = UserStatus.ACTIVE
141
145
  }
@@ -246,7 +250,8 @@ export class UserDB {
246
250
  }
247
251
 
248
252
  const change = dbUser ? 0 : 1 // no change if there is existing user
249
- return UserDB.quotas.addUsers(change, async () => {
253
+ const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0
254
+ return UserDB.quotas.addUsers(change, creatorsChange, async () => {
250
255
  await validateUniqueUser(email, tenantId)
251
256
 
252
257
  let builtUser = await UserDB.buildUser(user, opts, tenantId, dbUser)
@@ -308,6 +313,7 @@ export class UserDB {
308
313
 
309
314
  let usersToSave: any[] = []
310
315
  let newUsers: any[] = []
316
+ let newCreators: any[] = []
311
317
 
312
318
  const emails = newUsersRequested.map((user: User) => user.email)
313
319
  const existingEmails = await searchExistingEmails(emails)
@@ -328,59 +334,66 @@ export class UserDB {
328
334
  }
329
335
  newUser.userGroups = groups
330
336
  newUsers.push(newUser)
337
+ if (isCreator(newUser)) {
338
+ newCreators.push(newUser)
339
+ }
331
340
  }
332
341
 
333
342
  const account = await accountSdk.getAccountByTenantId(tenantId)
334
- return UserDB.quotas.addUsers(newUsers.length, async () => {
335
- // create the promises array that will be called by bulkDocs
336
- newUsers.forEach((user: any) => {
337
- usersToSave.push(
338
- UserDB.buildUser(
339
- user,
340
- {
341
- hashPassword: true,
342
- requirePassword: user.requirePassword,
343
- },
344
- tenantId,
345
- undefined, // no dbUser
346
- account
343
+ return UserDB.quotas.addUsers(
344
+ newUsers.length,
345
+ newCreators.length,
346
+ async () => {
347
+ // create the promises array that will be called by bulkDocs
348
+ newUsers.forEach((user: any) => {
349
+ usersToSave.push(
350
+ UserDB.buildUser(
351
+ user,
352
+ {
353
+ hashPassword: true,
354
+ requirePassword: user.requirePassword,
355
+ },
356
+ tenantId,
357
+ undefined, // no dbUser
358
+ account
359
+ )
347
360
  )
348
- )
349
- })
361
+ })
350
362
 
351
- const usersToBulkSave = await Promise.all(usersToSave)
352
- await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
363
+ const usersToBulkSave = await Promise.all(usersToSave)
364
+ await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
353
365
 
354
- // Post-processing of bulk added users, e.g. events and cache operations
355
- for (const user of usersToBulkSave) {
356
- // TODO: Refactor to bulk insert users into the info db
357
- // instead of relying on looping tenant creation
358
- await platform.users.addUser(tenantId, user._id, user.email)
359
- await eventHelpers.handleSaveEvents(user, undefined)
360
- }
361
-
362
- const saved = usersToBulkSave.map(user => {
363
- return {
364
- _id: user._id,
365
- email: user.email,
366
+ // Post-processing of bulk added users, e.g. events and cache operations
367
+ for (const user of usersToBulkSave) {
368
+ // TODO: Refactor to bulk insert users into the info db
369
+ // instead of relying on looping tenant creation
370
+ await platform.users.addUser(tenantId, user._id, user.email)
371
+ await eventHelpers.handleSaveEvents(user, undefined)
366
372
  }
367
- })
368
373
 
369
- // now update the groups
370
- if (Array.isArray(saved) && groups) {
371
- const groupPromises = []
372
- const createdUserIds = saved.map(user => user._id)
373
- for (let groupId of groups) {
374
- groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds))
374
+ const saved = usersToBulkSave.map(user => {
375
+ return {
376
+ _id: user._id,
377
+ email: user.email,
378
+ }
379
+ })
380
+
381
+ // now update the groups
382
+ if (Array.isArray(saved) && groups) {
383
+ const groupPromises = []
384
+ const createdUserIds = saved.map(user => user._id)
385
+ for (let groupId of groups) {
386
+ groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds))
387
+ }
388
+ await Promise.all(groupPromises)
375
389
  }
376
- await Promise.all(groupPromises)
377
- }
378
390
 
379
- return {
380
- successful: saved,
381
- unsuccessful,
391
+ return {
392
+ successful: saved,
393
+ unsuccessful,
394
+ }
382
395
  }
383
- })
396
+ )
384
397
  }
385
398
 
386
399
  static async bulkDelete(userIds: string[]): Promise<BulkUserDeleted> {
@@ -420,11 +433,12 @@ export class UserDB {
420
433
  _deleted: true,
421
434
  }))
422
435
  const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
436
+ const creatorsToDelete = usersToDelete.filter(isCreator)
423
437
 
424
- await UserDB.quotas.removeUsers(toDelete.length)
425
438
  for (let user of usersToDelete) {
426
439
  await bulkDeleteProcessing(user)
427
440
  }
441
+ await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length)
428
442
 
429
443
  // Build Response
430
444
  // index users by id
@@ -473,7 +487,8 @@ export class UserDB {
473
487
 
474
488
  await db.remove(userId, dbUser._rev)
475
489
 
476
- await UserDB.quotas.removeUsers(1)
490
+ const creatorsToDelete = isCreator(dbUser) ? 1 : 0
491
+ await UserDB.quotas.removeUsers(1, creatorsToDelete)
477
492
  await eventHelpers.handleDeleteEvents(dbUser)
478
493
  await cache.user.invalidateUser(userId)
479
494
  await sessions.invalidateSessions(userId, { reason: "deletion" })
@@ -14,14 +14,15 @@ import {
14
14
  } from "../db"
15
15
  import {
16
16
  BulkDocsResponse,
17
- ContextUser,
18
17
  SearchQuery,
19
18
  SearchQueryOperators,
20
19
  SearchUsersRequest,
21
20
  User,
21
+ ContextUser,
22
22
  } from "@budibase/types"
23
- import * as context from "../context"
24
23
  import { getGlobalDB } from "../context"
24
+ import * as context from "../context"
25
+ import { isCreator } from "./utils"
25
26
 
26
27
  type GetOpts = { cleanup?: boolean }
27
28
 
@@ -283,6 +284,19 @@ export async function getUserCount() {
283
284
  return response.total_rows
284
285
  }
285
286
 
287
+ export async function getCreatorCount() {
288
+ let creators = 0
289
+ async function iterate(startPage?: string) {
290
+ const page = await paginatedUsers({ bookmark: startPage })
291
+ creators += page.data.filter(isCreator).length
292
+ if (page.hasNextPage) {
293
+ await iterate(page.nextPage)
294
+ }
295
+ }
296
+ await iterate()
297
+ return creators
298
+ }
299
+
286
300
  // used to remove the builder/admin permissions, for processing the
287
301
  // user as an app user (they may have some specific role/group
288
302
  export function removePortalUserPermissions(user: User | ContextUser) {
@@ -10,6 +10,7 @@ import { getAccountByTenantId } from "../accounts"
10
10
  // extract from shared-core to make easily accessible from backend-core
11
11
  export const isBuilder = sdk.users.isBuilder
12
12
  export const isAdmin = sdk.users.isAdmin
13
+ export const isCreator = sdk.users.isCreator
13
14
  export const isGlobalBuilder = sdk.users.isGlobalBuilder
14
15
  export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
15
16
  export const hasAdminPermissions = sdk.users.hasAdminPermissions
@@ -79,8 +79,8 @@ export function isPublicApiRequest(ctx: Ctx): boolean {
79
79
 
80
80
  /**
81
81
  * Given a request tries to find the appId, which can be located in various places
82
- * @param {object} ctx The main request body to look through.
83
- * @returns {string|undefined} If an appId was found it will be returned.
82
+ * @param ctx The main request body to look through.
83
+ * @returns If an appId was found it will be returned.
84
84
  */
85
85
  export async function getAppIdFromCtx(ctx: Ctx) {
86
86
  // look in headers
@@ -135,7 +135,7 @@ function parseAppIdFromUrl(url?: string) {
135
135
 
136
136
  /**
137
137
  * opens the contents of the specified encrypted JWT.
138
- * @return {object} the contents of the token.
138
+ * @return the contents of the token.
139
139
  */
140
140
  export function openJwt(token: string) {
141
141
  if (!token) {
@@ -169,8 +169,8 @@ export function isValidInternalAPIKey(apiKey: string) {
169
169
 
170
170
  /**
171
171
  * Get a cookie from context, and decrypt if necessary.
172
- * @param {object} ctx The request which is to be manipulated.
173
- * @param {string} name The name of the cookie to get.
172
+ * @param ctx The request which is to be manipulated.
173
+ * @param name The name of the cookie to get.
174
174
  */
175
175
  export function getCookie(ctx: Ctx, name: string) {
176
176
  const cookie = ctx.cookies.get(name)
@@ -184,10 +184,10 @@ export function getCookie(ctx: Ctx, name: string) {
184
184
 
185
185
  /**
186
186
  * Store a cookie for the request - it will not expire.
187
- * @param {object} ctx The request which is to be manipulated.
188
- * @param {string} name The name of the cookie to set.
189
- * @param {string|object} value The value of cookie which will be set.
190
- * @param {object} opts options like whether to sign.
187
+ * @param ctx The request which is to be manipulated.
188
+ * @param name The name of the cookie to set.
189
+ * @param value The value of cookie which will be set.
190
+ * @param opts options like whether to sign.
191
191
  */
192
192
  export function setCookie(
193
193
  ctx: Ctx,
@@ -223,8 +223,8 @@ export function clearCookie(ctx: Ctx, name: string) {
223
223
  /**
224
224
  * Checks if the API call being made (based on the provided ctx object) is from the client. If
225
225
  * the call is not from a client app then it is from the builder.
226
- * @param {object} ctx The koa context object to be tested.
227
- * @return {boolean} returns true if the call is from the client lib (a built app rather than the builder).
226
+ * @param ctx The koa context object to be tested.
227
+ * @return returns true if the call is from the client lib (a built app rather than the builder).
228
228
  */
229
229
  export function isClient(ctx: Ctx) {
230
230
  return ctx.headers[Header.TYPE] === "client"
@@ -0,0 +1,54 @@
1
+ const _ = require('lodash/fp')
2
+ const {structures} = require("../../../tests")
3
+
4
+ jest.mock("../../../src/context")
5
+ jest.mock("../../../src/db")
6
+
7
+ const context = require("../../../src/context")
8
+ const db = require("../../../src/db")
9
+
10
+ const {getCreatorCount} = require('../../../src/users/users')
11
+
12
+ describe("Users", () => {
13
+
14
+ let getGlobalDBMock
15
+ let getGlobalUserParamsMock
16
+ let paginationMock
17
+
18
+ beforeEach(() => {
19
+ jest.resetAllMocks()
20
+
21
+ getGlobalDBMock = jest.spyOn(context, "getGlobalDB")
22
+ getGlobalUserParamsMock = jest.spyOn(db, "getGlobalUserParams")
23
+ paginationMock = jest.spyOn(db, "pagination")
24
+ })
25
+
26
+ it("Retrieves the number of creators", async () => {
27
+ const getUsers = (offset, limit, creators = false) => {
28
+ const range = _.range(offset, limit)
29
+ const opts = creators ? {builder: {global: true}} : undefined
30
+ return range.map(() => structures.users.user(opts))
31
+ }
32
+ const page1Data = getUsers(0, 8)
33
+ const page2Data = getUsers(8, 12, true)
34
+ getGlobalDBMock.mockImplementation(() => ({
35
+ name : "fake-db",
36
+ allDocs: () => ({
37
+ rows: [...page1Data, ...page2Data]
38
+ })
39
+ }))
40
+ paginationMock.mockImplementationOnce(() => ({
41
+ data: page1Data,
42
+ hasNextPage: true,
43
+ nextPage: "1"
44
+ }))
45
+ paginationMock.mockImplementation(() => ({
46
+ data: page2Data,
47
+ hasNextPage: false,
48
+ nextPage: undefined
49
+ }))
50
+ const creatorsCount = await getCreatorCount()
51
+ expect(creatorsCount).toBe(4)
52
+ expect(paginationMock).toHaveBeenCalledTimes(2)
53
+ })
54
+ })
@@ -72,6 +72,11 @@ export function quotas(): Quotas {
72
72
  value: 1,
73
73
  triggers: [],
74
74
  },
75
+ creators: {
76
+ name: "Creators",
77
+ value: 1,
78
+ triggers: [],
79
+ },
75
80
  userGroups: {
76
81
  name: "User Groups",
77
82
  value: 1,
@@ -118,6 +123,10 @@ export function customer(): Customer {
118
123
  export function subscription(): Subscription {
119
124
  return {
120
125
  amount: 10000,
126
+ amounts: {
127
+ user: 10000,
128
+ creator: 0,
129
+ },
121
130
  cancelAt: undefined,
122
131
  currency: "usd",
123
132
  currentPeriodEnd: 0,
@@ -126,6 +135,10 @@ export function subscription(): Subscription {
126
135
  duration: PriceDuration.MONTHLY,
127
136
  pastDueAt: undefined,
128
137
  quantity: 0,
138
+ quantities: {
139
+ user: 0,
140
+ creator: 0,
141
+ },
129
142
  status: "active",
130
143
  }
131
144
  }
@@ -1,6 +1,6 @@
1
1
  import { MonthlyQuotaName, QuotaUsage } from "@budibase/types"
2
2
 
3
- export const usage = (): QuotaUsage => {
3
+ export const usage = (users: number = 0, creators: number = 0): QuotaUsage => {
4
4
  return {
5
5
  _id: "usage_quota",
6
6
  quotaReset: new Date().toISOString(),
@@ -58,7 +58,8 @@ export const usage = (): QuotaUsage => {
58
58
  usageQuota: {
59
59
  apps: 0,
60
60
  plugins: 0,
61
- users: 0,
61
+ users,
62
+ creators,
62
63
  userGroups: 0,
63
64
  rows: 0,
64
65
  triggers: {},