@budibase/worker 1.1.32 → 1.2.2

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": "1.1.32",
4
+ "version": "1.2.2",
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.1.32",
39
- "@budibase/pro": "1.1.31",
40
- "@budibase/string-templates": "^1.1.32",
41
- "@budibase/types": "^1.1.32",
38
+ "@budibase/backend-core": "^1.2.2",
39
+ "@budibase/pro": "1.1.33-alpha.0",
40
+ "@budibase/string-templates": "^1.2.2",
41
+ "@budibase/types": "^1.2.2",
42
42
  "@koa/router": "8.0.8",
43
43
  "@sentry/node": "6.17.7",
44
44
  "@techpass/passport-openidconnect": "0.3.2",
@@ -101,5 +101,5 @@
101
101
  "./scripts/jestSetup.js"
102
102
  ]
103
103
  },
104
- "gitHead": "900d5de0723c1b7acf218446507d52755fdf7d83"
104
+ "gitHead": "91527fdba7a07be28f5026f2b89291341c42812a"
105
105
  }
@@ -13,6 +13,8 @@ import {
13
13
  cache,
14
14
  } from "@budibase/backend-core"
15
15
  import { checkAnyUserExists } from "../../../utilities/users"
16
+ import { groups as groupUtils } from "@budibase/pro"
17
+ const MAX_USERS_UPLOAD_LIMIT = 1000
16
18
 
17
19
  export const save = async (ctx: any) => {
18
20
  try {
@@ -22,6 +24,36 @@ export const save = async (ctx: any) => {
22
24
  }
23
25
  }
24
26
 
27
+ export const bulkCreate = async (ctx: any) => {
28
+ let { users: newUsersRequested, groups } = ctx.request.body
29
+
30
+ if (!env.SELF_HOSTED && newUsersRequested.length > MAX_USERS_UPLOAD_LIMIT) {
31
+ ctx.throw(
32
+ 400,
33
+ "Max limit for upload is 1000 users. Please reduce file size and try again."
34
+ )
35
+ }
36
+
37
+ const db = tenancy.getGlobalDB()
38
+ let groupsToSave: any[] = []
39
+
40
+ if (groups.length) {
41
+ for (const groupId of groups) {
42
+ let oldGroup = await db.get(groupId)
43
+ groupsToSave.push(oldGroup)
44
+ }
45
+ }
46
+
47
+ try {
48
+ let response = await users.bulkCreate(newUsersRequested, groups)
49
+ await groupUtils.bulkSaveGroupUsers(groupsToSave, response)
50
+
51
+ ctx.body = response
52
+ } catch (err: any) {
53
+ ctx.throw(err.status || 400, err)
54
+ }
55
+ }
56
+
25
57
  const parseBooleanParam = (param: any) => {
26
58
  return !(param && param === "false")
27
59
  }
@@ -82,14 +114,39 @@ export const adminUser = async (ctx: any) => {
82
114
  })
83
115
  }
84
116
 
117
+ export const countByApp = async (ctx: any) => {
118
+ const appId = ctx.params.appId
119
+ try {
120
+ const response = await users.countUsersByApp(appId)
121
+ ctx.body = response
122
+ } catch (err: any) {
123
+ ctx.throw(err.status || 400, err)
124
+ }
125
+ }
126
+
85
127
  export const destroy = async (ctx: any) => {
86
128
  const id = ctx.params.id
129
+
87
130
  await users.destroy(id, ctx.user)
131
+
88
132
  ctx.body = {
89
133
  message: `User ${id} deleted.`,
90
134
  }
91
135
  }
92
136
 
137
+ export const bulkDelete = async (ctx: any) => {
138
+ const { userIds } = ctx.request.body
139
+ try {
140
+ let usersResponse = await users.bulkDelete(userIds)
141
+
142
+ ctx.body = {
143
+ message: `${usersResponse.length} user(s) deleted`,
144
+ }
145
+ } catch (err) {
146
+ ctx.throw(err)
147
+ }
148
+ }
149
+
93
150
  export const search = async (ctx: any) => {
94
151
  const paginated = await users.paginatedUsers(ctx.request.body)
95
152
  // user hashed password shouldn't ever be returned
@@ -149,6 +206,39 @@ export const invite = async (ctx: any) => {
149
206
  await events.user.invited()
150
207
  }
151
208
 
209
+ export const inviteMultiple = async (ctx: any) => {
210
+ let { emails, userInfo } = ctx.request.body
211
+ let existing = false
212
+ let existingEmail
213
+ for (let email of emails) {
214
+ if (await usersCore.getGlobalUserByEmail(email)) {
215
+ existing = true
216
+ existingEmail = email
217
+ break
218
+ }
219
+ }
220
+
221
+ if (existing) {
222
+ ctx.throw(400, `${existingEmail} already exists`)
223
+ }
224
+ if (!userInfo) {
225
+ userInfo = {}
226
+ }
227
+ userInfo.tenantId = tenancy.getTenantId()
228
+ const opts: any = {
229
+ subject: "{{ company }} platform invitation",
230
+ info: userInfo,
231
+ }
232
+
233
+ for (let i = 0; i < emails.length; i++) {
234
+ await sendEmail(emails[i], EmailTemplatePurpose.INVITATION, opts)
235
+ }
236
+
237
+ ctx.body = {
238
+ message: "Invitations have been sent.",
239
+ }
240
+ }
241
+
152
242
  export const inviteAccept = async (ctx: any) => {
153
243
  const { inviteCode, password, firstName, lastName } = ctx.request.body
154
244
  try {
@@ -1,6 +1,6 @@
1
1
  const Router = require("@koa/router")
2
2
  const authController = require("../../controllers/global/auth")
3
- const joiValidator = require("../../../middleware/joi-validator")
3
+ const { joiValidator } = require("@budibase/backend-core/auth")
4
4
  const Joi = require("joi")
5
5
  const { updateTenantId } = require("@budibase/backend-core/tenancy")
6
6
 
@@ -1,7 +1,7 @@
1
1
  const Router = require("@koa/router")
2
2
  const controller = require("../../controllers/global/configs")
3
- const joiValidator = require("../../../middleware/joi-validator")
4
- const adminOnly = require("../../../middleware/adminOnly")
3
+ const { joiValidator } = require("@budibase/backend-core/auth")
4
+ const { adminOnly } = require("@budibase/backend-core/auth")
5
5
  const Joi = require("joi")
6
6
  const { Configs } = require("../../../constants")
7
7
 
@@ -65,6 +65,8 @@ function buildConfigSaveValidation() {
65
65
  _rev: Joi.string().optional(),
66
66
  workspace: Joi.string().optional(),
67
67
  type: Joi.string().valid(...Object.values(Configs)).required(),
68
+ createdAt: Joi.string().optional(),
69
+ updatedAt: Joi.string().optional(),
68
70
  config: Joi.alternatives()
69
71
  .conditional("type", {
70
72
  switch: [
@@ -75,7 +77,7 @@ function buildConfigSaveValidation() {
75
77
  { is: Configs.OIDC, then: oidcValidation() }
76
78
  ],
77
79
  }),
78
- }).required().unknown(true),
80
+ }).required().unknown(true),
79
81
  )
80
82
  }
81
83
 
@@ -1,8 +1,8 @@
1
1
  const Router = require("@koa/router")
2
2
  const controller = require("../../controllers/global/email")
3
3
  const { EmailTemplatePurpose } = require("../../../constants")
4
- const joiValidator = require("../../../middleware/joi-validator")
5
- const adminOnly = require("../../../middleware/adminOnly")
4
+ const { joiValidator } = require("@budibase/backend-core/auth")
5
+ const { adminOnly } = require("@budibase/backend-core/auth")
6
6
  const Joi = require("joi")
7
7
 
8
8
  const router = Router()
@@ -1,6 +1,6 @@
1
1
  const Router = require("@koa/router")
2
2
  const controller = require("../../controllers/global/roles")
3
- const adminOnly = require("../../../middleware/adminOnly")
3
+ const { adminOnly } = require("@budibase/backend-core/auth")
4
4
 
5
5
  const router = Router()
6
6
 
@@ -1,6 +1,6 @@
1
1
  const Router = require("@koa/router")
2
2
  const controller = require("../../controllers/global/sessions")
3
- const adminOnly = require("../../../middleware/adminOnly")
3
+ const { adminOnly } = require("@budibase/backend-core/auth")
4
4
 
5
5
  const router = Router()
6
6
 
@@ -1,9 +1,9 @@
1
1
  const Router = require("@koa/router")
2
2
  const controller = require("../../controllers/global/templates")
3
- const joiValidator = require("../../../middleware/joi-validator")
3
+ const { joiValidator } = require("@budibase/backend-core/auth")
4
4
  const Joi = require("joi")
5
5
  const { TemplatePurpose, TemplateTypes } = require("../../../constants")
6
- const adminOnly = require("../../../middleware/adminOnly")
6
+ const { adminOnly } = require("@budibase/backend-core/auth")
7
7
 
8
8
  const router = Router()
9
9
 
@@ -1,7 +1,7 @@
1
1
  const Router = require("@koa/router")
2
2
  const controller = require("../../controllers/global/users")
3
- const joiValidator = require("../../../middleware/joi-validator")
4
- const adminOnly = require("../../../middleware/adminOnly")
3
+ const { joiValidator } = require("@budibase/backend-core/auth")
4
+ const { adminOnly } = require("@budibase/backend-core/auth")
5
5
  const Joi = require("joi")
6
6
  const cloudRestricted = require("../../../middleware/cloudRestricted")
7
7
  const { users } = require("../validation")
@@ -30,6 +30,14 @@ function buildInviteValidation() {
30
30
  }).required())
31
31
  }
32
32
 
33
+ function buildInviteMultipleValidation() {
34
+ // prettier-ignore
35
+ return joiValidator.body(Joi.object({
36
+ emails: Joi.array().required(),
37
+ userInfo: Joi.object().optional(),
38
+ }).required())
39
+ }
40
+
33
41
  function buildInviteAcceptValidation() {
34
42
  // prettier-ignore
35
43
  return joiValidator.body(Joi.object({
@@ -45,9 +53,18 @@ router
45
53
  users.buildUserSaveValidation(),
46
54
  controller.save
47
55
  )
56
+ .post(
57
+ "/api/global/users/bulkCreate",
58
+ adminOnly,
59
+ users.buildUserBulkSaveValidation(),
60
+ controller.bulkCreate
61
+ )
62
+
48
63
  .get("/api/global/users", builderOrAdmin, controller.fetch)
49
64
  .post("/api/global/users/search", builderOrAdmin, controller.search)
50
65
  .delete("/api/global/users/:id", adminOnly, controller.destroy)
66
+ .post("/api/global/users/bulkDelete", adminOnly, controller.bulkDelete)
67
+ .get("/api/global/users/count/:appId", adminOnly, controller.countByApp)
51
68
  .get("/api/global/roles/:appId")
52
69
  .post(
53
70
  "/api/global/users/invite",
@@ -55,6 +72,19 @@ router
55
72
  buildInviteValidation(),
56
73
  controller.invite
57
74
  )
75
+ .post(
76
+ "/api/global/users/invite",
77
+ adminOnly,
78
+ buildInviteValidation(),
79
+ controller.invite
80
+ )
81
+ .post(
82
+ "/api/global/users/inviteMultiple",
83
+ adminOnly,
84
+ buildInviteMultipleValidation(),
85
+ controller.inviteMultiple
86
+ )
87
+
58
88
  // non-global endpoints
59
89
  .post(
60
90
  "/api/global/users/invite/accept",
@@ -1,7 +1,7 @@
1
1
  const Router = require("@koa/router")
2
2
  const controller = require("../../controllers/global/workspaces")
3
- const joiValidator = require("../../../middleware/joi-validator")
4
- const adminOnly = require("../../../middleware/adminOnly")
3
+ const { joiValidator } = require("@budibase/backend-core/auth")
4
+ const { adminOnly } = require("@budibase/backend-core/auth")
5
5
  const Joi = require("joi")
6
6
 
7
7
  const router = Router()
@@ -17,9 +17,9 @@ function buildWorkspaceSaveValidation() {
17
17
  roles: Joi.object({
18
18
  default: Joi.string().optional(),
19
19
  app: Joi.object()
20
- .pattern(/.*/, Joi.string())
21
- .required()
22
- .unknown(true),
20
+ .pattern(/.*/, Joi.string())
21
+ .required()
22
+ .unknown(true),
23
23
  }).unknown(true).optional(),
24
24
  }).required().unknown(true))
25
25
  }
@@ -1,3 +1,4 @@
1
+ const { api } = require("@budibase/pro")
1
2
  const userRoutes = require("./global/users")
2
3
  const configRoutes = require("./global/configs")
3
4
  const workspaceRoutes = require("./global/workspaces")
@@ -12,6 +13,7 @@ const statusRoutes = require("./system/status")
12
13
  const selfRoutes = require("./global/self")
13
14
  const licenseRoutes = require("./global/license")
14
15
 
16
+ let userGroupRoutes = api.groups
15
17
  exports.routes = [
16
18
  configRoutes,
17
19
  userRoutes,
@@ -26,4 +28,5 @@ exports.routes = [
26
28
  statusRoutes,
27
29
  selfRoutes,
28
30
  licenseRoutes,
31
+ userGroupRoutes,
29
32
  ]
@@ -1,6 +1,6 @@
1
1
  const Router = require("@koa/router")
2
2
  const controller = require("../../controllers/system/tenants")
3
- const adminOnly = require("../../../middleware/adminOnly")
3
+ const { adminOnly } = require("@budibase/backend-core/auth")
4
4
 
5
5
  const router = Router()
6
6
 
@@ -2,7 +2,6 @@ jest.mock("nodemailer")
2
2
  const { config, request, mocks, structures } = require("../../../tests")
3
3
  const sendMailMock = mocks.email.mock()
4
4
  const { events } = require("@budibase/backend-core")
5
-
6
5
  describe("/api/global/users", () => {
7
6
 
8
7
  beforeAll(async () => {
@@ -24,9 +23,9 @@ describe("/api/global/users", () => {
24
23
  .set(config.defaultHeaders())
25
24
  .expect("Content-Type", /json/)
26
25
  .expect(200)
27
-
28
- const emailCall = sendMailMock.mock.calls[0][0]
29
- // after this URL there should be a code
26
+
27
+ const emailCall = sendMailMock.mock.calls[0][0]
28
+ // after this URL there should be a code
30
29
  const parts = emailCall.html.split("http://localhost:10000/builder/invite?code=")
31
30
  const code = parts[1].split("\"")[0].split("&")[0]
32
31
  return { code, res }
@@ -60,7 +59,7 @@ describe("/api/global/users", () => {
60
59
  expect(events.user.inviteAccepted).toBeCalledWith(user)
61
60
  })
62
61
 
63
- const createUser = async (user) => {
62
+ const createUser = async (user) => {
64
63
  const existing = await config.getUser(user.email)
65
64
  if (existing) {
66
65
  await deleteUser(existing._id)
@@ -84,14 +83,37 @@ describe("/api/global/users", () => {
84
83
  return res.body
85
84
  }
86
85
 
86
+
87
+ const bulkCreateUsers = async (users) => {
88
+ const res = await request
89
+ .post(`/api/global/users/bulkCreate`)
90
+ .send(users)
91
+ .set(config.defaultHeaders())
92
+ .expect("Content-Type", /json/)
93
+ .expect(200)
94
+ return res.body
95
+ }
96
+
97
+ const bulkDeleteUsers = async (users) => {
98
+ const res = await request
99
+ .post(`/api/global/users/bulkDelete`)
100
+ .send(users)
101
+ .set(config.defaultHeaders())
102
+ .expect("Content-Type", /json/)
103
+ .expect(200)
104
+ return res.body
105
+ }
106
+
107
+
108
+
87
109
  const deleteUser = async (email) => {
88
110
  const user = await config.getUser(email)
89
111
  if (user) {
90
112
  await request
91
- .delete(`/api/global/users/${user._id}`)
92
- .set(config.defaultHeaders())
93
- .expect("Content-Type", /json/)
94
- .expect(200)
113
+ .delete(`/api/global/users/${user._id}`)
114
+ .set(config.defaultHeaders())
115
+ .expect("Content-Type", /json/)
116
+ .expect(200)
95
117
  }
96
118
  }
97
119
 
@@ -107,10 +129,25 @@ describe("/api/global/users", () => {
107
129
  expect(events.user.permissionAdminAssigned).not.toBeCalled()
108
130
  })
109
131
 
132
+ it("should be able to bulkCreate users with different permissions", async () => {
133
+ jest.clearAllMocks()
134
+ const builder = structures.users.builderUser({ email: "bulkbasic@test.com" })
135
+ const admin = structures.users.adminUser({ email: "bulkadmin@test.com" })
136
+ const user = structures.users.user({ email: "bulkuser@test.com" })
137
+
138
+ let toCreate = { users: [builder, admin, user], groups: [] }
139
+ await bulkCreateUsers(toCreate)
140
+
141
+ expect(events.user.created).toBeCalledTimes(3)
142
+ expect(events.user.permissionAdminAssigned).toBeCalledTimes(1)
143
+ expect(events.user.permissionBuilderAssigned).toBeCalledTimes(1)
144
+ })
145
+
146
+
110
147
  it("should be able to create an admin user", async () => {
111
148
  jest.clearAllMocks()
112
149
  const user = structures.users.adminUser({ email: "admin@test.com" })
113
- await createUser(user)
150
+ await createUser(user)
114
151
 
115
152
  expect(events.user.created).toBeCalledTimes(1)
116
153
  expect(events.user.updated).not.toBeCalled()
@@ -333,5 +370,21 @@ describe("/api/global/users", () => {
333
370
  expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
334
371
  expect(events.user.permissionAdminRemoved).not.toBeCalled()
335
372
  })
373
+
374
+ it("should be able to bulk delete users with different permissions", async () => {
375
+ jest.clearAllMocks()
376
+ const builder = structures.users.builderUser({ email: "basic@test.com" })
377
+ const admin = structures.users.adminUser({ email: "admin@test.com" })
378
+ const user = structures.users.user({ email: "user@test.com" })
379
+
380
+ let toCreate = { users: [builder, admin, user], groups: [] }
381
+ let createdUsers = await bulkCreateUsers(toCreate)
382
+ await bulkDeleteUsers({ userIds: [createdUsers[0]._id, createdUsers[1]._id, createdUsers[2]._id] })
383
+ expect(events.user.deleted).toBeCalledTimes(3)
384
+ expect(events.user.permissionAdminRemoved).toBeCalledTimes(1)
385
+ expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
386
+
387
+ })
388
+
336
389
  })
337
390
  })
@@ -1,22 +1,23 @@
1
1
  import joiValidator from "../../../middleware/joi-validator"
2
2
  import Joi from "joi"
3
3
 
4
+ let schema: any = {
5
+ email: Joi.string().allow(null, ""),
6
+ password: Joi.string().allow(null, ""),
7
+ forceResetPassword: Joi.boolean().optional(),
8
+ firstName: Joi.string().allow(null, ""),
9
+ lastName: Joi.string().allow(null, ""),
10
+ builder: Joi.object({
11
+ global: Joi.boolean().optional(),
12
+ apps: Joi.array().optional(),
13
+ })
14
+ .unknown(true)
15
+ .optional(),
16
+ // maps appId -> roleId for the user
17
+ roles: Joi.object().pattern(/.*/, Joi.string()).required().unknown(true),
18
+ }
19
+
4
20
  export const buildUserSaveValidation = (isSelf = false) => {
5
- let schema: any = {
6
- email: Joi.string().allow(null, ""),
7
- password: Joi.string().allow(null, ""),
8
- forceResetPassword: Joi.boolean().optional(),
9
- firstName: Joi.string().allow(null, ""),
10
- lastName: Joi.string().allow(null, ""),
11
- builder: Joi.object({
12
- global: Joi.boolean().optional(),
13
- apps: Joi.array().optional(),
14
- })
15
- .unknown(true)
16
- .optional(),
17
- // maps appId -> roleId for the user
18
- roles: Joi.object().pattern(/.*/, Joi.string()).required().unknown(true),
19
- }
20
21
  if (!isSelf) {
21
22
  schema = {
22
23
  ...schema,
@@ -26,3 +27,19 @@ export const buildUserSaveValidation = (isSelf = false) => {
26
27
  }
27
28
  return joiValidator.body(Joi.object(schema).required().unknown(true))
28
29
  }
30
+
31
+ export const buildUserBulkSaveValidation = (isSelf = false) => {
32
+ if (!isSelf) {
33
+ schema = {
34
+ ...schema,
35
+ _id: Joi.string(),
36
+ _rev: Joi.string(),
37
+ }
38
+ }
39
+ let bulkSaveSchema = {
40
+ groups: Joi.array().optional(),
41
+ users: Joi.array().items(Joi.object(schema).required().unknown(true)),
42
+ }
43
+
44
+ return joiValidator.body(Joi.object(bulkSaveSchema).required().unknown(true))
45
+ }
@@ -15,7 +15,8 @@ import {
15
15
  accounts,
16
16
  migrations,
17
17
  } from "@budibase/backend-core"
18
- import { MigrationType } from "@budibase/types"
18
+ import { MigrationType, User } from "@budibase/types"
19
+ import { groups as groupUtils } from "@budibase/pro"
19
20
 
20
21
  const PAGE_LIMIT = 8
21
22
 
@@ -29,10 +30,18 @@ export const allUsers = async () => {
29
30
  return response.rows.map((row: any) => row.doc)
30
31
  }
31
32
 
33
+ export const countUsersByApp = async (appId: string) => {
34
+ let response: any = await usersCore.searchGlobalUsersByApp(appId, {})
35
+ return {
36
+ userCount: response.length,
37
+ }
38
+ }
39
+
32
40
  export const paginatedUsers = async ({
33
41
  page,
34
- search,
35
- }: { page?: string; search?: string } = {}) => {
42
+ email,
43
+ appId,
44
+ }: { page?: string; email?: string; appId?: string } = {}) => {
36
45
  const db = tenancy.getGlobalDB()
37
46
  // get one extra document, to have the next page
38
47
  const opts: any = {
@@ -44,19 +53,24 @@ export const paginatedUsers = async ({
44
53
  opts.startkey = page
45
54
  }
46
55
  // property specifies what to use for the page/anchor
47
- let userList, property
48
- // no search, query allDocs
49
- if (!search) {
56
+ let userList,
57
+ property = "_id",
58
+ getKey
59
+ if (appId) {
60
+ userList = await usersCore.searchGlobalUsersByApp(appId, opts)
61
+ getKey = (doc: any) => usersCore.getGlobalUserByAppPage(appId, doc)
62
+ } else if (email) {
63
+ userList = await usersCore.searchGlobalUsersByEmail(email, opts)
64
+ property = "email"
65
+ } else {
66
+ // no search, query allDocs
50
67
  const response = await db.allDocs(dbUtils.getGlobalUserParams(null, opts))
51
68
  userList = response.rows.map((row: any) => row.doc)
52
- property = "_id"
53
- } else {
54
- userList = await usersCore.searchGlobalUsersByEmail(search, opts)
55
- property = "email"
56
69
  }
57
70
  return dbUtils.pagination(userList, PAGE_LIMIT, {
58
71
  paginate: true,
59
72
  property,
73
+ getKey,
60
74
  })
61
75
  }
62
76
 
@@ -87,6 +101,49 @@ interface SaveUserOpts {
87
101
  bulkCreate?: boolean
88
102
  }
89
103
 
104
+ export const buildUser = async (
105
+ user: any,
106
+ opts: SaveUserOpts = {
107
+ hashPassword: true,
108
+ requirePassword: true,
109
+ bulkCreate: false,
110
+ },
111
+ tenantId: string,
112
+ dbUser?: any
113
+ ) => {
114
+ let { password, _id } = user
115
+
116
+ let hashedPassword
117
+ if (password) {
118
+ hashedPassword = opts.hashPassword ? await utils.hash(password) : password
119
+ } else if (dbUser) {
120
+ hashedPassword = dbUser.password
121
+ } else if (opts.requirePassword) {
122
+ throw "Password must be specified."
123
+ }
124
+
125
+ _id = _id || dbUtils.generateGlobalUserID()
126
+
127
+ user = {
128
+ createdAt: Date.now(),
129
+ ...dbUser,
130
+ ...user,
131
+ _id,
132
+ password: hashedPassword,
133
+ tenantId,
134
+ }
135
+ // make sure the roles object is always present
136
+ if (!user.roles) {
137
+ user.roles = {}
138
+ }
139
+ // add the active status to a user if its not provided
140
+ if (user.status == null) {
141
+ user.status = constants.UserStatus.ACTIVE
142
+ }
143
+
144
+ return user
145
+ }
146
+
90
147
  export const save = async (
91
148
  user: any,
92
149
  opts: SaveUserOpts = {
@@ -97,7 +154,7 @@ export const save = async (
97
154
  ) => {
98
155
  const tenantId = tenancy.getTenantId()
99
156
  const db = tenancy.getGlobalDB()
100
- let { email, password, _id } = user
157
+ let { email, _id } = user
101
158
  // make sure another user isn't using the same email
102
159
  let dbUser: any
103
160
  if (opts.bulkCreate) {
@@ -128,36 +185,19 @@ export const save = async (
128
185
  dbUser = await db.get(_id)
129
186
  }
130
187
 
131
- // get the password, make sure one is defined
132
- let hashedPassword
133
- if (password) {
134
- hashedPassword = opts.hashPassword ? await utils.hash(password) : password
135
- } else if (dbUser) {
136
- hashedPassword = dbUser.password
137
- } else if (opts.requirePassword) {
138
- throw "Password must be specified."
139
- }
140
-
141
- _id = _id || dbUtils.generateGlobalUserID()
142
- user = {
143
- createdAt: Date.now(),
144
- ...dbUser,
145
- ...user,
146
- _id,
147
- password: hashedPassword,
188
+ let builtUser = await buildUser(
189
+ user,
190
+ {
191
+ hashPassword: true,
192
+ requirePassword: user.requirePassword,
193
+ },
148
194
  tenantId,
149
- }
150
- // make sure the roles object is always present
151
- if (!user.roles) {
152
- user.roles = {}
153
- }
154
- // add the active status to a user if its not provided
155
- if (user.status == null) {
156
- user.status = constants.UserStatus.ACTIVE
157
- }
195
+ dbUser
196
+ )
197
+
158
198
  try {
159
199
  const putOpts = {
160
- password: hashedPassword,
200
+ password: builtUser.password,
161
201
  ...user,
162
202
  }
163
203
  if (opts.bulkCreate) {
@@ -166,28 +206,21 @@ export const save = async (
166
206
  // save the user to db
167
207
  let response
168
208
  const putUserFn = () => {
169
- return db.put(user)
209
+ return db.put(builtUser)
170
210
  }
171
- if (eventHelpers.isAddingBuilder(user, dbUser)) {
211
+
212
+ if (eventHelpers.isAddingBuilder(builtUser, dbUser)) {
172
213
  response = await quotas.addDeveloper(putUserFn)
173
214
  } else {
174
215
  response = await putUserFn()
175
216
  }
176
- user._rev = response.rev
177
-
178
- await eventHelpers.handleSaveEvents(user, dbUser)
217
+ builtUser._rev = response.rev
179
218
 
180
- if (env.MULTI_TENANCY) {
181
- const afterCreateTenant = () =>
182
- migrations.backPopulateMigrations({
183
- type: MigrationType.GLOBAL,
184
- tenantId,
185
- })
186
- await tenancy.tryAddTenant(tenantId, _id, email, afterCreateTenant)
187
- }
219
+ await eventHelpers.handleSaveEvents(builtUser, dbUser)
220
+ await addTenant(tenantId, _id, email)
188
221
  await cache.user.invalidateUser(response.id)
189
222
  // let server know to sync user
190
- await apps.syncUserInApps(user._id)
223
+ await apps.syncUserInApps(builtUser._id)
191
224
 
192
225
  return {
193
226
  _id: response.id,
@@ -203,9 +236,143 @@ export const save = async (
203
236
  }
204
237
  }
205
238
 
239
+ export const addTenant = async (
240
+ tenantId: string,
241
+ _id: string,
242
+ email: string
243
+ ) => {
244
+ if (env.MULTI_TENANCY) {
245
+ const afterCreateTenant = () =>
246
+ migrations.backPopulateMigrations({
247
+ type: MigrationType.GLOBAL,
248
+ tenantId,
249
+ })
250
+ await tenancy.tryAddTenant(tenantId, _id, email, afterCreateTenant)
251
+ }
252
+ }
253
+
254
+ export const bulkCreate = async (
255
+ newUsersRequested: User[],
256
+ groups: string[]
257
+ ) => {
258
+ const db = tenancy.getGlobalDB()
259
+ const tenantId = tenancy.getTenantId()
260
+
261
+ let usersToSave: any[] = []
262
+ let newUsers: any[] = []
263
+
264
+ const allUsers = await db.allDocs(
265
+ dbUtils.getGlobalUserParams(null, {
266
+ include_docs: true,
267
+ })
268
+ )
269
+ let mapped = allUsers.rows.map((row: any) => row.id)
270
+
271
+ const currentUserEmails = mapped.map((x: any) => x.email) || []
272
+ for (const newUser of newUsersRequested) {
273
+ if (
274
+ newUsers.find((x: any) => x.email === newUser.email) ||
275
+ currentUserEmails.includes(newUser.email)
276
+ ) {
277
+ continue
278
+ }
279
+ newUser.userGroups = groups
280
+ newUsers.push(newUser)
281
+ }
282
+
283
+ // Figure out how many builders we are adding and create the promises
284
+ // array that will be called by bulkDocs
285
+ let builderCount = 0
286
+ newUsers.forEach((user: any) => {
287
+ if (eventHelpers.isAddingBuilder(user, null)) {
288
+ builderCount++
289
+ }
290
+ usersToSave.push(
291
+ buildUser(
292
+ user,
293
+ {
294
+ hashPassword: true,
295
+ requirePassword: user.requirePassword,
296
+ bulkCreate: false,
297
+ },
298
+ tenantId
299
+ )
300
+ )
301
+ })
302
+
303
+ const usersToBulkSave = await Promise.all(usersToSave)
304
+ await quotas.addDevelopers(() => db.bulkDocs(usersToBulkSave), builderCount)
305
+
306
+ // Post processing of bulk added users, i.e events and cache operations
307
+ for (const user of usersToBulkSave) {
308
+ await eventHelpers.handleSaveEvents(user, null)
309
+ await apps.syncUserInApps(user._id)
310
+ }
311
+
312
+ return usersToBulkSave.map(user => {
313
+ return {
314
+ _id: user._id,
315
+ email: user.email,
316
+ }
317
+ })
318
+ }
319
+
320
+ export const bulkDelete = async (userIds: any) => {
321
+ const db = tenancy.getGlobalDB()
322
+
323
+ let groupsToModify: any = {}
324
+ let builderCount = 0
325
+ // Get users and delete
326
+ let usersToDelete = (
327
+ await db.allDocs({
328
+ include_docs: true,
329
+ keys: userIds,
330
+ })
331
+ ).rows.map((user: any) => {
332
+ // if we find a user that has an associated group, add it to
333
+ // an array so we can easily use allDocs on them later.
334
+ // This prevents us having to re-loop over all the users
335
+ if (user.doc.userGroups) {
336
+ for (let groupId of user.doc.userGroups) {
337
+ if (!Object.keys(groupsToModify).includes(groupId)) {
338
+ groupsToModify[groupId] = [user.id]
339
+ } else {
340
+ groupsToModify[groupId] = [...groupsToModify[groupId], user.id]
341
+ }
342
+ }
343
+ }
344
+
345
+ // Also figure out how many builders are being deleted
346
+ if (eventHelpers.isAddingBuilder(user.doc, null)) {
347
+ builderCount++
348
+ }
349
+
350
+ return user.doc
351
+ })
352
+
353
+ const response = await db.bulkDocs(
354
+ usersToDelete.map((user: any) => ({
355
+ ...user,
356
+ _deleted: true,
357
+ }))
358
+ )
359
+
360
+ await groupUtils.bulkDeleteGroupUsers(groupsToModify)
361
+
362
+ //Deletion post processing
363
+ for (let user of usersToDelete) {
364
+ await bulkDeleteProcessing(user)
365
+ }
366
+
367
+ await quotas.removeDevelopers(builderCount)
368
+
369
+ return response
370
+ }
371
+
206
372
  export const destroy = async (id: string, currentUser: any) => {
207
373
  const db = tenancy.getGlobalDB()
208
374
  const dbUser = await db.get(id)
375
+ let groups = dbUser.userGroups
209
376
 
210
377
  if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
211
378
  // root account holder can't be deleted from inside budibase
@@ -221,7 +388,13 @@ export const destroy = async (id: string, currentUser: any) => {
221
388
  }
222
389
 
223
390
  await deprovisioning.removeUserFromInfoDB(dbUser)
391
+
224
392
  await db.remove(dbUser._id, dbUser._rev)
393
+
394
+ if (groups) {
395
+ await groupUtils.deleteGroupUsers(groups, dbUser)
396
+ }
397
+
225
398
  await eventHelpers.handleDeleteEvents(dbUser)
226
399
  await quotas.removeUser(dbUser)
227
400
  await cache.user.invalidateUser(dbUser._id)
@@ -229,3 +402,12 @@ export const destroy = async (id: string, currentUser: any) => {
229
402
  // let server know to sync user
230
403
  await apps.syncUserInApps(dbUser._id)
231
404
  }
405
+
406
+ const bulkDeleteProcessing = async (dbUser: User) => {
407
+ await deprovisioning.removeUserFromInfoDB(dbUser)
408
+ await eventHelpers.handleDeleteEvents(dbUser)
409
+ await cache.user.invalidateUser(dbUser._id)
410
+ await sessions.invalidateSessions(dbUser._id)
411
+ // let server know to sync user
412
+ await apps.syncUserInApps(dbUser._id)
413
+ }
@@ -11,7 +11,7 @@ const { createASession } = require("@budibase/backend-core/sessions")
11
11
  const { TENANT_ID, CSRF_TOKEN } = require("./structures")
12
12
  const structures = require("./structures")
13
13
  const { doInTenant } = require("@budibase/backend-core/tenancy")
14
-
14
+ const { groups } = require("@budibase/pro")
15
15
  class TestConfiguration {
16
16
  constructor(openServer = true) {
17
17
  if (openServer) {
@@ -116,6 +116,22 @@ class TestConfiguration {
116
116
  })
117
117
  }
118
118
 
119
+ async getGroup(id) {
120
+ return doInTenant(TENANT_ID, () => {
121
+ return groups.get(id)
122
+ })
123
+ }
124
+
125
+ async saveGroup(group) {
126
+ const res = await this.getRequest()
127
+ .post(`/api/global/groups`)
128
+ .send(group)
129
+ .set(this.defaultHeaders())
130
+ .expect("Content-Type", /json/)
131
+ .expect(200)
132
+ return res.body
133
+ }
134
+
119
135
  async createUser(email, password) {
120
136
  const user = await this.getUser(structures.users.email)
121
137
  if (user) {
@@ -0,0 +1,11 @@
1
+ export const UserGroup = () => {
2
+ let group = {
3
+ apps: [],
4
+ color: "var(--spectrum-global-color-blue-600)",
5
+ icon: "UserGroup",
6
+ name: "New group",
7
+ roles: {},
8
+ users: [],
9
+ }
10
+ return group
11
+ }
@@ -1,5 +1,6 @@
1
1
  const configs = require("./configs")
2
2
  const users = require("./users")
3
+ const groups = require("./groups")
3
4
 
4
5
  const TENANT_ID = "default"
5
6
  const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
@@ -9,4 +10,5 @@ module.exports = {
9
10
  users,
10
11
  TENANT_ID,
11
12
  CSRF_TOKEN,
13
+ groups,
12
14
  }
@@ -185,14 +185,20 @@ exports.sendEmail = async (
185
185
  // if there is a link code needed this will retrieve it
186
186
  const code = await getLinkCode(purpose, email, user, info)
187
187
  const context = await getSettingsTemplateContext(purpose, code)
188
- const message = {
188
+
189
+ let message = {
189
190
  from: from || config.from,
190
- to: email,
191
191
  html: await buildEmail(purpose, email, context, {
192
192
  user,
193
193
  contents,
194
194
  }),
195
195
  }
196
+
197
+ message = {
198
+ ...message,
199
+ to: email,
200
+ }
201
+
196
202
  if (subject || config.subject) {
197
203
  message.subject = await processString(subject || config.subject, context)
198
204
  }