@budibase/worker 1.2.58-alpha.6 → 1.2.58

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 +6 -7
  2. package/scripts/jestSetup.js +0 -6
  3. package/scripts/load/users.js +97 -0
  4. package/src/api/controllers/global/auth.ts +1 -14
  5. package/src/api/controllers/global/users.ts +55 -26
  6. package/src/api/{index.ts → index.js} +16 -12
  7. package/src/api/routes/global/configs.js +0 -1
  8. package/src/api/routes/index.js +0 -2
  9. package/src/api/routes/{global/tests/auth.spec.ts → tests/auth.spec.js} +56 -32
  10. package/src/api/routes/{global/tests/configs.spec.ts → tests/configs.spec.js} +47 -76
  11. package/src/api/routes/{global/tests/email.spec.ts → tests/email.spec.js} +15 -7
  12. package/src/api/routes/{global/tests/realEmail.spec.ts → tests/realEmail.spec.js} +21 -20
  13. package/src/api/routes/{global/tests/self.spec.ts → tests/self.spec.js} +18 -10
  14. package/src/api/routes/tests/users.spec.js +390 -0
  15. package/src/{environment.ts → environment.js} +9 -10
  16. package/src/index.ts +4 -6
  17. package/src/sdk/index.ts +0 -1
  18. package/src/sdk/users/users.ts +77 -271
  19. package/src/tests/TestConfiguration.js +231 -0
  20. package/src/tests/index.js +12 -0
  21. package/src/tests/mocks/index.js +5 -0
  22. package/src/tests/structures/index.js +14 -0
  23. package/src/tests/structures/users.ts +4 -12
  24. package/src/utilities/redis.js +0 -1
  25. package/src/api/controllers/system/accounts.ts +0 -21
  26. package/src/api/routes/global/tests/users.spec.ts +0 -512
  27. package/src/api/routes/system/accounts.ts +0 -19
  28. package/src/api/routes/system/tests/accounts.spec.ts +0 -57
  29. package/src/sdk/accounts/accounts.ts +0 -53
  30. package/src/sdk/accounts/index.ts +0 -1
  31. package/src/tests/TestConfiguration.ts +0 -273
  32. package/src/tests/api/accounts.ts +0 -28
  33. package/src/tests/api/auth.ts +0 -48
  34. package/src/tests/api/configs.ts +0 -40
  35. package/src/tests/api/email.ts +0 -24
  36. package/src/tests/api/index.ts +0 -25
  37. package/src/tests/api/self.ts +0 -21
  38. package/src/tests/api/users.ts +0 -111
  39. package/src/tests/index.ts +0 -14
  40. package/src/tests/mocks/index.ts +0 -7
  41. package/src/tests/structures/accounts.ts +0 -24
  42. package/src/tests/structures/index.ts +0 -20
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/worker",
3
3
  "email": "hi@budibase.com",
4
- "version": "1.2.58-alpha.6",
4
+ "version": "1.2.58",
5
5
  "description": "Budibase background service",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -35,10 +35,10 @@
35
35
  "author": "Budibase",
36
36
  "license": "GPL-3.0",
37
37
  "dependencies": {
38
- "@budibase/backend-core": "1.2.58-alpha.6",
39
- "@budibase/pro": "1.2.58-alpha.5",
40
- "@budibase/string-templates": "1.2.58-alpha.6",
41
- "@budibase/types": "1.2.58-alpha.6",
38
+ "@budibase/backend-core": "^1.2.58",
39
+ "@budibase/pro": "1.2.57",
40
+ "@budibase/string-templates": "^1.2.58",
41
+ "@budibase/types": "^1.2.58",
42
42
  "@koa/router": "8.0.8",
43
43
  "@sentry/node": "6.17.7",
44
44
  "@techpass/passport-openidconnect": "0.3.2",
@@ -74,7 +74,6 @@
74
74
  "@types/koa-router": "7.4.4",
75
75
  "@types/koa__router": "8.0.11",
76
76
  "@types/node": "14.18.20",
77
- "@types/uuid": "8.3.4",
78
77
  "@typescript-eslint/parser": "5.12.0",
79
78
  "copyfiles": "2.4.1",
80
79
  "eslint": "6.8.0",
@@ -103,5 +102,5 @@
103
102
  "./scripts/jestSetup.js"
104
103
  ]
105
104
  },
106
- "gitHead": "6225d7d6a9d66f5675411db8292acadb3d78446f"
105
+ "gitHead": "2922b47620173f41097d36b9dc5b1c0f8f7b063a"
107
106
  }
@@ -14,9 +14,3 @@ const tk = require("timekeeper")
14
14
  tk.freeze(mocks.date.MOCK_DATE)
15
15
 
16
16
  global.console.log = jest.fn() // console.log are ignored in tests
17
-
18
- if (!process.env.CI) {
19
- // set a longer timeout in dev for debugging
20
- // 100 seconds
21
- jest.setTimeout(100000)
22
- }
@@ -0,0 +1,97 @@
1
+ // get the JWT secret etc
2
+ require("../../src/environment")
3
+ require("@budibase/backend-core").init()
4
+ const {
5
+ getProdAppID,
6
+ generateGlobalUserID,
7
+ } = require("@budibase/backend-core/db")
8
+ const { doInTenant, getGlobalDB } = require("@budibase/backend-core/tenancy")
9
+ const users = require("../../src/sdk/users")
10
+ const { publicApiUserFix } = require("../../src/utilities/users")
11
+ const { hash } = require("@budibase/backend-core/utils")
12
+
13
+ const USER_LOAD_NUMBER = 10000
14
+ const BATCH_SIZE = 200
15
+ const PASSWORD = "test"
16
+ const TENANT_ID = "default"
17
+
18
+ const APP_ID = process.argv[2]
19
+
20
+ const words = [
21
+ "test",
22
+ "testing",
23
+ "budi",
24
+ "mail",
25
+ "age",
26
+ "risk",
27
+ "load",
28
+ "uno",
29
+ "arm",
30
+ "leg",
31
+ "pen",
32
+ "glass",
33
+ "box",
34
+ "chicken",
35
+ "bottle",
36
+ ]
37
+
38
+ if (!APP_ID) {
39
+ console.error("Must supply app ID as first CLI option!")
40
+ process.exit(-1)
41
+ }
42
+
43
+ const WORD_1 = words[Math.floor(Math.random() * words.length)]
44
+ const WORD_2 = words[Math.floor(Math.random() * words.length)]
45
+ let HASHED_PASSWORD
46
+
47
+ function generateUser(count) {
48
+ return {
49
+ _id: generateGlobalUserID(),
50
+ password: HASHED_PASSWORD,
51
+ email: `${WORD_1}${count}@${WORD_2}.com`,
52
+ roles: {
53
+ [getProdAppID(APP_ID)]: "BASIC",
54
+ },
55
+ status: "active",
56
+ forceResetPassword: false,
57
+ firstName: "John",
58
+ lastName: "Smith",
59
+ }
60
+ }
61
+
62
+ async function run() {
63
+ HASHED_PASSWORD = await hash(PASSWORD)
64
+ return doInTenant(TENANT_ID, async () => {
65
+ const db = getGlobalDB()
66
+ for (let i = 0; i < USER_LOAD_NUMBER; i += BATCH_SIZE) {
67
+ let userSavePromises = []
68
+ for (let j = 0; j < BATCH_SIZE; j++) {
69
+ // like the public API
70
+ const ctx = publicApiUserFix({
71
+ request: {
72
+ body: generateUser(i + j),
73
+ },
74
+ })
75
+ userSavePromises.push(
76
+ users.save(ctx.request.body, {
77
+ hashPassword: false,
78
+ requirePassword: true,
79
+ bulkCreate: true,
80
+ })
81
+ )
82
+ }
83
+ const allUsers = await Promise.all(userSavePromises)
84
+ await db.bulkDocs(allUsers)
85
+ console.log(`${i + BATCH_SIZE} users have been created.`)
86
+ }
87
+ })
88
+ }
89
+
90
+ run()
91
+ .then(() => {
92
+ console.log(`Generated ${USER_LOAD_NUMBER} users!`)
93
+ })
94
+ .catch(err => {
95
+ console.error("Failed for reason: ", err)
96
+ process.exit(-1)
97
+ })
@@ -227,22 +227,9 @@ export const oidcPreAuth = async (ctx: any, next: any) => {
227
227
 
228
228
  setCookie(ctx, configId, Cookies.OIDC_CONFIG)
229
229
 
230
- const db = getGlobalDB()
231
- const config = await core.db.getScopedConfig(db, {
232
- type: Configs.OIDC,
233
- group: ctx.query.group,
234
- })
235
-
236
- const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
237
-
238
- let authScopes =
239
- chosenConfig.scopes?.length > 0
240
- ? chosenConfig.scopes
241
- : ["profile", "email", "offline_access"]
242
-
243
230
  return passport.authenticate(strategy, {
244
231
  // required 'openid' scope is added by oidc strategy factory
245
- scope: authScopes,
232
+ scope: ["profile", "email", "offline_access"], //auth0 offline_access scope required for the refresh token behaviour.
246
233
  })(ctx, next)
247
234
  }
248
235
 
@@ -1,13 +1,9 @@
1
+ import { EmailTemplatePurpose } from "../../../constants"
1
2
  import { checkInviteCode } from "../../../utilities/redis"
3
+ import { sendEmail } from "../../../utilities/email"
2
4
  import { users } from "../../../sdk"
3
5
  import env from "../../../environment"
4
- import {
5
- BulkDeleteUsersRequest,
6
- CloudAccount,
7
- InviteUserRequest,
8
- InviteUsersRequest,
9
- User,
10
- } from "@budibase/types"
6
+ import { CloudAccount, User } from "@budibase/types"
11
7
  import {
12
8
  accounts,
13
9
  cache,
@@ -50,8 +46,8 @@ export const bulkCreate = async (ctx: any) => {
50
46
  }
51
47
 
52
48
  try {
53
- const response = await users.bulkCreate(newUsersRequested, groups)
54
- await groupUtils.bulkSaveGroupUsers(groupsToSave, response.successful)
49
+ let response = await users.bulkCreate(newUsersRequested, groups)
50
+ await groupUtils.bulkSaveGroupUsers(groupsToSave, response)
55
51
 
56
52
  ctx.body = response
57
53
  } catch (err: any) {
@@ -142,15 +138,17 @@ export const destroy = async (ctx: any) => {
142
138
  }
143
139
 
144
140
  export const bulkDelete = async (ctx: any) => {
145
- const { userIds } = ctx.request.body as BulkDeleteUsersRequest
141
+ const { userIds } = ctx.request.body
146
142
  if (userIds?.indexOf(ctx.user._id) !== -1) {
147
143
  ctx.throw(400, "Unable to delete self.")
148
144
  }
149
145
 
150
146
  try {
151
- let response = await users.bulkDelete(userIds)
147
+ let usersResponse = await users.bulkDelete(userIds)
152
148
 
153
- ctx.body = response
149
+ ctx.body = {
150
+ message: `${usersResponse.length} user(s) deleted`,
151
+ }
154
152
  } catch (err) {
155
153
  ctx.throw(err)
156
154
  }
@@ -195,27 +193,58 @@ export const tenantUserLookup = async (ctx: any) => {
195
193
  }
196
194
 
197
195
  export const invite = async (ctx: any) => {
198
- const request = ctx.request.body as InviteUserRequest
199
- const response = await users.invite([request])
200
-
201
- // explicitly throw for single user invite
202
- if (response.unsuccessful.length) {
203
- const reason = response.unsuccessful[0].reason
204
- if (reason === "Unavailable") {
205
- ctx.throw(400, reason)
206
- } else {
207
- ctx.throw(500, reason)
208
- }
196
+ let { email, userInfo } = ctx.request.body
197
+ const existing = await usersCore.getGlobalUserByEmail(email)
198
+ if (existing) {
199
+ ctx.throw(400, "Email address already in use.")
209
200
  }
210
-
201
+ if (!userInfo) {
202
+ userInfo = {}
203
+ }
204
+ userInfo.tenantId = tenancy.getTenantId()
205
+ const opts: any = {
206
+ subject: "{{ company }} platform invitation",
207
+ info: userInfo,
208
+ }
209
+ await sendEmail(email, EmailTemplatePurpose.INVITATION, opts)
211
210
  ctx.body = {
212
211
  message: "Invitation has been sent.",
213
212
  }
213
+ await events.user.invited()
214
214
  }
215
215
 
216
216
  export const inviteMultiple = async (ctx: any) => {
217
- const request = ctx.request.body as InviteUsersRequest
218
- ctx.body = await users.invite(request)
217
+ let users = ctx.request.body
218
+ let existing = false
219
+ let existingEmail
220
+ for (let user of users) {
221
+ if (await usersCore.getGlobalUserByEmail(user.email)) {
222
+ existing = true
223
+ existingEmail = user.email
224
+ break
225
+ }
226
+ }
227
+
228
+ if (existing) {
229
+ ctx.throw(400, `${existingEmail} already exists`)
230
+ }
231
+
232
+ for (let i = 0; i < users.length; i++) {
233
+ let userInfo = users[i].userInfo
234
+ if (!userInfo) {
235
+ userInfo = {}
236
+ }
237
+ userInfo.tenantId = tenancy.getTenantId()
238
+ const opts: any = {
239
+ subject: "{{ company }} platform invitation",
240
+ info: userInfo,
241
+ }
242
+ await sendEmail(users[i].email, EmailTemplatePurpose.INVITATION, opts)
243
+ }
244
+
245
+ ctx.body = {
246
+ message: "Invitations have been sent.",
247
+ }
219
248
  }
220
249
 
221
250
  export const inviteAccept = async (ctx: any) => {
@@ -1,10 +1,15 @@
1
- import Router from "@koa/router"
1
+ const Router = require("@koa/router")
2
2
  const compress = require("koa-compress")
3
3
  const zlib = require("zlib")
4
- import { routes } from "./routes"
5
- import { middleware as pro } from "@budibase/pro"
6
- import { errors, auth, middleware } from "@budibase/backend-core"
7
- import { APIError } from "@budibase/types"
4
+ const { routes } = require("./routes")
5
+ const {
6
+ buildAuthMiddleware,
7
+ auditLog,
8
+ buildTenancyMiddleware,
9
+ buildCsrfMiddleware,
10
+ } = require("@budibase/backend-core/auth")
11
+ const { middleware: pro } = require("@budibase/pro")
12
+ const { errors } = require("@budibase/backend-core")
8
13
 
9
14
  const PUBLIC_ENDPOINTS = [
10
15
  // old deprecated endpoints kept for backwards compat
@@ -92,9 +97,9 @@ router
92
97
  })
93
98
  )
94
99
  .use("/health", ctx => (ctx.status = 200))
95
- .use(auth.buildAuthMiddleware(PUBLIC_ENDPOINTS))
96
- .use(auth.buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
97
- .use(auth.buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS }))
100
+ .use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
101
+ .use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
102
+ .use(buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS }))
98
103
  .use(pro.licensing())
99
104
  // for now no public access is allowed to worker (bar health check)
100
105
  .use((ctx, next) => {
@@ -109,22 +114,21 @@ router
109
114
  }
110
115
  return next()
111
116
  })
112
- .use(middleware.auditLog)
117
+ .use(auditLog)
113
118
 
114
119
  // error handling middleware - TODO: This could be moved to backend-core
115
120
  router.use(async (ctx, next) => {
116
121
  try {
117
122
  await next()
118
- } catch (err: any) {
123
+ } catch (err) {
119
124
  ctx.log.error(err)
120
125
  ctx.status = err.status || err.statusCode || 500
121
126
  const error = errors.getPublicError(err)
122
- const body: APIError = {
127
+ ctx.body = {
123
128
  message: err.message,
124
129
  status: ctx.status,
125
130
  error,
126
131
  }
127
- ctx.body = body
128
132
  }
129
133
  })
130
134
 
@@ -53,7 +53,6 @@ function oidcValidation() {
53
53
  name: Joi.string().allow("", null),
54
54
  uuid: Joi.string().required(),
55
55
  activated: Joi.boolean().required(),
56
- scopes: Joi.array().optional()
57
56
  })
58
57
  ).required(true)
59
58
  }).unknown(true)
@@ -12,7 +12,6 @@ const statusRoutes = require("./system/status")
12
12
  const selfRoutes = require("./global/self")
13
13
  const licenseRoutes = require("./global/license")
14
14
  const migrationRoutes = require("./system/migrations")
15
- const accountRoutes = require("./system/accounts")
16
15
 
17
16
  let userGroupRoutes = api.groups
18
17
  exports.routes = [
@@ -30,5 +29,4 @@ exports.routes = [
30
29
  licenseRoutes,
31
30
  userGroupRoutes,
32
31
  migrationRoutes,
33
- accountRoutes,
34
32
  ]
@@ -1,11 +1,11 @@
1
1
  jest.mock("nodemailer")
2
- import { TestConfiguration, mocks, API } from "../../../../tests"
2
+ const { config, request, mocks, structures } = require("../../../tests")
3
3
  const sendMailMock = mocks.email.mock()
4
- import { events } from "@budibase/backend-core"
4
+ const { events } = require("@budibase/backend-core")
5
+
6
+ const TENANT_ID = structures.TENANT_ID
5
7
 
6
8
  describe("/api/global/auth", () => {
7
- const config = new TestConfiguration()
8
- const api = new API(config)
9
9
 
10
10
  beforeAll(async () => {
11
11
  await config.beforeAll()
@@ -19,32 +19,56 @@ describe("/api/global/auth", () => {
19
19
  jest.clearAllMocks()
20
20
  })
21
21
 
22
+ const requestPasswordReset = async () => {
23
+ await config.saveSmtpConfig()
24
+ await config.saveSettingsConfig()
25
+ await config.createUser()
26
+ const res = await request
27
+ .post(`/api/global/auth/${TENANT_ID}/reset`)
28
+ .send({
29
+ email: "test@test.com",
30
+ })
31
+ .expect("Content-Type", /json/)
32
+ .expect(200)
33
+ const emailCall = sendMailMock.mock.calls[0][0]
34
+ const parts = emailCall.html.split(`http://localhost:10000/builder/auth/reset?code=`)
35
+ const code = parts[1].split("\"")[0].split("&")[0]
36
+ return { code, res }
37
+ }
38
+
22
39
  it("should logout", async () => {
23
- await api.auth.logout()
40
+ await request
41
+ .post("/api/global/auth/logout")
42
+ .set(config.defaultHeaders())
43
+ .expect(200)
24
44
  expect(events.auth.logout).toBeCalledTimes(1)
25
45
  })
26
46
 
27
47
  it("should be able to generate password reset email", async () => {
28
- const { res, code } = await api.auth.requestPasswordReset(sendMailMock)
48
+ const { res, code } = await requestPasswordReset()
29
49
  const user = await config.getUser("test@test.com")
30
50
 
31
- expect(res.body).toEqual({
32
- message: "Please check your email for a reset link.",
33
- })
51
+ expect(res.body).toEqual({ message: "Please check your email for a reset link." })
34
52
  expect(sendMailMock).toHaveBeenCalled()
35
-
53
+
36
54
  expect(code).toBeDefined()
37
55
  expect(events.user.passwordResetRequested).toBeCalledTimes(1)
38
56
  expect(events.user.passwordResetRequested).toBeCalledWith(user)
39
57
  })
40
58
 
41
59
  it("should allow resetting user password with code", async () => {
42
- const { code } = await api.auth.requestPasswordReset(sendMailMock)
60
+ const { code } = await requestPasswordReset()
43
61
  const user = await config.getUser("test@test.com")
44
- delete user.password
45
-
46
- const res = await api.auth.updatePassword(code)
62
+ delete user.password
47
63
 
64
+ const res = await request
65
+ .post(`/api/global/auth/${TENANT_ID}/reset/update`)
66
+ .send({
67
+ password: "newpassword",
68
+ resetCode: code,
69
+ })
70
+ .expect("Content-Type", /json/)
71
+ .expect(200)
48
72
  expect(res.body).toEqual({ message: "password reset successfully." })
49
73
  expect(events.user.passwordReset).toBeCalledTimes(1)
50
74
  expect(events.user.passwordReset).toBeCalledWith(user)
@@ -55,15 +79,15 @@ describe("/api/global/auth", () => {
55
79
 
56
80
  const passportSpy = jest.spyOn(auth.passport, "authenticate")
57
81
  let oidcConf
58
- let chosenConfig: any
59
- let configId: string
82
+ let chosenConfig
83
+ let configId
60
84
 
61
85
  // mock the oidc strategy implementation and return value
62
86
  let strategyFactory = jest.fn()
63
87
  let mockStrategyReturn = jest.fn()
64
88
  let mockStrategyConfig = jest.fn()
65
89
  auth.oidc.fetchStrategyConfig = mockStrategyConfig
66
-
90
+
67
91
  strategyFactory.mockReturnValue(mockStrategyReturn)
68
92
  auth.oidc.strategyFactory = strategyFactory
69
93
 
@@ -75,34 +99,34 @@ describe("/api/global/auth", () => {
75
99
  })
76
100
 
77
101
  afterEach(() => {
78
- expect(strategyFactory).toBeCalledWith(chosenConfig, expect.any(Function))
102
+ expect(strategyFactory).toBeCalledWith(
103
+ chosenConfig,
104
+ expect.any(Function)
105
+ )
79
106
  })
80
107
 
81
108
  describe("oidc configs", () => {
82
109
  it("should load strategy and delegate to passport", async () => {
83
- await api.configs.getOIDCConfig(configId)
110
+ await request.get(`/api/global/auth/${TENANT_ID}/oidc/configs/${configId}`)
84
111
 
85
112
  expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
86
- scope: ["profile", "email", "offline_access"],
113
+ scope: ["profile", "email", "offline_access"]
87
114
  })
88
- expect(passportSpy.mock.calls.length).toBe(1)
115
+ expect(passportSpy.mock.calls.length).toBe(1);
89
116
  })
90
117
  })
91
118
 
92
119
  describe("oidc callback", () => {
93
120
  it("should load strategy and delegate to passport", async () => {
94
- await api.configs.OIDCCallback(configId)
95
-
96
- expect(passportSpy).toBeCalledWith(
97
- mockStrategyReturn,
98
- {
99
- successRedirect: "/",
100
- failureRedirect: "/error",
101
- },
102
- expect.anything()
103
- )
104
- expect(passportSpy.mock.calls.length).toBe(1)
121
+ await request.get(`/api/global/auth/${TENANT_ID}/oidc/callback`)
122
+ .set(config.getOIDConfigCookie(configId))
123
+
124
+ expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
125
+ successRedirect: "/", failureRedirect: "/error"
126
+ }, expect.anything())
127
+ expect(passportSpy.mock.calls.length).toBe(1);
105
128
  })
106
129
  })
130
+
107
131
  })
108
132
  })