@budibase/worker 2.13.10 → 2.13.11

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.13.10",
4
+ "version": "2.13.11",
5
5
  "description": "Budibase background service",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -37,10 +37,10 @@
37
37
  "author": "Budibase",
38
38
  "license": "GPL-3.0",
39
39
  "dependencies": {
40
- "@budibase/backend-core": "2.13.10",
41
- "@budibase/pro": "2.13.10",
42
- "@budibase/string-templates": "2.13.10",
43
- "@budibase/types": "2.13.10",
40
+ "@budibase/backend-core": "2.13.11",
41
+ "@budibase/pro": "2.13.11",
42
+ "@budibase/string-templates": "2.13.11",
43
+ "@budibase/types": "2.13.11",
44
44
  "@koa/router": "8.0.8",
45
45
  "@techpass/passport-openidconnect": "0.3.2",
46
46
  "@types/global-agent": "2.1.1",
@@ -107,5 +107,5 @@
107
107
  }
108
108
  }
109
109
  },
110
- "gitHead": "cd1db2c1fc02e152cb156464cd43aba32d5ccdf0"
110
+ "gitHead": "230e8d171197a2db905600d2914decbca281f8e0"
111
111
  }
@@ -1,8 +1,3 @@
1
- import {
2
- checkInviteCode,
3
- getInviteCodes,
4
- updateInviteCode,
5
- } from "../../../utilities/redis"
6
1
  import * as userSdk from "../../../sdk/users"
7
2
  import env from "../../../environment"
8
3
  import {
@@ -16,6 +11,7 @@ import {
16
11
  Ctx,
17
12
  InviteUserRequest,
18
13
  InviteUsersRequest,
14
+ InviteUsersResponse,
19
15
  MigrationType,
20
16
  SaveUserResponse,
21
17
  SearchUsersRequest,
@@ -249,59 +245,35 @@ export const tenantUserLookup = async (ctx: any) => {
249
245
  /*
250
246
  Encapsulate the app user onboarding flows here.
251
247
  */
252
- export const onboardUsers = async (ctx: Ctx<InviteUsersRequest>) => {
253
- const request = ctx.request.body
254
- const isBulkCreate = "create" in request
255
-
256
- const emailConfigured = await isEmailConfigured()
257
-
258
- let onboardingResponse
259
-
260
- if (isBulkCreate) {
261
- // @ts-ignore
262
- const { users, groups, roles } = request.create
263
- const assignUsers = users.map((user: User) => (user.roles = roles))
264
- onboardingResponse = await userSdk.db.bulkCreate(assignUsers, groups)
265
- ctx.body = onboardingResponse
266
- } else if (emailConfigured) {
267
- onboardingResponse = await inviteMultiple(ctx)
268
- } else if (!emailConfigured) {
269
- const inviteRequest = ctx.request.body
270
-
271
- let createdPasswords: any = {}
272
-
273
- const users: User[] = inviteRequest.map(invite => {
274
- let password = Math.random().toString(36).substring(2, 22)
275
-
276
- // Temp password to be passed to the user.
277
- createdPasswords[invite.email] = password
278
-
279
- return {
280
- email: invite.email,
281
- password,
282
- forceResetPassword: true,
283
- roles: invite.userInfo.apps,
284
- admin: invite.userInfo.admin,
285
- builder: invite.userInfo.builder,
286
- tenantId: tenancy.getTenantId(),
287
- }
288
- })
289
- let bulkCreateReponse = await userSdk.db.bulkCreate(users, [])
248
+ export const onboardUsers = async (
249
+ ctx: Ctx<InviteUsersRequest, InviteUsersResponse>
250
+ ) => {
251
+ if (await isEmailConfigured()) {
252
+ await inviteMultiple(ctx)
253
+ return
254
+ }
290
255
 
291
- // Apply temporary credentials
292
- ctx.body = {
293
- ...bulkCreateReponse,
294
- successful: bulkCreateReponse?.successful.map(user => {
295
- return {
296
- ...user,
297
- password: createdPasswords[user.email],
298
- }
299
- }),
300
- created: true,
256
+ let createdPasswords: Record<string, string> = {}
257
+ const users: User[] = ctx.request.body.map(invite => {
258
+ let password = Math.random().toString(36).substring(2, 22)
259
+ createdPasswords[invite.email] = password
260
+
261
+ return {
262
+ email: invite.email,
263
+ password,
264
+ forceResetPassword: true,
265
+ roles: invite.userInfo.apps,
266
+ admin: invite.userInfo.admin,
267
+ builder: invite.userInfo.builder,
268
+ tenantId: tenancy.getTenantId(),
301
269
  }
302
- } else {
303
- ctx.throw(400, "User onboarding failed")
270
+ })
271
+
272
+ let resp = await userSdk.db.bulkCreate(users)
273
+ for (const user of resp.successful) {
274
+ user.password = createdPasswords[user.email]
304
275
  }
276
+ ctx.body = { ...resp, created: true }
305
277
  }
306
278
 
307
279
  export const invite = async (ctx: Ctx<InviteUserRequest>) => {
@@ -328,18 +300,18 @@ export const invite = async (ctx: Ctx<InviteUserRequest>) => {
328
300
  }
329
301
 
330
302
  export const inviteMultiple = async (ctx: Ctx<InviteUsersRequest>) => {
331
- const request = ctx.request.body
332
- ctx.body = await userSdk.invite(request)
303
+ ctx.body = await userSdk.invite(ctx.request.body)
333
304
  }
334
305
 
335
306
  export const checkInvite = async (ctx: any) => {
336
307
  const { code } = ctx.params
337
308
  let invite
338
309
  try {
339
- invite = await checkInviteCode(code, false)
310
+ invite = await cache.invite.getCode(code)
340
311
  } catch (e) {
341
312
  console.warn("Error getting invite from code", e)
342
313
  ctx.throw(400, "There was a problem with the invite")
314
+ return
343
315
  }
344
316
  ctx.body = {
345
317
  email: invite.email,
@@ -347,14 +319,12 @@ export const checkInvite = async (ctx: any) => {
347
319
  }
348
320
 
349
321
  export const getUserInvites = async (ctx: any) => {
350
- let invites
351
322
  try {
352
323
  // Restricted to the currently authenticated tenant
353
- invites = await getInviteCodes()
324
+ ctx.body = await cache.invite.getInviteCodes()
354
325
  } catch (e) {
355
326
  ctx.throw(400, "There was a problem fetching invites")
356
327
  }
357
- ctx.body = invites
358
328
  }
359
329
 
360
330
  export const updateInvite = async (ctx: any) => {
@@ -365,12 +335,10 @@ export const updateInvite = async (ctx: any) => {
365
335
 
366
336
  let invite
367
337
  try {
368
- invite = await checkInviteCode(code, false)
369
- if (!invite) {
370
- throw new Error("The invite could not be retrieved")
371
- }
338
+ invite = await cache.invite.getCode(code)
372
339
  } catch (e) {
373
340
  ctx.throw(400, "There was a problem with the invite")
341
+ return
374
342
  }
375
343
 
376
344
  let updated = {
@@ -395,7 +363,7 @@ export const updateInvite = async (ctx: any) => {
395
363
  }
396
364
  }
397
365
 
398
- await updateInviteCode(code, updated)
366
+ await cache.invite.updateCode(code, updated)
399
367
  ctx.body = { ...invite }
400
368
  }
401
369
 
@@ -405,7 +373,8 @@ export const inviteAccept = async (
405
373
  const { inviteCode, password, firstName, lastName } = ctx.request.body
406
374
  try {
407
375
  // info is an extension of the user object that was stored by global
408
- const { email, info }: any = await checkInviteCode(inviteCode)
376
+ const { email, info }: any = await cache.invite.getCode(inviteCode)
377
+ await cache.invite.deleteCode(inviteCode)
409
378
  const user = await tenancy.doInTenant(info.tenantId, async () => {
410
379
  let request: any = {
411
380
  firstName,
@@ -1,11 +1,12 @@
1
1
  import { InviteUsersResponse, User } from "@budibase/types"
2
2
 
3
- jest.mock("nodemailer")
4
3
  import { TestConfiguration, mocks, structures } from "../../../../tests"
5
- const sendMailMock = mocks.email.mock()
6
4
  import { events, tenancy, accounts as _accounts } from "@budibase/backend-core"
7
5
  import * as userSdk from "../../../../sdk/users"
8
6
 
7
+ jest.mock("nodemailer")
8
+ const sendMailMock = mocks.email.mock()
9
+
9
10
  const accounts = jest.mocked(_accounts)
10
11
 
11
12
  describe("/api/global/users", () => {
@@ -54,6 +55,24 @@ describe("/api/global/users", () => {
54
55
  expect(events.user.invited).toBeCalledTimes(0)
55
56
  })
56
57
 
58
+ it("should not invite the same user twice", async () => {
59
+ const email = structures.users.newEmail()
60
+ await config.api.users.sendUserInvite(sendMailMock, email)
61
+
62
+ jest.clearAllMocks()
63
+
64
+ const { code, res } = await config.api.users.sendUserInvite(
65
+ sendMailMock,
66
+ email,
67
+ 400
68
+ )
69
+
70
+ expect(res.body.message).toBe(`Unavailable`)
71
+ expect(sendMailMock).toHaveBeenCalledTimes(0)
72
+ expect(code).toBeUndefined()
73
+ expect(events.user.invited).toBeCalledTimes(0)
74
+ })
75
+
57
76
  it("should be able to create new user from invite", async () => {
58
77
  const email = structures.users.newEmail()
59
78
  const { code } = await config.api.users.sendUserInvite(
@@ -101,6 +120,23 @@ describe("/api/global/users", () => {
101
120
  expect(sendMailMock).toHaveBeenCalledTimes(0)
102
121
  expect(events.user.invited).toBeCalledTimes(0)
103
122
  })
123
+
124
+ it("should not be able to generate an invitation for user that has already been invited", async () => {
125
+ const email = structures.users.newEmail()
126
+ await config.api.users.sendUserInvite(sendMailMock, email)
127
+
128
+ jest.clearAllMocks()
129
+
130
+ const request = [{ email: email, userInfo: {} }]
131
+ const res = await config.api.users.sendMultiUserInvite(request)
132
+
133
+ const body = res.body as InviteUsersResponse
134
+ expect(body.successful.length).toBe(0)
135
+ expect(body.unsuccessful.length).toBe(1)
136
+ expect(body.unsuccessful[0].reason).toBe("Unavailable")
137
+ expect(sendMailMock).toHaveBeenCalledTimes(0)
138
+ expect(events.user.invited).toBeCalledTimes(0)
139
+ })
104
140
  })
105
141
 
106
142
  describe("POST /api/global/users/bulk", () => {
@@ -633,4 +669,25 @@ describe("/api/global/users", () => {
633
669
  expect(response.body.message).toBe("Unable to delete self.")
634
670
  })
635
671
  })
672
+
673
+ describe("POST /api/global/users/onboard", () => {
674
+ it("should successfully onboard a user", async () => {
675
+ const response = await config.api.users.onboardUser([
676
+ { email: structures.users.newEmail(), userInfo: {} },
677
+ ])
678
+ expect(response.successful.length).toBe(1)
679
+ expect(response.unsuccessful.length).toBe(0)
680
+ })
681
+
682
+ it("should not onboard a user who has been invited", async () => {
683
+ const email = structures.users.newEmail()
684
+ await config.api.users.sendUserInvite(sendMailMock, email)
685
+
686
+ const response = await config.api.users.onboardUser([
687
+ { email, userInfo: {} },
688
+ ])
689
+ expect(response.successful.length).toBe(0)
690
+ expect(response.unsuccessful.length).toBe(1)
691
+ })
692
+ })
636
693
  })
package/src/index.ts CHANGED
@@ -16,13 +16,13 @@ import {
16
16
  queue,
17
17
  env as coreEnv,
18
18
  timers,
19
+ redis,
19
20
  } from "@budibase/backend-core"
20
21
  db.init()
21
22
  import Koa from "koa"
22
23
  import koaBody from "koa-body"
23
24
  import http from "http"
24
25
  import api from "./api"
25
- import * as redis from "./utilities/redis"
26
26
 
27
27
  const koaSession = require("koa-session")
28
28
  import { userAgent } from "koa-useragent"
@@ -72,8 +72,8 @@ server.on("close", async () => {
72
72
  shuttingDown = true
73
73
  console.log("Server Closed")
74
74
  timers.cleanup()
75
- await redis.shutdown()
76
- await events.shutdown()
75
+ events.shutdown()
76
+ await redis.clients.shutdown()
77
77
  await queue.shutdown()
78
78
  if (!env.isTest()) {
79
79
  process.exit(errCode)
@@ -88,7 +88,7 @@ const shutdown = () => {
88
88
  export default server.listen(parseInt(env.PORT || "4002"), async () => {
89
89
  console.log(`Worker running on ${JSON.stringify(server.address())}`)
90
90
  await initPro()
91
- await redis.init()
91
+ await redis.clients.init()
92
92
  // configure events to use the pro audit log write
93
93
  // can't integrate directly into backend-core due to cyclic issues
94
94
  await events.processors.init(proSdk.auditLogs.write)
@@ -6,12 +6,12 @@ import {
6
6
  sessions,
7
7
  tenancy,
8
8
  utils as coreUtils,
9
+ cache,
9
10
  } from "@budibase/backend-core"
10
11
  import { PlatformLogoutOpts, User } from "@budibase/types"
11
12
  import jwt from "jsonwebtoken"
12
13
  import * as userSdk from "../users"
13
14
  import * as emails from "../../utilities/email"
14
- import * as redis from "../../utilities/redis"
15
15
  import { EmailTemplatePurpose } from "../../constants"
16
16
 
17
17
  // LOGIN / LOGOUT
@@ -73,7 +73,7 @@ export const reset = async (email: string) => {
73
73
  * Perform the user password update if the provided reset code is valid.
74
74
  */
75
75
  export const resetUpdate = async (resetCode: string, password: string) => {
76
- const { userId } = await redis.checkResetPasswordCode(resetCode)
76
+ const { userId } = await cache.passwordReset.getCode(resetCode)
77
77
 
78
78
  let user = await userSdk.db.getUser(userId)
79
79
  user.password = password
@@ -1,5 +1,9 @@
1
1
  import { events, tenancy, users as usersCore } from "@budibase/backend-core"
2
- import { InviteUsersRequest, InviteUsersResponse } from "@budibase/types"
2
+ import {
3
+ InviteUserRequest,
4
+ InviteUsersRequest,
5
+ InviteUsersResponse,
6
+ } from "@budibase/types"
3
7
  import { sendEmail } from "../../utilities/email"
4
8
  import { EmailTemplatePurpose } from "../../constants"
5
9
 
@@ -14,11 +18,13 @@ export async function invite(
14
18
  const matchedEmails = await usersCore.searchExistingEmails(
15
19
  users.map(u => u.email)
16
20
  )
17
- const newUsers = []
21
+ const newUsers: InviteUserRequest[] = []
18
22
 
19
23
  // separate duplicates from new users
20
24
  for (let user of users) {
21
25
  if (matchedEmails.includes(user.email)) {
26
+ // This "Unavailable" is load bearing. The tests and frontend both check for it
27
+ // specifically
22
28
  response.unsuccessful.push({ email: user.email, reason: "Unavailable" })
23
29
  } else {
24
30
  newUsers.push(user)
@@ -5,6 +5,7 @@ import {
5
5
  User,
6
6
  CreateAdminUserRequest,
7
7
  SearchQuery,
8
+ InviteUsersResponse,
8
9
  } from "@budibase/types"
9
10
  import structures from "../structures"
10
11
  import { generator } from "@budibase/backend-core/tests"
@@ -176,4 +177,24 @@ export class UserAPI extends TestAPI {
176
177
  .expect("Content-Type", /json/)
177
178
  .expect(200)
178
179
  }
180
+
181
+ onboardUser = async (
182
+ req: InviteUsersRequest
183
+ ): Promise<InviteUsersResponse> => {
184
+ const resp = await this.request
185
+ .post(`/api/global/users/onboard`)
186
+ .send(req)
187
+ .set(this.config.defaultHeaders())
188
+ .expect("Content-Type", /json/)
189
+
190
+ if (resp.status !== 200) {
191
+ throw new Error(
192
+ `request failed with status ${resp.status} and body ${JSON.stringify(
193
+ resp.body
194
+ )}`
195
+ )
196
+ }
197
+
198
+ return resp.body as InviteUsersResponse
199
+ }
179
200
  }
@@ -3,9 +3,8 @@ import { EmailTemplatePurpose, TemplateType } from "../constants"
3
3
  import { getTemplateByPurpose, EmailTemplates } from "../constants/templates"
4
4
  import { getSettingsTemplateContext } from "./templates"
5
5
  import { processString } from "@budibase/string-templates"
6
- import { getResetPasswordCode, getInviteCode } from "./redis"
7
6
  import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types"
8
- import { configs } from "@budibase/backend-core"
7
+ import { configs, cache } from "@budibase/backend-core"
9
8
  import ical from "ical-generator"
10
9
  const nodemailer = require("nodemailer")
11
10
 
@@ -61,9 +60,9 @@ async function getLinkCode(
61
60
  ) {
62
61
  switch (purpose) {
63
62
  case EmailTemplatePurpose.PASSWORD_RECOVERY:
64
- return getResetPasswordCode(user._id!, info)
63
+ return cache.passwordReset.createCode(user._id!, info)
65
64
  case EmailTemplatePurpose.INVITATION:
66
- return getInviteCode(email, info)
65
+ return cache.invite.createCode(email, info)
67
66
  default:
68
67
  return null
69
68
  }
@@ -1,150 +0,0 @@
1
- import { redis, utils, tenancy } from "@budibase/backend-core"
2
- import env from "../environment"
3
-
4
- function getExpirySecondsForDB(db: string) {
5
- switch (db) {
6
- case redis.utils.Databases.PW_RESETS:
7
- // a hour
8
- return 3600
9
- case redis.utils.Databases.INVITATIONS:
10
- // a week
11
- return 604800
12
- }
13
- }
14
-
15
- let pwResetClient: any, invitationClient: any
16
-
17
- function getClient(db: string) {
18
- switch (db) {
19
- case redis.utils.Databases.PW_RESETS:
20
- return pwResetClient
21
- case redis.utils.Databases.INVITATIONS:
22
- return invitationClient
23
- }
24
- }
25
-
26
- async function writeACode(db: string, value: any) {
27
- const client = await getClient(db)
28
- const code = utils.newid()
29
- await client.store(code, value, getExpirySecondsForDB(db))
30
- return code
31
- }
32
-
33
- async function updateACode(db: string, code: string, value: any) {
34
- const client = await getClient(db)
35
- await client.store(code, value, getExpirySecondsForDB(db))
36
- }
37
-
38
- /**
39
- * Given an invite code and invite body, allow the update an existing/valid invite in redis
40
- * @param inviteCode The invite code for an invite in redis
41
- * @param value The body of the updated user invitation
42
- */
43
- export async function updateInviteCode(inviteCode: string, value: string) {
44
- await updateACode(redis.utils.Databases.INVITATIONS, inviteCode, value)
45
- }
46
-
47
- async function getACode(db: string, code: string, deleteCode = true) {
48
- const client = await getClient(db)
49
- const value = await client.get(code)
50
- if (!value) {
51
- throw new Error("Invalid code.")
52
- }
53
- if (deleteCode) {
54
- await client.delete(code)
55
- }
56
- return value
57
- }
58
-
59
- export async function init() {
60
- pwResetClient = new redis.Client(redis.utils.Databases.PW_RESETS)
61
- invitationClient = new redis.Client(redis.utils.Databases.INVITATIONS)
62
- await pwResetClient.init()
63
- await invitationClient.init()
64
- }
65
-
66
- /**
67
- * make sure redis connection is closed.
68
- */
69
- export async function shutdown() {
70
- if (pwResetClient) await pwResetClient.finish()
71
- if (invitationClient) await invitationClient.finish()
72
- // shutdown core clients
73
- await redis.clients.shutdown()
74
- console.log("Redis shutdown")
75
- }
76
-
77
- /**
78
- * Given a user ID this will store a code (that is returned) for an hour in redis.
79
- * The user can then return this code for resetting their password (through their reset link).
80
- * @param userId the ID of the user which is to be reset.
81
- * @param info Info about the user/the reset process.
82
- * @return returns the code that was stored to redis.
83
- */
84
- export async function getResetPasswordCode(userId: string, info: any) {
85
- return writeACode(redis.utils.Databases.PW_RESETS, { userId, info })
86
- }
87
-
88
- /**
89
- * Given a reset code this will lookup to redis, check if the code is valid and delete if required.
90
- * @param resetCode The code provided via the email link.
91
- * @param deleteCode If the code is used/finished with this will delete it - defaults to true.
92
- * @return returns the user ID if it is found
93
- */
94
- export async function checkResetPasswordCode(
95
- resetCode: string,
96
- deleteCode = true
97
- ) {
98
- try {
99
- return getACode(redis.utils.Databases.PW_RESETS, resetCode, deleteCode)
100
- } catch (err) {
101
- throw "Provided information is not valid, cannot reset password - please try again."
102
- }
103
- }
104
-
105
- /**
106
- * Generates an invitation code and writes it to redis - which can later be checked for user creation.
107
- * @param email the email address which the code is being sent to (for use later).
108
- * @param info Information to be carried along with the invitation.
109
- * @return returns the code that was stored to redis.
110
- */
111
- export async function getInviteCode(email: string, info: any) {
112
- return writeACode(redis.utils.Databases.INVITATIONS, { email, info })
113
- }
114
-
115
- /**
116
- * Checks that the provided invite code is valid - will return the email address of user that was invited.
117
- * @param inviteCode the invite code that was provided as part of the link.
118
- * @param deleteCode whether or not the code should be deleted after retrieval - defaults to true.
119
- * @return If the code is valid then an email address will be returned.
120
- */
121
- export async function checkInviteCode(
122
- inviteCode: string,
123
- deleteCode: boolean = true
124
- ) {
125
- try {
126
- return getACode(redis.utils.Databases.INVITATIONS, inviteCode, deleteCode)
127
- } catch (err) {
128
- throw "Invitation is not valid or has expired, please request a new one."
129
- }
130
- }
131
-
132
- /**
133
- Get all currently available user invitations for the current tenant.
134
- **/
135
- export async function getInviteCodes() {
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
- if (!env.MULTI_TENANCY) {
146
- return results
147
- }
148
- const tenantId = tenancy.getTenantId()
149
- return results.filter(invite => tenantId === invite.info.tenantId)
150
- }