@budibase/worker 1.2.58 → 1.3.1

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 +26 -55
  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 +512 -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 +271 -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 +111 -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,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,9 +14,28 @@ import {
14
14
  HTTPError,
15
15
  accounts,
16
16
  migrations,
17
+ StaticDatabases,
18
+ ViewName,
19
+ events,
17
20
  } from "@budibase/backend-core"
18
- import { MigrationType, User } from "@budibase/types"
21
+ import {
22
+ MigrationType,
23
+ PlatformUserByEmail,
24
+ User,
25
+ BulkCreateUsersResponse,
26
+ CreateUserResponse,
27
+ BulkDeleteUsersResponse,
28
+ CloudAccount,
29
+ AllDocsResponse,
30
+ RowResponse,
31
+ BulkDocsResponse,
32
+ AccountMetadata,
33
+ InviteUsersRequest,
34
+ InviteUsersResponse,
35
+ } from "@budibase/types"
19
36
  import { groups as groupUtils } from "@budibase/pro"
37
+ import { sendEmail } from "../../utilities/email"
38
+ import { EmailTemplatePurpose } from "../../constants"
20
39
 
21
40
  const PAGE_LIMIT = 8
22
41
 
@@ -98,7 +117,6 @@ export const getUser = async (userId: string) => {
98
117
  interface SaveUserOpts {
99
118
  hashPassword?: boolean
100
119
  requirePassword?: boolean
101
- bulkCreate?: boolean
102
120
  }
103
121
 
104
122
  const buildUser = async (
@@ -109,7 +127,7 @@ const buildUser = async (
109
127
  },
110
128
  tenantId: string,
111
129
  dbUser?: any
112
- ) => {
130
+ ): Promise<User> => {
113
131
  let { password, _id } = user
114
132
 
115
133
  let hashedPassword
@@ -143,62 +161,63 @@ const buildUser = async (
143
161
  return user
144
162
  }
145
163
 
164
+ const validateUniqueUser = async (email: string, tenantId: string) => {
165
+ // check budibase users in other tenants
166
+ if (env.MULTI_TENANCY) {
167
+ const tenantUser = await tenancy.getTenantUser(email)
168
+ if (tenantUser != null && tenantUser.tenantId !== tenantId) {
169
+ throw `Unavailable`
170
+ }
171
+ }
172
+
173
+ // check root account users in account portal
174
+ if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
175
+ const account = await accounts.getAccount(email)
176
+ if (account && account.verified && account.tenantId !== tenantId) {
177
+ throw `Unavailable`
178
+ }
179
+ }
180
+ }
181
+
146
182
  export const save = async (
147
- user: any,
183
+ user: User,
148
184
  opts: SaveUserOpts = {
149
185
  hashPassword: true,
150
186
  requirePassword: true,
151
- bulkCreate: false,
152
187
  }
153
- ) => {
188
+ ): Promise<CreateUserResponse> => {
154
189
  const tenantId = tenancy.getTenantId()
155
190
  const db = tenancy.getGlobalDB()
156
191
  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
192
 
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
- }
193
+ let dbUser: User | undefined
194
+ if (_id) {
195
+ // try to get existing user from db
196
+ dbUser = (await db.get(_id)) as User
197
+ if (email && dbUser.email !== email) {
198
+ throw "Email address cannot be changed"
174
199
  }
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
- }
200
+ email = dbUser.email
201
+ } else if (email) {
202
+ // no id was specified - load from email instead
203
+ dbUser = await usersCore.getGlobalUserByEmail(email)
204
+ if (dbUser && dbUser._id !== _id) {
205
+ throw `Unavailable`
182
206
  }
183
- } else if (_id) {
184
- dbUser = await db.get(_id)
207
+ } else {
208
+ throw new Error("_id or email is required")
185
209
  }
186
210
 
211
+ await validateUniqueUser(email, tenantId)
212
+
187
213
  let builtUser = await buildUser(user, opts, tenantId, dbUser)
188
214
 
189
215
  // make sure we set the _id field for a new user
190
216
  if (!_id) {
191
- _id = builtUser._id
217
+ _id = builtUser._id!
192
218
  }
193
219
 
194
220
  try {
195
- const putOpts = {
196
- password: builtUser.password,
197
- ...user,
198
- }
199
- if (opts.bulkCreate) {
200
- return putOpts
201
- }
202
221
  // save the user to db
203
222
  let response
204
223
  const putUserFn = () => {
@@ -247,29 +266,87 @@ export const addTenant = async (
247
266
  }
248
267
  }
249
268
 
269
+ const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
270
+ return dbUtils.queryGlobalView(ViewName.USER_BY_EMAIL, {
271
+ keys: emails,
272
+ include_docs: true,
273
+ arrayResponse: true,
274
+ })
275
+ }
276
+
277
+ const getExistingPlatformUsers = async (
278
+ emails: string[]
279
+ ): Promise<PlatformUserByEmail[]> => {
280
+ return dbUtils.doWithDB(
281
+ StaticDatabases.PLATFORM_INFO.name,
282
+ async (infoDb: any) => {
283
+ const response: AllDocsResponse<PlatformUserByEmail> =
284
+ await infoDb.allDocs({
285
+ keys: emails,
286
+ include_docs: true,
287
+ })
288
+ return response.rows
289
+ .filter(row => row.doc && (row.error !== "not_found") !== null)
290
+ .map((row: any) => row.doc)
291
+ }
292
+ )
293
+ }
294
+
295
+ const getExistingAccounts = async (
296
+ emails: string[]
297
+ ): Promise<AccountMetadata[]> => {
298
+ return dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, {
299
+ keys: emails,
300
+ include_docs: true,
301
+ arrayResponse: true,
302
+ })
303
+ }
304
+
305
+ /**
306
+ * Apply a system-wide search on emails:
307
+ * - in tenant
308
+ * - cross tenant
309
+ * - accounts
310
+ * return an array of emails that match the supplied emails.
311
+ */
312
+ const searchExistingEmails = async (emails: string[]) => {
313
+ let matchedEmails: string[] = []
314
+
315
+ const existingTenantUsers = await getExistingTenantUsers(emails)
316
+ matchedEmails.push(...existingTenantUsers.map(user => user.email))
317
+
318
+ const existingPlatformUsers = await getExistingPlatformUsers(emails)
319
+ matchedEmails.push(...existingPlatformUsers.map(user => user._id!))
320
+
321
+ const existingAccounts = await getExistingAccounts(emails)
322
+ matchedEmails.push(...existingAccounts.map(account => account.email))
323
+
324
+ return [...new Set(matchedEmails)]
325
+ }
326
+
250
327
  export const bulkCreate = async (
251
328
  newUsersRequested: User[],
252
329
  groups: string[]
253
- ) => {
330
+ ): Promise<BulkCreateUsersResponse> => {
254
331
  const db = tenancy.getGlobalDB()
255
332
  const tenantId = tenancy.getTenantId()
256
333
 
257
334
  let usersToSave: any[] = []
258
335
  let newUsers: any[] = []
259
336
 
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)
337
+ const emails = newUsersRequested.map((user: User) => user.email)
338
+ const existingEmails = await searchExistingEmails(emails)
339
+ const unsuccessful: { email: string; reason: string }[] = []
266
340
 
267
- const currentUserEmails = mapped.map((x: any) => x.email) || []
268
341
  for (const newUser of newUsersRequested) {
269
342
  if (
270
343
  newUsers.find((x: any) => x.email === newUser.email) ||
271
- currentUserEmails.includes(newUser.email)
344
+ existingEmails.includes(newUser.email)
272
345
  ) {
346
+ unsuccessful.push({
347
+ email: newUser.email,
348
+ reason: `Unavailable`,
349
+ })
273
350
  continue
274
351
  }
275
352
  newUser.userGroups = groups
@@ -307,63 +384,130 @@ export const bulkCreate = async (
307
384
  await apps.syncUserInApps(user._id)
308
385
  }
309
386
 
310
- return usersToBulkSave.map(user => {
387
+ const saved = usersToBulkSave.map(user => {
311
388
  return {
312
389
  _id: user._id,
313
390
  email: user.email,
314
391
  }
315
392
  })
393
+
394
+ return {
395
+ successful: saved,
396
+ unsuccessful,
397
+ }
398
+ }
399
+
400
+ /**
401
+ * For the given user id's, return the account holder if it is in the ids.
402
+ */
403
+ const getAccountHolderFromUserIds = async (
404
+ userIds: string[]
405
+ ): Promise<CloudAccount | undefined> => {
406
+ if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
407
+ const tenantId = tenancy.getTenantId()
408
+ const account = await accounts.getAccountByTenantId(tenantId)
409
+ if (!account) {
410
+ throw new Error(`Account not found for tenantId=${tenantId}`)
411
+ }
412
+
413
+ const budibaseUserId = account.budibaseUserId
414
+ if (userIds.includes(budibaseUserId)) {
415
+ return account
416
+ }
417
+ }
316
418
  }
317
419
 
318
- export const bulkDelete = async (userIds: any) => {
420
+ export const bulkDelete = async (
421
+ userIds: string[]
422
+ ): Promise<BulkDeleteUsersResponse> => {
319
423
  const db = tenancy.getGlobalDB()
320
424
 
425
+ const response: BulkDeleteUsersResponse = {
426
+ successful: [],
427
+ unsuccessful: [],
428
+ }
429
+
430
+ // remove the account holder from the delete request if present
431
+ const account = await getAccountHolderFromUserIds(userIds)
432
+ if (account) {
433
+ userIds = userIds.filter(u => u !== account.budibaseUserId)
434
+ // mark user as unsuccessful
435
+ response.unsuccessful.push({
436
+ _id: account.budibaseUserId,
437
+ email: account.email,
438
+ reason: "Account holder cannot be deleted",
439
+ })
440
+ }
441
+
321
442
  let groupsToModify: any = {}
322
443
  let builderCount = 0
444
+
323
445
  // 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]
446
+ const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
447
+ include_docs: true,
448
+ keys: userIds,
449
+ })
450
+ const usersToDelete: User[] = allDocsResponse.rows.map(
451
+ (user: RowResponse<User>) => {
452
+ // if we find a user that has an associated group, add it to
453
+ // an array so we can easily use allDocs on them later.
454
+ // This prevents us having to re-loop over all the users
455
+ if (user.doc.userGroups) {
456
+ for (let groupId of user.doc.userGroups) {
457
+ if (!Object.keys(groupsToModify).includes(groupId)) {
458
+ groupsToModify[groupId] = [user.id]
459
+ } else {
460
+ groupsToModify[groupId] = [...groupsToModify[groupId], user.id]
461
+ }
339
462
  }
340
463
  }
341
- }
342
464
 
343
- // Also figure out how many builders are being deleted
344
- if (eventHelpers.isAddingBuilder(user.doc, null)) {
345
- builderCount++
346
- }
465
+ // Also figure out how many builders are being deleted
466
+ if (eventHelpers.isAddingBuilder(user.doc, null)) {
467
+ builderCount++
468
+ }
347
469
 
348
- return user.doc
349
- })
470
+ return user.doc
471
+ }
472
+ )
350
473
 
351
- const response = await db.bulkDocs(
352
- usersToDelete.map((user: any) => ({
474
+ // Delete from DB
475
+ const dbResponse: BulkDocsResponse = await db.bulkDocs(
476
+ usersToDelete.map(user => ({
353
477
  ...user,
354
478
  _deleted: true,
355
479
  }))
356
480
  )
357
481
 
482
+ // Deletion post processing
358
483
  await groupUtils.bulkDeleteGroupUsers(groupsToModify)
359
-
360
- //Deletion post processing
361
484
  for (let user of usersToDelete) {
362
485
  await bulkDeleteProcessing(user)
363
486
  }
364
-
365
487
  await quotas.removeDevelopers(builderCount)
366
488
 
489
+ // Build Response
490
+ // index users by id
491
+ const userIndex: { [key: string]: User } = {}
492
+ usersToDelete.reduce((prev, current) => {
493
+ prev[current._id!] = current
494
+ return prev
495
+ }, userIndex)
496
+
497
+ // add the successful and unsuccessful users to response
498
+ dbResponse.forEach(item => {
499
+ const email = userIndex[item.id].email
500
+ if (item.ok) {
501
+ response.successful.push({ _id: item.id, email })
502
+ } else {
503
+ response.unsuccessful.push({
504
+ _id: item.id,
505
+ email,
506
+ reason: "Database error",
507
+ })
508
+ }
509
+ })
510
+
367
511
  return response
368
512
  }
369
513
 
@@ -411,3 +555,53 @@ const bulkDeleteProcessing = async (dbUser: User) => {
411
555
  // let server know to sync user
412
556
  await apps.syncUserInApps(userId)
413
557
  }
558
+
559
+ export const invite = async (
560
+ users: InviteUsersRequest
561
+ ): Promise<InviteUsersResponse> => {
562
+ const response: InviteUsersResponse = {
563
+ successful: [],
564
+ unsuccessful: [],
565
+ }
566
+
567
+ const matchedEmails = await searchExistingEmails(users.map(u => u.email))
568
+ const newUsers = []
569
+
570
+ // separate duplicates from new users
571
+ for (let user of users) {
572
+ if (matchedEmails.includes(user.email)) {
573
+ response.unsuccessful.push({ email: user.email, reason: "Unavailable" })
574
+ } else {
575
+ newUsers.push(user)
576
+ }
577
+ }
578
+ // overwrite users with new only
579
+ users = newUsers
580
+
581
+ // send the emails for new users
582
+ const tenantId = tenancy.getTenantId()
583
+ for (let user of users) {
584
+ try {
585
+ let userInfo = user.userInfo
586
+ if (!userInfo) {
587
+ userInfo = {}
588
+ }
589
+ userInfo.tenantId = tenantId
590
+ const opts: any = {
591
+ subject: "{{ company }} platform invitation",
592
+ info: userInfo,
593
+ }
594
+ await sendEmail(user.email, EmailTemplatePurpose.INVITATION, opts)
595
+ response.successful.push({ email: user.email })
596
+ await events.user.invited()
597
+ } catch (e) {
598
+ console.error(`Failed to send email invitation email=${user.email}`, e)
599
+ response.unsuccessful.push({
600
+ email: user.email,
601
+ reason: "Failed to send email",
602
+ })
603
+ }
604
+ }
605
+
606
+ return response
607
+ }