@budibase/worker 2.8.31-alpha.0 → 2.8.31
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/jest.config.ts +2 -0
- package/nodemon.json +6 -9
- package/package.json +27 -12
- package/scripts/test.sh +2 -2
- package/src/api/controllers/global/auth.ts +5 -5
- package/src/api/controllers/global/configs.ts +3 -3
- package/src/api/controllers/global/email.ts +3 -3
- package/src/api/controllers/global/roles.ts +7 -7
- package/src/api/controllers/global/self.ts +4 -4
- package/src/api/controllers/global/users.ts +14 -14
- package/src/api/routes/global/tests/auth.spec.ts +3 -3
- package/src/api/routes/global/tests/scim.spec.ts +2 -2
- package/src/api/routes/global/tests/self.spec.ts +2 -2
- package/src/api/routes/global/tests/users.spec.ts +2 -2
- package/src/api/routes/index.ts +0 -8
- package/src/api/routes/system/tests/status.spec.ts +4 -7
- package/src/environment.ts +1 -5
- package/src/initPro.ts +8 -1
- package/src/migrations/functions/globalInfoSyncUsers.ts +1 -1
- package/src/sdk/auth/auth.ts +9 -8
- package/src/sdk/users/events.ts +176 -0
- package/src/sdk/users/index.ts +0 -5
- package/src/sdk/users/tests/users.spec.ts +8 -8
- package/src/sdk/users/users.ts +590 -7
- package/src/tests/TestConfiguration.ts +4 -4
- package/src/tests/api/users.ts +0 -20
- package/src/tests/mocks/index.ts +1 -1
- package/tsconfig.json +4 -2
- package/src/api/controllers/system/logs.ts +0 -13
- package/src/api/routes/global/tests/appBuilder.spec.ts +0 -62
- package/src/api/routes/system/logs.ts +0 -9
package/src/sdk/users/users.ts
CHANGED
|
@@ -1,19 +1,602 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import env from "../../environment"
|
|
2
|
+
import * as eventHelpers from "./events"
|
|
3
|
+
import {
|
|
4
|
+
accounts,
|
|
5
|
+
cache,
|
|
6
|
+
constants,
|
|
7
|
+
db as dbUtils,
|
|
8
|
+
events,
|
|
9
|
+
HTTPError,
|
|
10
|
+
sessions,
|
|
11
|
+
tenancy,
|
|
12
|
+
platform,
|
|
13
|
+
users as usersCore,
|
|
14
|
+
utils,
|
|
15
|
+
ViewName,
|
|
16
|
+
env as coreEnv,
|
|
17
|
+
context,
|
|
18
|
+
EmailUnavailableError,
|
|
19
|
+
} from "@budibase/backend-core"
|
|
20
|
+
import {
|
|
21
|
+
AccountMetadata,
|
|
22
|
+
AllDocsResponse,
|
|
23
|
+
CloudAccount,
|
|
24
|
+
InviteUsersRequest,
|
|
25
|
+
InviteUsersResponse,
|
|
26
|
+
isSSOAccount,
|
|
27
|
+
isSSOUser,
|
|
28
|
+
PlatformUser,
|
|
29
|
+
PlatformUserByEmail,
|
|
30
|
+
RowResponse,
|
|
31
|
+
User,
|
|
32
|
+
SaveUserOpts,
|
|
33
|
+
BulkUserCreated,
|
|
34
|
+
BulkUserDeleted,
|
|
35
|
+
Account,
|
|
36
|
+
} from "@budibase/types"
|
|
3
37
|
import { sendEmail } from "../../utilities/email"
|
|
4
38
|
import { EmailTemplatePurpose } from "../../constants"
|
|
39
|
+
import * as pro from "@budibase/pro"
|
|
40
|
+
import * as accountSdk from "../accounts"
|
|
5
41
|
|
|
6
|
-
export async
|
|
42
|
+
export const allUsers = async () => {
|
|
43
|
+
const db = tenancy.getGlobalDB()
|
|
44
|
+
const response = await db.allDocs(
|
|
45
|
+
dbUtils.getGlobalUserParams(null, {
|
|
46
|
+
include_docs: true,
|
|
47
|
+
})
|
|
48
|
+
)
|
|
49
|
+
return response.rows.map((row: any) => row.doc)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const countUsersByApp = async (appId: string) => {
|
|
53
|
+
let response: any = await usersCore.searchGlobalUsersByApp(appId, {})
|
|
54
|
+
return {
|
|
55
|
+
userCount: response.length,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const getUsersByAppAccess = async (appId?: string) => {
|
|
60
|
+
const opts: any = {
|
|
61
|
+
include_docs: true,
|
|
62
|
+
limit: 50,
|
|
63
|
+
}
|
|
64
|
+
let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
|
|
65
|
+
appId,
|
|
66
|
+
opts
|
|
67
|
+
)
|
|
68
|
+
return response
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getUserByEmail(email: string) {
|
|
72
|
+
return usersCore.getGlobalUserByEmail(email)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Gets a user by ID from the global database, based on the current tenancy.
|
|
77
|
+
*/
|
|
78
|
+
export const getUser = async (userId: string) => {
|
|
79
|
+
const user = await usersCore.getById(userId)
|
|
80
|
+
if (user) {
|
|
81
|
+
delete user.password
|
|
82
|
+
}
|
|
83
|
+
return user
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const buildUser = async (
|
|
87
|
+
user: User,
|
|
88
|
+
opts: SaveUserOpts = {
|
|
89
|
+
hashPassword: true,
|
|
90
|
+
requirePassword: true,
|
|
91
|
+
},
|
|
92
|
+
tenantId: string,
|
|
93
|
+
dbUser?: any,
|
|
94
|
+
account?: Account
|
|
95
|
+
): Promise<User> => {
|
|
96
|
+
let { password, _id } = user
|
|
97
|
+
|
|
98
|
+
// don't require a password if the db user doesn't already have one
|
|
99
|
+
if (dbUser && !dbUser.password) {
|
|
100
|
+
opts.requirePassword = false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let hashedPassword
|
|
104
|
+
if (password) {
|
|
105
|
+
if (await isPreventPasswordActions(user, account)) {
|
|
106
|
+
throw new HTTPError("Password change is disabled for this user", 400)
|
|
107
|
+
}
|
|
108
|
+
hashedPassword = opts.hashPassword ? await utils.hash(password) : password
|
|
109
|
+
} else if (dbUser) {
|
|
110
|
+
hashedPassword = dbUser.password
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// passwords are never required if sso is enforced
|
|
114
|
+
const requirePasswords =
|
|
115
|
+
opts.requirePassword && !(await pro.features.isSSOEnforced())
|
|
116
|
+
if (!hashedPassword && requirePasswords) {
|
|
117
|
+
throw "Password must be specified."
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_id = _id || dbUtils.generateGlobalUserID()
|
|
121
|
+
|
|
122
|
+
const fullUser = {
|
|
123
|
+
createdAt: Date.now(),
|
|
124
|
+
...dbUser,
|
|
125
|
+
...user,
|
|
126
|
+
_id,
|
|
127
|
+
password: hashedPassword,
|
|
128
|
+
tenantId,
|
|
129
|
+
}
|
|
130
|
+
// make sure the roles object is always present
|
|
131
|
+
if (!fullUser.roles) {
|
|
132
|
+
fullUser.roles = {}
|
|
133
|
+
}
|
|
134
|
+
// add the active status to a user if its not provided
|
|
135
|
+
if (fullUser.status == null) {
|
|
136
|
+
fullUser.status = constants.UserStatus.ACTIVE
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return fullUser
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// lookup, could be email or userId, either will return a doc
|
|
143
|
+
export const getPlatformUser = async (
|
|
144
|
+
identifier: string
|
|
145
|
+
): Promise<PlatformUser | null> => {
|
|
146
|
+
// use the view here and allow to find anyone regardless of casing
|
|
147
|
+
// Use lowercase to ensure email login is case insensitive
|
|
148
|
+
const response = dbUtils.queryPlatformView(
|
|
149
|
+
ViewName.PLATFORM_USERS_LOWERCASE,
|
|
150
|
+
{
|
|
151
|
+
keys: [identifier.toLowerCase()],
|
|
152
|
+
include_docs: true,
|
|
153
|
+
}
|
|
154
|
+
) as Promise<PlatformUser>
|
|
155
|
+
return response
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const validateUniqueUser = async (email: string, tenantId: string) => {
|
|
159
|
+
// check budibase users in other tenants
|
|
160
|
+
if (env.MULTI_TENANCY) {
|
|
161
|
+
const tenantUser = await getPlatformUser(email)
|
|
162
|
+
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
|
|
163
|
+
throw new EmailUnavailableError(email)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// check root account users in account portal
|
|
168
|
+
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
169
|
+
const account = await accounts.getAccount(email)
|
|
170
|
+
if (account && account.verified && account.tenantId !== tenantId) {
|
|
171
|
+
throw new EmailUnavailableError(email)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function isPreventPasswordActions(user: User, account?: Account) {
|
|
177
|
+
// when in maintenance mode we allow sso users with the admin role
|
|
178
|
+
// to perform any password action - this prevents lockout
|
|
179
|
+
if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) {
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// SSO is enforced for all users
|
|
184
|
+
if (await pro.features.isSSOEnforced()) {
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check local sso
|
|
189
|
+
if (isSSOUser(user)) {
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check account sso
|
|
194
|
+
if (!account) {
|
|
195
|
+
account = await accountSdk.api.getAccountByTenantId(tenancy.getTenantId())
|
|
196
|
+
}
|
|
197
|
+
return !!(account && account.email === user.email && isSSOAccount(account))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// TODO: The single save should re-use the bulk insert with a single
|
|
201
|
+
// user so that we don't need to duplicate logic
|
|
202
|
+
export const save = async (
|
|
203
|
+
user: User,
|
|
204
|
+
opts: SaveUserOpts = {}
|
|
205
|
+
): Promise<User> => {
|
|
206
|
+
// default booleans to true
|
|
207
|
+
if (opts.hashPassword == null) {
|
|
208
|
+
opts.hashPassword = true
|
|
209
|
+
}
|
|
210
|
+
if (opts.requirePassword == null) {
|
|
211
|
+
opts.requirePassword = true
|
|
212
|
+
}
|
|
213
|
+
const tenantId = tenancy.getTenantId()
|
|
214
|
+
const db = tenancy.getGlobalDB()
|
|
215
|
+
|
|
216
|
+
let { email, _id, userGroups = [], roles } = user
|
|
217
|
+
|
|
218
|
+
if (!email && !_id) {
|
|
219
|
+
throw new Error("_id or email is required")
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let dbUser: User | undefined
|
|
223
|
+
if (_id) {
|
|
224
|
+
// try to get existing user from db
|
|
225
|
+
try {
|
|
226
|
+
dbUser = (await db.get(_id)) as User
|
|
227
|
+
if (email && dbUser.email !== email) {
|
|
228
|
+
throw "Email address cannot be changed"
|
|
229
|
+
}
|
|
230
|
+
email = dbUser.email
|
|
231
|
+
} catch (e: any) {
|
|
232
|
+
if (e.status === 404) {
|
|
233
|
+
// do nothing, save this new user with the id specified - required for SSO auth
|
|
234
|
+
} else {
|
|
235
|
+
throw e
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!dbUser && email) {
|
|
241
|
+
// no id was specified - load from email instead
|
|
242
|
+
dbUser = await usersCore.getGlobalUserByEmail(email)
|
|
243
|
+
if (dbUser && dbUser._id !== _id) {
|
|
244
|
+
throw new EmailUnavailableError(email)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const change = dbUser ? 0 : 1 // no change if there is existing user
|
|
249
|
+
return pro.quotas.addUsers(change, async () => {
|
|
250
|
+
await validateUniqueUser(email, tenantId)
|
|
251
|
+
|
|
252
|
+
let builtUser = await buildUser(user, opts, tenantId, dbUser)
|
|
253
|
+
// don't allow a user to update its own roles/perms
|
|
254
|
+
if (opts.currentUserId && opts.currentUserId === dbUser?._id) {
|
|
255
|
+
builtUser.builder = dbUser.builder
|
|
256
|
+
builtUser.admin = dbUser.admin
|
|
257
|
+
builtUser.roles = dbUser.roles
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!dbUser && roles?.length) {
|
|
261
|
+
builtUser.roles = { ...roles }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// make sure we set the _id field for a new user
|
|
265
|
+
// Also if this is a new user, associate groups with them
|
|
266
|
+
let groupPromises = []
|
|
267
|
+
if (!_id) {
|
|
268
|
+
_id = builtUser._id!
|
|
269
|
+
|
|
270
|
+
if (userGroups.length > 0) {
|
|
271
|
+
for (let groupId of userGroups) {
|
|
272
|
+
groupPromises.push(pro.groups.addUsers(groupId, [_id]))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
// save the user to db
|
|
279
|
+
let response = await db.put(builtUser)
|
|
280
|
+
builtUser._rev = response.rev
|
|
281
|
+
|
|
282
|
+
await eventHelpers.handleSaveEvents(builtUser, dbUser)
|
|
283
|
+
await platform.users.addUser(tenantId, builtUser._id!, builtUser.email)
|
|
284
|
+
await cache.user.invalidateUser(response.id)
|
|
285
|
+
|
|
286
|
+
await Promise.all(groupPromises)
|
|
287
|
+
|
|
288
|
+
// finally returned the saved user from the db
|
|
289
|
+
return db.get(builtUser._id!)
|
|
290
|
+
} catch (err: any) {
|
|
291
|
+
if (err.status === 409) {
|
|
292
|
+
throw "User exists already"
|
|
293
|
+
} else {
|
|
294
|
+
throw err
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
|
|
301
|
+
const lcEmails = emails.map(email => email.toLowerCase())
|
|
302
|
+
const params = {
|
|
303
|
+
keys: lcEmails,
|
|
304
|
+
include_docs: true,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const opts = {
|
|
308
|
+
arrayResponse: true,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return dbUtils.queryGlobalView(
|
|
312
|
+
ViewName.USER_BY_EMAIL,
|
|
313
|
+
params,
|
|
314
|
+
undefined,
|
|
315
|
+
opts
|
|
316
|
+
) as Promise<User[]>
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const getExistingPlatformUsers = async (
|
|
320
|
+
emails: string[]
|
|
321
|
+
): Promise<PlatformUserByEmail[]> => {
|
|
322
|
+
const lcEmails = emails.map(email => email.toLowerCase())
|
|
323
|
+
const params = {
|
|
324
|
+
keys: lcEmails,
|
|
325
|
+
include_docs: true,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const opts = {
|
|
329
|
+
arrayResponse: true,
|
|
330
|
+
}
|
|
331
|
+
return dbUtils.queryPlatformView(
|
|
332
|
+
ViewName.PLATFORM_USERS_LOWERCASE,
|
|
333
|
+
params,
|
|
334
|
+
opts
|
|
335
|
+
) as Promise<PlatformUserByEmail[]>
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const getExistingAccounts = async (
|
|
339
|
+
emails: string[]
|
|
340
|
+
): Promise<AccountMetadata[]> => {
|
|
341
|
+
const lcEmails = emails.map(email => email.toLowerCase())
|
|
342
|
+
const params = {
|
|
343
|
+
keys: lcEmails,
|
|
344
|
+
include_docs: true,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const opts = {
|
|
348
|
+
arrayResponse: true,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return dbUtils.queryPlatformView(
|
|
352
|
+
ViewName.ACCOUNT_BY_EMAIL,
|
|
353
|
+
params,
|
|
354
|
+
opts
|
|
355
|
+
) as Promise<AccountMetadata[]>
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Apply a system-wide search on emails:
|
|
360
|
+
* - in tenant
|
|
361
|
+
* - cross tenant
|
|
362
|
+
* - accounts
|
|
363
|
+
* return an array of emails that match the supplied emails.
|
|
364
|
+
*/
|
|
365
|
+
const searchExistingEmails = async (emails: string[]) => {
|
|
366
|
+
let matchedEmails: string[] = []
|
|
367
|
+
|
|
368
|
+
const existingTenantUsers = await getExistingTenantUsers(emails)
|
|
369
|
+
matchedEmails.push(...existingTenantUsers.map(user => user.email))
|
|
370
|
+
|
|
371
|
+
const existingPlatformUsers = await getExistingPlatformUsers(emails)
|
|
372
|
+
matchedEmails.push(...existingPlatformUsers.map(user => user._id!))
|
|
373
|
+
|
|
374
|
+
const existingAccounts = await getExistingAccounts(emails)
|
|
375
|
+
matchedEmails.push(...existingAccounts.map(account => account.email))
|
|
376
|
+
|
|
377
|
+
return [...new Set(matchedEmails.map(email => email.toLowerCase()))]
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export const bulkCreate = async (
|
|
381
|
+
newUsersRequested: User[],
|
|
382
|
+
groups: string[]
|
|
383
|
+
): Promise<BulkUserCreated> => {
|
|
384
|
+
const tenantId = tenancy.getTenantId()
|
|
385
|
+
|
|
386
|
+
let usersToSave: any[] = []
|
|
387
|
+
let newUsers: any[] = []
|
|
388
|
+
|
|
389
|
+
const emails = newUsersRequested.map((user: User) => user.email)
|
|
390
|
+
const existingEmails = await searchExistingEmails(emails)
|
|
391
|
+
const unsuccessful: { email: string; reason: string }[] = []
|
|
392
|
+
|
|
393
|
+
for (const newUser of newUsersRequested) {
|
|
394
|
+
if (
|
|
395
|
+
newUsers.find(
|
|
396
|
+
(x: User) => x.email.toLowerCase() === newUser.email.toLowerCase()
|
|
397
|
+
) ||
|
|
398
|
+
existingEmails.includes(newUser.email.toLowerCase())
|
|
399
|
+
) {
|
|
400
|
+
unsuccessful.push({
|
|
401
|
+
email: newUser.email,
|
|
402
|
+
reason: `Unavailable`,
|
|
403
|
+
})
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
newUser.userGroups = groups
|
|
407
|
+
newUsers.push(newUser)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const account = await accountSdk.api.getAccountByTenantId(tenantId)
|
|
411
|
+
return pro.quotas.addUsers(newUsers.length, async () => {
|
|
412
|
+
// create the promises array that will be called by bulkDocs
|
|
413
|
+
newUsers.forEach((user: any) => {
|
|
414
|
+
usersToSave.push(
|
|
415
|
+
buildUser(
|
|
416
|
+
user,
|
|
417
|
+
{
|
|
418
|
+
hashPassword: true,
|
|
419
|
+
requirePassword: user.requirePassword,
|
|
420
|
+
},
|
|
421
|
+
tenantId,
|
|
422
|
+
undefined, // no dbUser
|
|
423
|
+
account
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
const usersToBulkSave = await Promise.all(usersToSave)
|
|
429
|
+
await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
|
|
430
|
+
|
|
431
|
+
// Post-processing of bulk added users, e.g. events and cache operations
|
|
432
|
+
for (const user of usersToBulkSave) {
|
|
433
|
+
// TODO: Refactor to bulk insert users into the info db
|
|
434
|
+
// instead of relying on looping tenant creation
|
|
435
|
+
await platform.users.addUser(tenantId, user._id, user.email)
|
|
436
|
+
await eventHelpers.handleSaveEvents(user, undefined)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const saved = usersToBulkSave.map(user => {
|
|
440
|
+
return {
|
|
441
|
+
_id: user._id,
|
|
442
|
+
email: user.email,
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
// now update the groups
|
|
447
|
+
if (Array.isArray(saved) && groups) {
|
|
448
|
+
const groupPromises = []
|
|
449
|
+
const createdUserIds = saved.map(user => user._id)
|
|
450
|
+
for (let groupId of groups) {
|
|
451
|
+
groupPromises.push(pro.groups.addUsers(groupId, createdUserIds))
|
|
452
|
+
}
|
|
453
|
+
await Promise.all(groupPromises)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
successful: saved,
|
|
458
|
+
unsuccessful,
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* For the given user id's, return the account holder if it is in the ids.
|
|
465
|
+
*/
|
|
466
|
+
const getAccountHolderFromUserIds = async (
|
|
467
|
+
userIds: string[]
|
|
468
|
+
): Promise<CloudAccount | undefined> => {
|
|
469
|
+
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
470
|
+
const tenantId = tenancy.getTenantId()
|
|
471
|
+
const account = await accounts.getAccountByTenantId(tenantId)
|
|
472
|
+
if (!account) {
|
|
473
|
+
throw new Error(`Account not found for tenantId=${tenantId}`)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const budibaseUserId = account.budibaseUserId
|
|
477
|
+
if (userIds.includes(budibaseUserId)) {
|
|
478
|
+
return account
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export const bulkDelete = async (
|
|
484
|
+
userIds: string[]
|
|
485
|
+
): Promise<BulkUserDeleted> => {
|
|
486
|
+
const db = tenancy.getGlobalDB()
|
|
487
|
+
|
|
488
|
+
const response: BulkUserDeleted = {
|
|
489
|
+
successful: [],
|
|
490
|
+
unsuccessful: [],
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// remove the account holder from the delete request if present
|
|
494
|
+
const account = await getAccountHolderFromUserIds(userIds)
|
|
495
|
+
if (account) {
|
|
496
|
+
userIds = userIds.filter(u => u !== account.budibaseUserId)
|
|
497
|
+
// mark user as unsuccessful
|
|
498
|
+
response.unsuccessful.push({
|
|
499
|
+
_id: account.budibaseUserId,
|
|
500
|
+
email: account.email,
|
|
501
|
+
reason: "Account holder cannot be deleted",
|
|
502
|
+
})
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Get users and delete
|
|
506
|
+
const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
|
|
507
|
+
include_docs: true,
|
|
508
|
+
keys: userIds,
|
|
509
|
+
})
|
|
510
|
+
const usersToDelete: User[] = allDocsResponse.rows.map(
|
|
511
|
+
(user: RowResponse<User>) => {
|
|
512
|
+
return user.doc
|
|
513
|
+
}
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
// Delete from DB
|
|
517
|
+
const toDelete = usersToDelete.map(user => ({
|
|
518
|
+
...user,
|
|
519
|
+
_deleted: true,
|
|
520
|
+
}))
|
|
521
|
+
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
|
522
|
+
|
|
523
|
+
await pro.quotas.removeUsers(toDelete.length)
|
|
524
|
+
for (let user of usersToDelete) {
|
|
525
|
+
await bulkDeleteProcessing(user)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Build Response
|
|
529
|
+
// index users by id
|
|
530
|
+
const userIndex: { [key: string]: User } = {}
|
|
531
|
+
usersToDelete.reduce((prev, current) => {
|
|
532
|
+
prev[current._id!] = current
|
|
533
|
+
return prev
|
|
534
|
+
}, userIndex)
|
|
535
|
+
|
|
536
|
+
// add the successful and unsuccessful users to response
|
|
537
|
+
dbResponse.forEach(item => {
|
|
538
|
+
const email = userIndex[item.id].email
|
|
539
|
+
if (item.ok) {
|
|
540
|
+
response.successful.push({ _id: item.id, email })
|
|
541
|
+
} else {
|
|
542
|
+
response.unsuccessful.push({
|
|
543
|
+
_id: item.id,
|
|
544
|
+
email,
|
|
545
|
+
reason: "Database error",
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
return response
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// TODO: The single delete should re-use the bulk delete with a single
|
|
554
|
+
// user so that we don't need to duplicate logic
|
|
555
|
+
export const destroy = async (id: string) => {
|
|
556
|
+
const db = tenancy.getGlobalDB()
|
|
557
|
+
const dbUser = (await db.get(id)) as User
|
|
558
|
+
const userId = dbUser._id as string
|
|
559
|
+
|
|
560
|
+
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
561
|
+
// root account holder can't be deleted from inside budibase
|
|
562
|
+
const email = dbUser.email
|
|
563
|
+
const account = await accounts.getAccount(email)
|
|
564
|
+
if (account) {
|
|
565
|
+
if (dbUser.userId === context.getIdentity()!._id) {
|
|
566
|
+
throw new HTTPError('Please visit "Account" to delete this user', 400)
|
|
567
|
+
} else {
|
|
568
|
+
throw new HTTPError("Account holder cannot be deleted", 400)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
await platform.users.removeUser(dbUser)
|
|
574
|
+
|
|
575
|
+
await db.remove(userId, dbUser._rev)
|
|
576
|
+
|
|
577
|
+
await pro.quotas.removeUsers(1)
|
|
578
|
+
await eventHelpers.handleDeleteEvents(dbUser)
|
|
579
|
+
await cache.user.invalidateUser(userId)
|
|
580
|
+
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const bulkDeleteProcessing = async (dbUser: User) => {
|
|
584
|
+
const userId = dbUser._id as string
|
|
585
|
+
await platform.users.removeUser(dbUser)
|
|
586
|
+
await eventHelpers.handleDeleteEvents(dbUser)
|
|
587
|
+
await cache.user.invalidateUser(userId)
|
|
588
|
+
await sessions.invalidateSessions(userId, { reason: "bulk-deletion" })
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export const invite = async (
|
|
7
592
|
users: InviteUsersRequest
|
|
8
|
-
): Promise<InviteUsersResponse> {
|
|
593
|
+
): Promise<InviteUsersResponse> => {
|
|
9
594
|
const response: InviteUsersResponse = {
|
|
10
595
|
successful: [],
|
|
11
596
|
unsuccessful: [],
|
|
12
597
|
}
|
|
13
598
|
|
|
14
|
-
const matchedEmails = await
|
|
15
|
-
users.map(u => u.email)
|
|
16
|
-
)
|
|
599
|
+
const matchedEmails = await searchExistingEmails(users.map(u => u.email))
|
|
17
600
|
const newUsers = []
|
|
18
601
|
|
|
19
602
|
// separate duplicates from new users
|
|
@@ -251,9 +251,9 @@ class TestConfiguration {
|
|
|
251
251
|
})
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
async getUser(email: string): Promise<User> {
|
|
255
|
-
return context.doInTenant(this.getTenantId(),
|
|
256
|
-
return
|
|
254
|
+
async getUser(email: string): Promise<User | undefined> {
|
|
255
|
+
return context.doInTenant(this.getTenantId(), () => {
|
|
256
|
+
return users.getGlobalUserByEmail(email)
|
|
257
257
|
})
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -263,7 +263,7 @@ class TestConfiguration {
|
|
|
263
263
|
}
|
|
264
264
|
const response = await this._req(user, null, controllers.users.save)
|
|
265
265
|
const body = response as SaveUserResponse
|
|
266
|
-
return
|
|
266
|
+
return this.getUser(body.email) as Promise<User>
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
// CONFIGS
|
package/src/tests/api/users.ts
CHANGED
|
@@ -140,24 +140,4 @@ export class UserAPI extends TestAPI {
|
|
|
140
140
|
.expect("Content-Type", /json/)
|
|
141
141
|
.expect(opts?.status ? opts.status : 200)
|
|
142
142
|
}
|
|
143
|
-
|
|
144
|
-
grantBuilderToApp = (
|
|
145
|
-
userId: string,
|
|
146
|
-
appId: string,
|
|
147
|
-
statusCode: number = 200
|
|
148
|
-
) => {
|
|
149
|
-
return this.request
|
|
150
|
-
.post(`/api/global/users/${userId}/app/${appId}/builder`)
|
|
151
|
-
.set(this.config.defaultHeaders())
|
|
152
|
-
.expect("Content-Type", /json/)
|
|
153
|
-
.expect(statusCode)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
revokeBuilderFromApp = (userId: string, appId: string) => {
|
|
157
|
-
return this.request
|
|
158
|
-
.delete(`/api/global/users/${userId}/app/${appId}/builder`)
|
|
159
|
-
.set(this.config.defaultHeaders())
|
|
160
|
-
.expect("Content-Type", /json/)
|
|
161
|
-
.expect(200)
|
|
162
|
-
}
|
|
163
143
|
}
|
package/src/tests/mocks/index.ts
CHANGED
package/tsconfig.json
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
"extends": "./tsconfig.build.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
4
|
"composite": true,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"sourceMap": true,
|
|
5
7
|
"baseUrl": "."
|
|
6
8
|
},
|
|
7
9
|
"ts-node": {
|
|
8
10
|
"require": ["tsconfig-paths/register"],
|
|
9
11
|
"swc": true
|
|
10
12
|
},
|
|
11
|
-
"include": ["src/**/*"
|
|
12
|
-
"exclude": ["
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["dist"]
|
|
13
15
|
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { UserCtx } from "@budibase/types"
|
|
2
|
-
import { installation, logging } from "@budibase/backend-core"
|
|
3
|
-
|
|
4
|
-
export async function getLogs(ctx: UserCtx) {
|
|
5
|
-
const logReadStream = logging.system.getLogReadStream()
|
|
6
|
-
|
|
7
|
-
const { installId } = await installation.getInstall()
|
|
8
|
-
|
|
9
|
-
const fileName = `${installId}-${Date.now()}.log`
|
|
10
|
-
|
|
11
|
-
ctx.set("content-disposition", `attachment; filename=${fileName}`)
|
|
12
|
-
ctx.body = logReadStream
|
|
13
|
-
}
|