@budibase/worker 2.3.18-alpha.16 → 2.3.18-alpha.17

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/worker",
3
3
  "email": "hi@budibase.com",
4
- "version": "2.3.18-alpha.16",
4
+ "version": "2.3.18-alpha.17",
5
5
  "description": "Budibase background service",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -36,10 +36,10 @@
36
36
  "author": "Budibase",
37
37
  "license": "GPL-3.0",
38
38
  "dependencies": {
39
- "@budibase/backend-core": "2.3.18-alpha.16",
40
- "@budibase/pro": "2.3.18-alpha.15",
41
- "@budibase/string-templates": "2.3.18-alpha.16",
42
- "@budibase/types": "2.3.18-alpha.16",
39
+ "@budibase/backend-core": "2.3.18-alpha.17",
40
+ "@budibase/pro": "2.3.18-alpha.16",
41
+ "@budibase/string-templates": "2.3.18-alpha.17",
42
+ "@budibase/types": "2.3.18-alpha.17",
43
43
  "@koa/router": "8.0.8",
44
44
  "@sentry/node": "6.17.7",
45
45
  "@techpass/passport-openidconnect": "0.3.2",
@@ -101,5 +101,5 @@
101
101
  "typescript": "4.7.3",
102
102
  "update-dotenv": "1.1.1"
103
103
  },
104
- "gitHead": "c350c15ae0869875936c804b89fcd46e4da4d8bb"
104
+ "gitHead": "f703ebb73409290aa949c240c51b95c7ce3826c8"
105
105
  }
@@ -1,4 +1,9 @@
1
- import { checkInviteCode } from "../../../utilities/redis"
1
+ import {
2
+ checkInviteCode,
3
+ getInviteCodes,
4
+ updateInviteCode,
5
+ } from "../../../utilities/redis"
6
+ // import sdk from "../../../sdk"
2
7
  import * as userSdk from "../../../sdk/users"
3
8
  import env from "../../../environment"
4
9
  import {
@@ -28,6 +33,7 @@ import {
28
33
  platform,
29
34
  } from "@budibase/backend-core"
30
35
  import { checkAnyUserExists } from "../../../utilities/users"
36
+ import { isEmailConfigured } from "../../../utilities/email"
31
37
 
32
38
  const MAX_USERS_UPLOAD_LIMIT = 1000
33
39
 
@@ -179,16 +185,28 @@ export const destroy = async (ctx: any) => {
179
185
  }
180
186
  }
181
187
 
188
+ export const getAppUsers = async (ctx: any) => {
189
+ const body = ctx.request.body as SearchUsersRequest
190
+ const users = await userSdk.getUsersByAppAccess(body?.appId)
191
+
192
+ ctx.body = { data: users }
193
+ }
194
+
182
195
  export const search = async (ctx: any) => {
183
196
  const body = ctx.request.body as SearchUsersRequest
184
- const paginated = await userSdk.paginatedUsers(body)
185
- // user hashed password shouldn't ever be returned
186
- for (let user of paginated.data) {
187
- if (user) {
188
- delete user.password
197
+
198
+ if (body.paginated === false) {
199
+ await getAppUsers(ctx)
200
+ } else {
201
+ const paginated = await userSdk.paginatedUsers(body)
202
+ // user hashed password shouldn't ever be returned
203
+ for (let user of paginated.data) {
204
+ if (user) {
205
+ delete user.password
206
+ }
189
207
  }
208
+ ctx.body = paginated
190
209
  }
191
- ctx.body = paginated
192
210
  }
193
211
 
194
212
  // called internally by app server user fetch
@@ -218,9 +236,71 @@ export const tenantUserLookup = async (ctx: any) => {
218
236
  }
219
237
  }
220
238
 
239
+ /*
240
+ Encapsulate the app user onboarding flows here.
241
+ */
242
+ export const onboardUsers = async (ctx: any) => {
243
+ const request = ctx.request.body as InviteUsersRequest | BulkUserRequest
244
+ const isBulkCreate = "create" in request
245
+
246
+ const emailConfigured = await isEmailConfigured()
247
+
248
+ let onboardingResponse
249
+
250
+ if (isBulkCreate) {
251
+ // @ts-ignore
252
+ const { users, groups, roles } = request.create
253
+ const assignUsers = users.map((user: User) => (user.roles = roles))
254
+ onboardingResponse = await userSdk.bulkCreate(assignUsers, groups)
255
+ ctx.body = onboardingResponse
256
+ } else if (emailConfigured) {
257
+ onboardingResponse = await inviteMultiple(ctx)
258
+ } else if (!emailConfigured) {
259
+ const inviteRequest = ctx.request.body as InviteUsersRequest
260
+
261
+ let createdPasswords: any = {}
262
+
263
+ const users: User[] = inviteRequest.map(invite => {
264
+ let password = Math.random().toString(36).substring(2, 22)
265
+
266
+ // Temp password to be passed to the user.
267
+ createdPasswords[invite.email] = password
268
+
269
+ return {
270
+ email: invite.email,
271
+ password,
272
+ forceResetPassword: true,
273
+ roles: invite.userInfo.apps,
274
+ admin: { global: false },
275
+ builder: { global: false },
276
+ tenantId: tenancy.getTenantId(),
277
+ }
278
+ })
279
+ let bulkCreateReponse = await userSdk.bulkCreate(users, [])
280
+
281
+ // Apply temporary credentials
282
+ let createWithCredentials = {
283
+ ...bulkCreateReponse,
284
+ successful: bulkCreateReponse?.successful.map(user => {
285
+ return {
286
+ ...user,
287
+ password: createdPasswords[user.email],
288
+ }
289
+ }),
290
+ created: true,
291
+ }
292
+
293
+ ctx.body = createWithCredentials
294
+ } else {
295
+ ctx.throw(400, "User onboarding failed")
296
+ }
297
+ }
298
+
221
299
  export const invite = async (ctx: any) => {
222
300
  const request = ctx.request.body as InviteUserRequest
223
- const response = await userSdk.invite([request])
301
+
302
+ let multiRequest = [request] as InviteUsersRequest
303
+ const response = await userSdk.invite(multiRequest)
224
304
 
225
305
  // explicitly throw for single user invite
226
306
  if (response.unsuccessful.length) {
@@ -234,6 +314,8 @@ export const invite = async (ctx: any) => {
234
314
 
235
315
  ctx.body = {
236
316
  message: "Invitation has been sent.",
317
+ successful: response.successful,
318
+ unsuccessful: response.unsuccessful,
237
319
  }
238
320
  }
239
321
 
@@ -255,6 +337,53 @@ export const checkInvite = async (ctx: any) => {
255
337
  }
256
338
  }
257
339
 
340
+ export const getUserInvites = async (ctx: any) => {
341
+ let invites
342
+ try {
343
+ // Restricted to the currently authenticated tenant
344
+ invites = await getInviteCodes([ctx.user.tenantId])
345
+ } catch (e) {
346
+ ctx.throw(400, "There was a problem fetching invites")
347
+ }
348
+ ctx.body = invites
349
+ }
350
+
351
+ export const updateInvite = async (ctx: any) => {
352
+ const { code } = ctx.params
353
+ let updateBody = { ...ctx.request.body }
354
+
355
+ delete updateBody.email
356
+
357
+ let invite
358
+ try {
359
+ invite = await checkInviteCode(code, false)
360
+ if (!invite) {
361
+ throw new Error("The invite could not be retrieved")
362
+ }
363
+ } catch (e) {
364
+ ctx.throw(400, "There was a problem with the invite")
365
+ }
366
+
367
+ let updated = {
368
+ ...invite,
369
+ }
370
+
371
+ if (!updateBody?.apps || !Object.keys(updateBody?.apps).length) {
372
+ updated.info.apps = []
373
+ } else {
374
+ updated.info = {
375
+ ...invite.info,
376
+ apps: {
377
+ ...invite.info.apps,
378
+ ...updateBody.apps,
379
+ },
380
+ }
381
+ }
382
+
383
+ await updateInviteCode(code, updated)
384
+ ctx.body = { ...invite }
385
+ }
386
+
258
387
  export const inviteAccept = async (
259
388
  ctx: Ctx<AcceptUserInviteRequest, AcceptUserInviteResponse>
260
389
  ) => {
@@ -263,13 +392,23 @@ export const inviteAccept = async (
263
392
  // info is an extension of the user object that was stored by global
264
393
  const { email, info }: any = await checkInviteCode(inviteCode)
265
394
  const user = await tenancy.doInTenant(info.tenantId, async () => {
266
- const saved = await userSdk.save({
395
+ let request = {
267
396
  firstName,
268
397
  lastName,
269
398
  password,
270
399
  email,
400
+ roles: info.apps,
401
+ tenantId: info.tenantId,
402
+ }
403
+
404
+ delete info.apps
405
+
406
+ request = {
407
+ ...request,
271
408
  ...info,
272
- })
409
+ }
410
+
411
+ const saved = await userSdk.save(request)
273
412
  const db = tenancy.getGlobalDB()
274
413
  const user = await db.get(saved._id)
275
414
  await events.user.inviteAccepted(user)
@@ -30,7 +30,11 @@ describe("/api/global/users", () => {
30
30
  email
31
31
  )
32
32
 
33
- expect(res.body).toEqual({ message: "Invitation has been sent." })
33
+ expect(res.body?.message).toBe("Invitation has been sent.")
34
+ expect(res.body?.unsuccessful.length).toBe(0)
35
+ expect(res.body?.successful.length).toBe(1)
36
+ expect(res.body?.successful[0].email).toBe(email)
37
+
34
38
  expect(sendMailMock).toHaveBeenCalled()
35
39
  expect(code).toBeDefined()
36
40
  expect(events.user.invited).toBeCalledTimes(1)
@@ -38,13 +38,6 @@ function buildInviteMultipleValidation() {
38
38
  ))
39
39
  }
40
40
 
41
- function buildInviteLookupValidation() {
42
- // prettier-ignore
43
- return auth.joiValidator.params(Joi.object({
44
- code: Joi.string().required()
45
- }).unknown(true))
46
- }
47
-
48
41
  const createUserAdminOnly = (ctx: any, next: any) => {
49
42
  if (!ctx.request.body._id) {
50
43
  return auth.adminOnly(ctx, next)
@@ -88,22 +81,34 @@ router
88
81
  .get("/api/global/roles/:appId")
89
82
  .post(
90
83
  "/api/global/users/invite",
91
- auth.adminOnly,
84
+ auth.builderOrAdmin,
92
85
  buildInviteValidation(),
93
86
  controller.invite
94
87
  )
88
+ .post(
89
+ "/api/global/users/onboard",
90
+ auth.builderOrAdmin,
91
+ buildInviteMultipleValidation(),
92
+ controller.onboardUsers
93
+ )
95
94
  .post(
96
95
  "/api/global/users/multi/invite",
97
- auth.adminOnly,
96
+ auth.builderOrAdmin,
98
97
  buildInviteMultipleValidation(),
99
98
  controller.inviteMultiple
100
99
  )
101
100
 
102
101
  // non-global endpoints
102
+ .get("/api/global/users/invite/:code", controller.checkInvite)
103
+ .post(
104
+ "/api/global/users/invite/update/:code",
105
+ auth.builderOrAdmin,
106
+ controller.updateInvite
107
+ )
103
108
  .get(
104
- "/api/global/users/invite/:code",
105
- buildInviteLookupValidation(),
106
- controller.checkInvite
109
+ "/api/global/users/invites",
110
+ auth.builderOrAdmin,
111
+ controller.getUserInvites
107
112
  )
108
113
  .post(
109
114
  "/api/global/users/invite/accept",
@@ -57,11 +57,22 @@ export const countUsersByApp = async (appId: string) => {
57
57
  }
58
58
  }
59
59
 
60
+ export const getUsersByAppAccess = async (appId?: string) => {
61
+ const opts: any = {
62
+ include_docs: true,
63
+ limit: 50,
64
+ }
65
+ let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
66
+ appId,
67
+ opts
68
+ )
69
+ return response
70
+ }
71
+
60
72
  export const paginatedUsers = async ({
61
73
  page,
62
74
  email,
63
75
  appId,
64
- userIds,
65
76
  }: SearchUsersRequest = {}) => {
66
77
  const db = tenancy.getGlobalDB()
67
78
  // get one extra document, to have the next page
@@ -234,7 +245,7 @@ export const save = async (
234
245
  const tenantId = tenancy.getTenantId()
235
246
  const db = tenancy.getGlobalDB()
236
247
 
237
- let { email, _id, userGroups = [] } = user
248
+ let { email, _id, userGroups = [], roles } = user
238
249
 
239
250
  if (!email && !_id) {
240
251
  throw new Error("_id or email is required")
@@ -276,6 +287,10 @@ export const save = async (
276
287
  builtUser.roles = dbUser.roles
277
288
  }
278
289
 
290
+ if (!dbUser && roles?.length) {
291
+ builtUser.roles = { ...roles }
292
+ }
293
+
279
294
  // make sure we set the _id field for a new user
280
295
  // Also if this is a new user, associate groups with them
281
296
  let groupPromises = []
@@ -7,7 +7,7 @@ function getExpirySecondsForDB(db: string) {
7
7
  return 3600
8
8
  case redis.utils.Databases.INVITATIONS:
9
9
  // a day
10
- return 86400
10
+ return 604800
11
11
  }
12
12
  }
13
13
 
@@ -29,6 +29,20 @@ async function writeACode(db: string, value: any) {
29
29
  return code
30
30
  }
31
31
 
32
+ async function updateACode(db: string, code: string, value: any) {
33
+ const client = await getClient(db)
34
+ await client.store(code, value, getExpirySecondsForDB(db))
35
+ }
36
+
37
+ /**
38
+ * Given an invite code and invite body, allow the update an existing/valid invite in redis
39
+ * @param {string} inviteCode The invite code for an invite in redis
40
+ * @param {object} value The body of the updated user invitation
41
+ */
42
+ export async function updateInviteCode(inviteCode: string, value: string) {
43
+ await updateACode(redis.utils.Databases.INVITATIONS, inviteCode, value)
44
+ }
45
+
32
46
  async function getACode(db: string, code: string, deleteCode = true) {
33
47
  const client = await getClient(db)
34
48
  const value = await client.get(code)
@@ -113,3 +127,27 @@ export async function checkInviteCode(
113
127
  throw "Invitation is not valid or has expired, please request a new one."
114
128
  }
115
129
  }
130
+
131
+ /**
132
+ Get all currently available user invitations.
133
+ @return {Object[]} A list of all objects containing invite metadata
134
+ **/
135
+ export async function getInviteCodes(tenantIds?: string[]) {
136
+ const client = await getClient(redis.utils.Databases.INVITATIONS)
137
+ const invites: any[] = await client.scan()
138
+
139
+ const results = invites.map(invite => {
140
+ return {
141
+ ...invite.value,
142
+ code: invite.key,
143
+ }
144
+ })
145
+ return results.reduce((acc, invite) => {
146
+ if (tenantIds?.length && tenantIds.includes(invite.info.tenantId)) {
147
+ acc.push(invite)
148
+ } else {
149
+ acc.push(invite)
150
+ }
151
+ return acc
152
+ }, [])
153
+ }