@budibase/backend-core 2.8.31 → 2.8.32-alpha.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.
Files changed (132) hide show
  1. package/dist/jest.config.js +10 -4
  2. package/dist/jest.config.js.map +1 -1
  3. package/dist/package.json +14 -26
  4. package/dist/src/auth/auth.js.map +1 -1
  5. package/dist/src/cache/appMetadata.d.ts +7 -1
  6. package/dist/src/cache/appMetadata.js +5 -8
  7. package/dist/src/cache/appMetadata.js.map +1 -1
  8. package/dist/src/cache/user.js.map +1 -1
  9. package/dist/src/constants/db.d.ts +1 -39
  10. package/dist/src/constants/db.js +8 -43
  11. package/dist/src/constants/db.js.map +1 -1
  12. package/dist/src/constants/misc.d.ts +2 -0
  13. package/dist/src/constants/misc.js +2 -0
  14. package/dist/src/constants/misc.js.map +1 -1
  15. package/dist/src/db/constants.d.ts +2 -0
  16. package/dist/src/db/constants.js +13 -0
  17. package/dist/src/db/constants.js.map +1 -0
  18. package/dist/src/db/couch/index.d.ts +1 -0
  19. package/dist/src/db/couch/index.js +1 -0
  20. package/dist/src/db/couch/index.js.map +1 -1
  21. package/dist/src/db/searchIndexes/searchIndexes.js.map +1 -1
  22. package/dist/src/db/utils.js +7 -2
  23. package/dist/src/db/utils.js.map +1 -1
  24. package/dist/src/db/views.d.ts +0 -1
  25. package/dist/src/db/views.js +1 -12
  26. package/dist/src/db/views.js.map +1 -1
  27. package/dist/src/environment.d.ts +11 -4
  28. package/dist/src/environment.js +21 -70
  29. package/dist/src/environment.js.map +1 -1
  30. package/dist/src/events/identification.js +3 -3
  31. package/dist/src/events/identification.js.map +1 -1
  32. package/dist/src/logging/index.d.ts +1 -1
  33. package/dist/src/logging/index.js +2 -3
  34. package/dist/src/logging/index.js.map +1 -1
  35. package/dist/src/logging/pino/logger.js +40 -24
  36. package/dist/src/logging/pino/logger.js.map +1 -1
  37. package/dist/src/logging/system.d.ts +9 -0
  38. package/dist/src/logging/system.js +101 -0
  39. package/dist/src/logging/system.js.map +1 -0
  40. package/dist/src/middleware/adminOnly.d.ts +2 -2
  41. package/dist/src/middleware/adminOnly.js +2 -2
  42. package/dist/src/middleware/adminOnly.js.map +1 -1
  43. package/dist/src/middleware/builderOnly.d.ts +2 -2
  44. package/dist/src/middleware/builderOnly.js +16 -2
  45. package/dist/src/middleware/builderOnly.js.map +1 -1
  46. package/dist/src/middleware/builderOrAdmin.d.ts +2 -2
  47. package/dist/src/middleware/builderOrAdmin.js +17 -4
  48. package/dist/src/middleware/builderOrAdmin.js.map +1 -1
  49. package/dist/src/security/permissions.d.ts +31 -18
  50. package/dist/src/security/permissions.js +46 -57
  51. package/dist/src/security/permissions.js.map +1 -1
  52. package/dist/src/security/roles.js +7 -4
  53. package/dist/src/security/roles.js.map +1 -1
  54. package/dist/src/users/db.d.ts +38 -0
  55. package/dist/src/users/db.js +407 -0
  56. package/dist/src/users/db.js.map +1 -0
  57. package/dist/src/users/events.d.ts +5 -0
  58. package/dist/src/users/events.js +169 -0
  59. package/dist/src/users/events.js.map +1 -0
  60. package/dist/src/users/index.d.ts +4 -0
  61. package/dist/src/users/index.js +23 -0
  62. package/dist/src/users/index.js.map +1 -0
  63. package/dist/src/users/lookup.d.ts +13 -0
  64. package/dist/src/users/lookup.js +112 -0
  65. package/dist/src/users/lookup.js.map +1 -0
  66. package/dist/src/{users.d.ts → users/users.d.ts} +4 -2
  67. package/dist/src/{users.js → users/users.js} +24 -4
  68. package/dist/src/users/users.js.map +1 -0
  69. package/dist/src/users/utils.d.ts +14 -0
  70. package/dist/src/users/utils.js +92 -0
  71. package/dist/src/users/utils.js.map +1 -0
  72. package/dist/tests/core/utilities/jestUtils.d.ts +7 -0
  73. package/dist/tests/core/utilities/jestUtils.js +14 -1
  74. package/dist/tests/core/utilities/jestUtils.js.map +1 -1
  75. package/dist/tests/core/utilities/mocks/events.d.ts +0 -1
  76. package/dist/tests/core/utilities/mocks/events.js +0 -1
  77. package/dist/tests/core/utilities/mocks/events.js.map +1 -1
  78. package/dist/tests/core/utilities/mocks/licenses.d.ts +1 -0
  79. package/dist/tests/core/utilities/mocks/licenses.js +8 -4
  80. package/dist/tests/core/utilities/mocks/licenses.js.map +1 -1
  81. package/dist/tests/core/utilities/structures/accounts.js +3 -3
  82. package/dist/tests/core/utilities/structures/accounts.js.map +1 -1
  83. package/dist/tests/core/utilities/structures/scim.js +1 -5
  84. package/dist/tests/core/utilities/structures/scim.js.map +1 -1
  85. package/dist/tests/core/utilities/structures/sso.js +2 -2
  86. package/dist/tests/core/utilities/structures/sso.js.map +1 -1
  87. package/dist/tests/core/utilities/structures/users.d.ts +3 -1
  88. package/dist/tests/core/utilities/structures/users.js +13 -1
  89. package/dist/tests/core/utilities/structures/users.js.map +1 -1
  90. package/dist/tsconfig.build.tsbuildinfo +1 -1
  91. package/jest.config.ts +1 -2
  92. package/package.json +14 -26
  93. package/scripts/test.sh +2 -2
  94. package/src/auth/auth.ts +1 -1
  95. package/src/cache/appMetadata.ts +10 -8
  96. package/src/cache/tests/writethrough.spec.ts +7 -7
  97. package/src/cache/user.ts +1 -1
  98. package/src/constants/db.ts +4 -42
  99. package/src/constants/misc.ts +2 -0
  100. package/src/db/constants.ts +10 -0
  101. package/src/db/couch/index.ts +1 -0
  102. package/src/db/searchIndexes/searchIndexes.ts +1 -1
  103. package/src/db/utils.ts +9 -3
  104. package/src/db/views.ts +0 -11
  105. package/src/environment.ts +24 -4
  106. package/src/events/identification.ts +3 -2
  107. package/src/logging/index.ts +1 -3
  108. package/src/logging/pino/logger.ts +45 -24
  109. package/src/logging/system.ts +81 -0
  110. package/src/logging/tests/system.spec.ts +61 -0
  111. package/src/middleware/adminOnly.ts +4 -6
  112. package/src/middleware/builderOnly.ts +15 -6
  113. package/src/middleware/builderOrAdmin.ts +16 -8
  114. package/src/middleware/tests/builder.spec.ts +180 -0
  115. package/src/security/permissions.ts +5 -21
  116. package/src/security/roles.ts +1 -1
  117. package/src/security/tests/permissions.spec.ts +1 -1
  118. package/src/users/db.ts +460 -0
  119. package/src/users/events.ts +176 -0
  120. package/src/users/index.ts +4 -0
  121. package/src/users/lookup.ts +102 -0
  122. package/src/{users.ts → users/users.ts} +33 -7
  123. package/src/users/utils.ts +55 -0
  124. package/tests/core/utilities/jestUtils.ts +21 -0
  125. package/tests/core/utilities/mocks/events.ts +0 -2
  126. package/tests/core/utilities/mocks/licenses.ts +7 -3
  127. package/tests/core/utilities/structures/accounts.ts +3 -5
  128. package/tests/core/utilities/structures/scim.ts +4 -5
  129. package/tests/core/utilities/structures/sso.ts +2 -2
  130. package/tests/core/utilities/structures/users.ts +19 -0
  131. package/tsconfig.json +2 -2
  132. package/dist/src/users.js.map +0 -1
@@ -0,0 +1,460 @@
1
+ import env from "../environment"
2
+ import * as eventHelpers from "./events"
3
+ import * as accounts from "../accounts"
4
+ import * as cache from "../cache"
5
+ import { getIdentity, getTenantId, getGlobalDB } from "../context"
6
+ import * as dbUtils from "../db"
7
+ import { EmailUnavailableError, HTTPError } from "../errors"
8
+ import * as platform from "../platform"
9
+ import * as sessions from "../security/sessions"
10
+ import * as usersCore from "./users"
11
+ import {
12
+ AllDocsResponse,
13
+ BulkUserCreated,
14
+ BulkUserDeleted,
15
+ RowResponse,
16
+ SaveUserOpts,
17
+ User,
18
+ Account,
19
+ isSSOUser,
20
+ isSSOAccount,
21
+ UserStatus,
22
+ } from "@budibase/types"
23
+ import * as accountSdk from "../accounts"
24
+ import {
25
+ validateUniqueUser,
26
+ getAccountHolderFromUserIds,
27
+ isAdmin,
28
+ } from "./utils"
29
+ import { searchExistingEmails } from "./lookup"
30
+ import { hash } from "../utils"
31
+
32
+ type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any>
33
+ type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
34
+ type FeatureFn = () => Promise<Boolean>
35
+ type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn }
36
+ type GroupFns = { addUsers: GroupUpdateFn }
37
+ type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
38
+
39
+ const bulkDeleteProcessing = async (dbUser: User) => {
40
+ const userId = dbUser._id as string
41
+ await platform.users.removeUser(dbUser)
42
+ await eventHelpers.handleDeleteEvents(dbUser)
43
+ await cache.user.invalidateUser(userId)
44
+ await sessions.invalidateSessions(userId, { reason: "bulk-deletion" })
45
+ }
46
+
47
+ export class UserDB {
48
+ static quotas: QuotaFns
49
+ static groups: GroupFns
50
+ static features: FeatureFns
51
+
52
+ static init(quotaFns: QuotaFns, groupFns: GroupFns, featureFns: FeatureFns) {
53
+ UserDB.quotas = quotaFns
54
+ UserDB.groups = groupFns
55
+ UserDB.features = featureFns
56
+ }
57
+
58
+ static async isPreventPasswordActions(user: User, account?: Account) {
59
+ // when in maintenance mode we allow sso users with the admin role
60
+ // to perform any password action - this prevents lockout
61
+ if (env.ENABLE_SSO_MAINTENANCE_MODE && isAdmin(user)) {
62
+ return false
63
+ }
64
+
65
+ // SSO is enforced for all users
66
+ if (await UserDB.features.isSSOEnforced()) {
67
+ return true
68
+ }
69
+
70
+ // Check local sso
71
+ if (isSSOUser(user)) {
72
+ return true
73
+ }
74
+
75
+ // Check account sso
76
+ if (!account) {
77
+ account = await accountSdk.getAccountByTenantId(getTenantId())
78
+ }
79
+ return !!(account && account.email === user.email && isSSOAccount(account))
80
+ }
81
+
82
+ static async buildUser(
83
+ user: User,
84
+ opts: SaveUserOpts = {
85
+ hashPassword: true,
86
+ requirePassword: true,
87
+ },
88
+ tenantId: string,
89
+ dbUser?: any,
90
+ account?: Account
91
+ ): Promise<User> {
92
+ let { password, _id } = user
93
+
94
+ // don't require a password if the db user doesn't already have one
95
+ if (dbUser && !dbUser.password) {
96
+ opts.requirePassword = false
97
+ }
98
+
99
+ let hashedPassword
100
+ if (password) {
101
+ if (await UserDB.isPreventPasswordActions(user, account)) {
102
+ throw new HTTPError("Password change is disabled for this user", 400)
103
+ }
104
+ hashedPassword = opts.hashPassword ? await hash(password) : password
105
+ } else if (dbUser) {
106
+ hashedPassword = dbUser.password
107
+ }
108
+
109
+ // passwords are never required if sso is enforced
110
+ const requirePasswords =
111
+ opts.requirePassword && !(await UserDB.features.isSSOEnforced())
112
+ if (!hashedPassword && requirePasswords) {
113
+ throw "Password must be specified."
114
+ }
115
+
116
+ _id = _id || dbUtils.generateGlobalUserID()
117
+
118
+ const fullUser = {
119
+ createdAt: Date.now(),
120
+ ...dbUser,
121
+ ...user,
122
+ _id,
123
+ password: hashedPassword,
124
+ tenantId,
125
+ }
126
+ // make sure the roles object is always present
127
+ if (!fullUser.roles) {
128
+ fullUser.roles = {}
129
+ }
130
+ // add the active status to a user if its not provided
131
+ if (fullUser.status == null) {
132
+ fullUser.status = UserStatus.ACTIVE
133
+ }
134
+
135
+ return fullUser
136
+ }
137
+
138
+ static async allUsers() {
139
+ const db = getGlobalDB()
140
+ const response = await db.allDocs(
141
+ dbUtils.getGlobalUserParams(null, {
142
+ include_docs: true,
143
+ })
144
+ )
145
+ return response.rows.map((row: any) => row.doc)
146
+ }
147
+
148
+ static async countUsersByApp(appId: string) {
149
+ let response: any = await usersCore.searchGlobalUsersByApp(appId, {})
150
+ return {
151
+ userCount: response.length,
152
+ }
153
+ }
154
+
155
+ static async getUsersByAppAccess(appId?: string) {
156
+ const opts: any = {
157
+ include_docs: true,
158
+ limit: 50,
159
+ }
160
+ let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
161
+ appId,
162
+ opts
163
+ )
164
+ return response
165
+ }
166
+
167
+ static async getUserByEmail(email: string) {
168
+ return usersCore.getGlobalUserByEmail(email)
169
+ }
170
+
171
+ /**
172
+ * Gets a user by ID from the global database, based on the current tenancy.
173
+ */
174
+ static async getUser(userId: string) {
175
+ const user = await usersCore.getById(userId)
176
+ if (user) {
177
+ delete user.password
178
+ }
179
+ return user
180
+ }
181
+
182
+ static async save(user: User, opts: SaveUserOpts = {}): Promise<User> {
183
+ // default booleans to true
184
+ if (opts.hashPassword == null) {
185
+ opts.hashPassword = true
186
+ }
187
+ if (opts.requirePassword == null) {
188
+ opts.requirePassword = true
189
+ }
190
+ const tenantId = getTenantId()
191
+ const db = getGlobalDB()
192
+
193
+ let { email, _id, userGroups = [], roles } = user
194
+
195
+ if (!email && !_id) {
196
+ throw new Error("_id or email is required")
197
+ }
198
+
199
+ if (
200
+ user.builder?.apps?.length &&
201
+ !(await UserDB.features.isAppBuildersEnabled())
202
+ ) {
203
+ throw new Error("Unable to update app builders, please check license")
204
+ }
205
+
206
+ let dbUser: User | undefined
207
+ if (_id) {
208
+ // try to get existing user from db
209
+ try {
210
+ dbUser = (await db.get(_id)) as User
211
+ if (email && dbUser.email !== email) {
212
+ throw "Email address cannot be changed"
213
+ }
214
+ email = dbUser.email
215
+ } catch (e: any) {
216
+ if (e.status === 404) {
217
+ // do nothing, save this new user with the id specified - required for SSO auth
218
+ } else {
219
+ throw e
220
+ }
221
+ }
222
+ }
223
+
224
+ if (!dbUser && email) {
225
+ // no id was specified - load from email instead
226
+ dbUser = await usersCore.getGlobalUserByEmail(email)
227
+ if (dbUser && dbUser._id !== _id) {
228
+ throw new EmailUnavailableError(email)
229
+ }
230
+ }
231
+
232
+ const change = dbUser ? 0 : 1 // no change if there is existing user
233
+ return UserDB.quotas.addUsers(change, async () => {
234
+ await validateUniqueUser(email, tenantId)
235
+
236
+ let builtUser = await UserDB.buildUser(user, opts, tenantId, dbUser)
237
+ // don't allow a user to update its own roles/perms
238
+ if (opts.currentUserId && opts.currentUserId === dbUser?._id) {
239
+ builtUser = usersCore.cleanseUserObject(builtUser, dbUser) as User
240
+ }
241
+
242
+ if (!dbUser && roles?.length) {
243
+ builtUser.roles = { ...roles }
244
+ }
245
+
246
+ // make sure we set the _id field for a new user
247
+ // Also if this is a new user, associate groups with them
248
+ let groupPromises = []
249
+ if (!_id) {
250
+ _id = builtUser._id!
251
+
252
+ if (userGroups.length > 0) {
253
+ for (let groupId of userGroups) {
254
+ groupPromises.push(UserDB.groups.addUsers(groupId, [_id!]))
255
+ }
256
+ }
257
+ }
258
+
259
+ try {
260
+ // save the user to db
261
+ let response = await db.put(builtUser)
262
+ builtUser._rev = response.rev
263
+
264
+ await eventHelpers.handleSaveEvents(builtUser, dbUser)
265
+ await platform.users.addUser(tenantId, builtUser._id!, builtUser.email)
266
+ await cache.user.invalidateUser(response.id)
267
+
268
+ await Promise.all(groupPromises)
269
+
270
+ // finally returned the saved user from the db
271
+ return db.get(builtUser._id!)
272
+ } catch (err: any) {
273
+ if (err.status === 409) {
274
+ throw "User exists already"
275
+ } else {
276
+ throw err
277
+ }
278
+ }
279
+ })
280
+ }
281
+
282
+ static async bulkCreate(
283
+ newUsersRequested: User[],
284
+ groups: string[]
285
+ ): Promise<BulkUserCreated> {
286
+ const tenantId = getTenantId()
287
+
288
+ let usersToSave: any[] = []
289
+ let newUsers: any[] = []
290
+
291
+ const emails = newUsersRequested.map((user: User) => user.email)
292
+ const existingEmails = await searchExistingEmails(emails)
293
+ const unsuccessful: { email: string; reason: string }[] = []
294
+
295
+ for (const newUser of newUsersRequested) {
296
+ if (
297
+ newUsers.find(
298
+ (x: User) => x.email.toLowerCase() === newUser.email.toLowerCase()
299
+ ) ||
300
+ existingEmails.includes(newUser.email.toLowerCase())
301
+ ) {
302
+ unsuccessful.push({
303
+ email: newUser.email,
304
+ reason: `Unavailable`,
305
+ })
306
+ continue
307
+ }
308
+ newUser.userGroups = groups
309
+ newUsers.push(newUser)
310
+ }
311
+
312
+ const account = await accountSdk.getAccountByTenantId(tenantId)
313
+ return UserDB.quotas.addUsers(newUsers.length, async () => {
314
+ // create the promises array that will be called by bulkDocs
315
+ newUsers.forEach((user: any) => {
316
+ usersToSave.push(
317
+ UserDB.buildUser(
318
+ user,
319
+ {
320
+ hashPassword: true,
321
+ requirePassword: user.requirePassword,
322
+ },
323
+ tenantId,
324
+ undefined, // no dbUser
325
+ account
326
+ )
327
+ )
328
+ })
329
+
330
+ const usersToBulkSave = await Promise.all(usersToSave)
331
+ await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
332
+
333
+ // Post-processing of bulk added users, e.g. events and cache operations
334
+ for (const user of usersToBulkSave) {
335
+ // TODO: Refactor to bulk insert users into the info db
336
+ // instead of relying on looping tenant creation
337
+ await platform.users.addUser(tenantId, user._id, user.email)
338
+ await eventHelpers.handleSaveEvents(user, undefined)
339
+ }
340
+
341
+ const saved = usersToBulkSave.map(user => {
342
+ return {
343
+ _id: user._id,
344
+ email: user.email,
345
+ }
346
+ })
347
+
348
+ // now update the groups
349
+ if (Array.isArray(saved) && groups) {
350
+ const groupPromises = []
351
+ const createdUserIds = saved.map(user => user._id)
352
+ for (let groupId of groups) {
353
+ groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds))
354
+ }
355
+ await Promise.all(groupPromises)
356
+ }
357
+
358
+ return {
359
+ successful: saved,
360
+ unsuccessful,
361
+ }
362
+ })
363
+ }
364
+
365
+ static async bulkDelete(userIds: string[]): Promise<BulkUserDeleted> {
366
+ const db = getGlobalDB()
367
+
368
+ const response: BulkUserDeleted = {
369
+ successful: [],
370
+ unsuccessful: [],
371
+ }
372
+
373
+ // remove the account holder from the delete request if present
374
+ const account = await getAccountHolderFromUserIds(userIds)
375
+ if (account) {
376
+ userIds = userIds.filter(u => u !== account.budibaseUserId)
377
+ // mark user as unsuccessful
378
+ response.unsuccessful.push({
379
+ _id: account.budibaseUserId,
380
+ email: account.email,
381
+ reason: "Account holder cannot be deleted",
382
+ })
383
+ }
384
+
385
+ // Get users and delete
386
+ const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
387
+ include_docs: true,
388
+ keys: userIds,
389
+ })
390
+ const usersToDelete: User[] = allDocsResponse.rows.map(
391
+ (user: RowResponse<User>) => {
392
+ return user.doc
393
+ }
394
+ )
395
+
396
+ // Delete from DB
397
+ const toDelete = usersToDelete.map(user => ({
398
+ ...user,
399
+ _deleted: true,
400
+ }))
401
+ const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
402
+
403
+ await UserDB.quotas.removeUsers(toDelete.length)
404
+ for (let user of usersToDelete) {
405
+ await bulkDeleteProcessing(user)
406
+ }
407
+
408
+ // Build Response
409
+ // index users by id
410
+ const userIndex: { [key: string]: User } = {}
411
+ usersToDelete.reduce((prev, current) => {
412
+ prev[current._id!] = current
413
+ return prev
414
+ }, userIndex)
415
+
416
+ // add the successful and unsuccessful users to response
417
+ dbResponse.forEach(item => {
418
+ const email = userIndex[item.id].email
419
+ if (item.ok) {
420
+ response.successful.push({ _id: item.id, email })
421
+ } else {
422
+ response.unsuccessful.push({
423
+ _id: item.id,
424
+ email,
425
+ reason: "Database error",
426
+ })
427
+ }
428
+ })
429
+
430
+ return response
431
+ }
432
+
433
+ static async destroy(id: string) {
434
+ const db = getGlobalDB()
435
+ const dbUser = (await db.get(id)) as User
436
+ const userId = dbUser._id as string
437
+
438
+ if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
439
+ // root account holder can't be deleted from inside budibase
440
+ const email = dbUser.email
441
+ const account = await accounts.getAccount(email)
442
+ if (account) {
443
+ if (dbUser.userId === getIdentity()!._id) {
444
+ throw new HTTPError('Please visit "Account" to delete this user', 400)
445
+ } else {
446
+ throw new HTTPError("Account holder cannot be deleted", 400)
447
+ }
448
+ }
449
+ }
450
+
451
+ await platform.users.removeUser(dbUser)
452
+
453
+ await db.remove(userId, dbUser._rev)
454
+
455
+ await UserDB.quotas.removeUsers(1)
456
+ await eventHelpers.handleDeleteEvents(dbUser)
457
+ await cache.user.invalidateUser(userId)
458
+ await sessions.invalidateSessions(userId, { reason: "deletion" })
459
+ }
460
+ }
@@ -0,0 +1,176 @@
1
+ import env from "../environment"
2
+ import * as events from "../events"
3
+ import * as accounts from "../accounts"
4
+ import { getTenantId } from "../context"
5
+ import { User, UserRoles, CloudAccount } from "@budibase/types"
6
+ import { hasBuilderPermissions, hasAdminPermissions } from "./utils"
7
+
8
+ export const handleDeleteEvents = async (user: any) => {
9
+ await events.user.deleted(user)
10
+
11
+ if (hasBuilderPermissions(user)) {
12
+ await events.user.permissionBuilderRemoved(user)
13
+ }
14
+
15
+ if (hasAdminPermissions(user)) {
16
+ await events.user.permissionAdminRemoved(user)
17
+ }
18
+ }
19
+
20
+ const assignAppRoleEvents = async (
21
+ user: User,
22
+ roles: UserRoles,
23
+ existingRoles: UserRoles
24
+ ) => {
25
+ for (const [appId, role] of Object.entries(roles)) {
26
+ // app role in existing is not same as new
27
+ if (!existingRoles || existingRoles[appId] !== role) {
28
+ await events.role.assigned(user, role)
29
+ }
30
+ }
31
+ }
32
+
33
+ const unassignAppRoleEvents = async (
34
+ user: User,
35
+ roles: UserRoles,
36
+ existingRoles: UserRoles
37
+ ) => {
38
+ if (!existingRoles) {
39
+ return
40
+ }
41
+ for (const [appId, role] of Object.entries(existingRoles)) {
42
+ // app role in new is not same as existing
43
+ if (!roles || roles[appId] !== role) {
44
+ await events.role.unassigned(user, role)
45
+ }
46
+ }
47
+ }
48
+
49
+ const handleAppRoleEvents = async (user: any, existingUser: any) => {
50
+ const roles = user.roles
51
+ const existingRoles = existingUser?.roles
52
+
53
+ await assignAppRoleEvents(user, roles, existingRoles)
54
+ await unassignAppRoleEvents(user, roles, existingRoles)
55
+ }
56
+
57
+ export const handleSaveEvents = async (
58
+ user: User,
59
+ existingUser: User | undefined
60
+ ) => {
61
+ const tenantId = getTenantId()
62
+ let tenantAccount: CloudAccount | undefined
63
+ if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
64
+ tenantAccount = await accounts.getAccountByTenantId(tenantId)
65
+ }
66
+ await events.identification.identifyUser(user, tenantAccount)
67
+
68
+ if (existingUser) {
69
+ await events.user.updated(user)
70
+
71
+ if (isRemovingBuilder(user, existingUser)) {
72
+ await events.user.permissionBuilderRemoved(user)
73
+ }
74
+
75
+ if (isRemovingAdmin(user, existingUser)) {
76
+ await events.user.permissionAdminRemoved(user)
77
+ }
78
+
79
+ if (isOnboardingComplete(user, existingUser)) {
80
+ await events.user.onboardingComplete(user)
81
+ }
82
+
83
+ if (
84
+ !existingUser.forceResetPassword &&
85
+ user.forceResetPassword &&
86
+ user.password
87
+ ) {
88
+ await events.user.passwordForceReset(user)
89
+ }
90
+
91
+ if (user.password !== existingUser.password) {
92
+ await events.user.passwordUpdated(user)
93
+ }
94
+ } else {
95
+ await events.user.created(user)
96
+ }
97
+
98
+ if (isAddingBuilder(user, existingUser)) {
99
+ await events.user.permissionBuilderAssigned(user)
100
+ }
101
+
102
+ if (isAddingAdmin(user, existingUser)) {
103
+ await events.user.permissionAdminAssigned(user)
104
+ }
105
+
106
+ await handleAppRoleEvents(user, existingUser)
107
+ }
108
+
109
+ export const isAddingBuilder = (user: any, existingUser: any) => {
110
+ return isAddingPermission(user, existingUser, hasBuilderPermissions)
111
+ }
112
+
113
+ export const isRemovingBuilder = (user: any, existingUser: any) => {
114
+ return isRemovingPermission(user, existingUser, hasBuilderPermissions)
115
+ }
116
+
117
+ const isAddingAdmin = (user: any, existingUser: any) => {
118
+ return isAddingPermission(user, existingUser, hasAdminPermissions)
119
+ }
120
+
121
+ const isRemovingAdmin = (user: any, existingUser: any) => {
122
+ return isRemovingPermission(user, existingUser, hasAdminPermissions)
123
+ }
124
+
125
+ const isOnboardingComplete = (user: any, existingUser: any) => {
126
+ return !existingUser?.onboardedAt && typeof user.onboardedAt === "string"
127
+ }
128
+
129
+ /**
130
+ * Check if a permission is being added to a new or existing user.
131
+ */
132
+ const isAddingPermission = (
133
+ user: any,
134
+ existingUser: any,
135
+ hasPermission: any
136
+ ) => {
137
+ // new user doesn't have the permission
138
+ if (!hasPermission(user)) {
139
+ return false
140
+ }
141
+
142
+ // existing user has the permission
143
+ if (existingUser && hasPermission(existingUser)) {
144
+ return false
145
+ }
146
+
147
+ // permission is being added
148
+ return true
149
+ }
150
+
151
+ /**
152
+ * Check if a permission is being removed from an existing user.
153
+ */
154
+ const isRemovingPermission = (
155
+ user: any,
156
+ existingUser: any,
157
+ hasPermission: any
158
+ ) => {
159
+ // new user has the permission
160
+ if (hasPermission(user)) {
161
+ return false
162
+ }
163
+
164
+ // no existing user or existing user doesn't have the permission
165
+ if (!existingUser) {
166
+ return false
167
+ }
168
+
169
+ // existing user doesn't have the permission
170
+ if (!hasPermission(existingUser)) {
171
+ return false
172
+ }
173
+
174
+ // permission is being removed
175
+ return true
176
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./users"
2
+ export * from "./utils"
3
+ export * from "./lookup"
4
+ export { UserDB } from "./db"