@budibase/worker 1.1.28 → 1.1.29-alpha.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.28",
4
+ "version": "1.1.29-alpha.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.28",
39
- "@budibase/pro": "1.1.27",
40
- "@budibase/string-templates": "^1.1.28",
41
- "@budibase/types": "^1.1.28",
38
+ "@budibase/backend-core": "^1.1.29-alpha.2",
39
+ "@budibase/pro": "1.1.29-alpha.1",
40
+ "@budibase/string-templates": "^1.1.29-alpha.2",
41
+ "@budibase/types": "^1.1.29-alpha.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": "a05e429f1713e13b4d321ad3709c3cb1913e0159"
104
+ "gitHead": "b6680b2f69b7a019cb1ddd6bfd924edeac02d9d8"
105
105
  }
@@ -3,7 +3,7 @@ import { checkInviteCode } from "../../../utilities/redis"
3
3
  import { sendEmail } from "../../../utilities/email"
4
4
  import { users } from "../../../sdk"
5
5
  import env from "../../../environment"
6
- import { User, CloudAccount } from "@budibase/types"
6
+ import { User, CloudAccount, UserGroup } from "@budibase/types"
7
7
  import {
8
8
  events,
9
9
  errors,
@@ -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
  }
@@ -84,12 +116,27 @@ export const adminUser = async (ctx: any) => {
84
116
 
85
117
  export const destroy = async (ctx: any) => {
86
118
  const id = ctx.params.id
119
+
87
120
  await users.destroy(id, ctx.user)
121
+
88
122
  ctx.body = {
89
123
  message: `User ${id} deleted.`,
90
124
  }
91
125
  }
92
126
 
127
+ export const bulkDelete = async (ctx: any) => {
128
+ const { userIds } = ctx.request.body
129
+ try {
130
+ let usersResponse = await users.bulkDelete(userIds)
131
+
132
+ ctx.body = {
133
+ message: `${usersResponse.length} user(s) deleted`,
134
+ }
135
+ } catch (err) {
136
+ ctx.throw(err)
137
+ }
138
+ }
139
+
93
140
  export const search = async (ctx: any) => {
94
141
  const paginated = await users.paginatedUsers(ctx.request.body)
95
142
  // user hashed password shouldn't ever be returned
@@ -149,6 +196,39 @@ export const invite = async (ctx: any) => {
149
196
  await events.user.invited()
150
197
  }
151
198
 
199
+ export const inviteMultiple = async (ctx: any) => {
200
+ let { emails, userInfo } = ctx.request.body
201
+ let existing = false
202
+ let existingEmail
203
+ for (let email of emails) {
204
+ if (await usersCore.getGlobalUserByEmail(email)) {
205
+ existing = true
206
+ existingEmail = email
207
+ break
208
+ }
209
+ }
210
+
211
+ if (existing) {
212
+ ctx.throw(400, `${existingEmail} already exists`)
213
+ }
214
+ if (!userInfo) {
215
+ userInfo = {}
216
+ }
217
+ userInfo.tenantId = tenancy.getTenantId()
218
+ const opts: any = {
219
+ subject: "{{ company }} platform invitation",
220
+ info: userInfo,
221
+ }
222
+
223
+ for (let i = 0; i < emails.length; i++) {
224
+ await sendEmail(emails[i], EmailTemplatePurpose.INVITATION, opts)
225
+ }
226
+
227
+ ctx.body = {
228
+ message: "Invitations have been sent.",
229
+ }
230
+ }
231
+
152
232
  export const inviteAccept = async (ctx: any) => {
153
233
  const { inviteCode, password, firstName, lastName } = ctx.request.body
154
234
  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,17 @@ 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)
51
67
  .get("/api/global/roles/:appId")
52
68
  .post(
53
69
  "/api/global/users/invite",
@@ -55,6 +71,19 @@ router
55
71
  buildInviteValidation(),
56
72
  controller.invite
57
73
  )
74
+ .post(
75
+ "/api/global/users/invite",
76
+ adminOnly,
77
+ buildInviteValidation(),
78
+ controller.invite
79
+ )
80
+ .post(
81
+ "/api/global/users/inviteMultiple",
82
+ adminOnly,
83
+ buildInviteMultipleValidation(),
84
+ controller.inviteMultiple
85
+ )
86
+
58
87
  // non-global endpoints
59
88
  .post(
60
89
  "/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,11 +15,12 @@ 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
 
22
- export const allUsers = async () => {
23
+ export const allUsers = async (newDb?: any) => {
23
24
  const db = tenancy.getGlobalDB()
24
25
  const response = await db.allDocs(
25
26
  dbUtils.getGlobalUserParams(null, {
@@ -31,8 +32,9 @@ export const allUsers = async () => {
31
32
 
32
33
  export const paginatedUsers = async ({
33
34
  page,
34
- search,
35
- }: { page?: string; search?: string } = {}) => {
35
+ email,
36
+ appId,
37
+ }: { page?: string; email?: string; appId?: string } = {}) => {
36
38
  const db = tenancy.getGlobalDB()
37
39
  // get one extra document, to have the next page
38
40
  const opts: any = {
@@ -44,19 +46,24 @@ export const paginatedUsers = async ({
44
46
  opts.startkey = page
45
47
  }
46
48
  // property specifies what to use for the page/anchor
47
- let userList, property
48
- // no search, query allDocs
49
- if (!search) {
49
+ let userList,
50
+ property = "_id",
51
+ getKey
52
+ if (appId) {
53
+ userList = await usersCore.searchGlobalUsersByApp(appId, opts)
54
+ getKey = (doc: any) => usersCore.getGlobalUserByAppPage(appId, doc)
55
+ } else if (email) {
56
+ userList = await usersCore.searchGlobalUsersByEmail(email, opts)
57
+ property = "email"
58
+ } else {
59
+ // no search, query allDocs
50
60
  const response = await db.allDocs(dbUtils.getGlobalUserParams(null, opts))
51
61
  userList = response.rows.map((row: any) => row.doc)
52
- property = "_id"
53
- } else {
54
- userList = await usersCore.searchGlobalUsersByEmail(search, opts)
55
- property = "email"
56
62
  }
57
63
  return dbUtils.pagination(userList, PAGE_LIMIT, {
58
64
  paginate: true,
59
65
  property,
66
+ getKey,
60
67
  })
61
68
  }
62
69
 
@@ -87,6 +94,49 @@ interface SaveUserOpts {
87
94
  bulkCreate?: boolean
88
95
  }
89
96
 
97
+ export const buildUser = async (
98
+ user: any,
99
+ opts: SaveUserOpts = {
100
+ hashPassword: true,
101
+ requirePassword: true,
102
+ bulkCreate: false,
103
+ },
104
+ tenantId: string,
105
+ dbUser?: any
106
+ ) => {
107
+ let { password, _id } = user
108
+
109
+ let hashedPassword
110
+ if (password) {
111
+ hashedPassword = opts.hashPassword ? await utils.hash(password) : password
112
+ } else if (dbUser) {
113
+ hashedPassword = dbUser.password
114
+ } else if (opts.requirePassword) {
115
+ throw "Password must be specified."
116
+ }
117
+
118
+ _id = _id || dbUtils.generateGlobalUserID()
119
+
120
+ user = {
121
+ createdAt: Date.now(),
122
+ ...dbUser,
123
+ ...user,
124
+ _id,
125
+ password: hashedPassword,
126
+ tenantId,
127
+ }
128
+ // make sure the roles object is always present
129
+ if (!user.roles) {
130
+ user.roles = {}
131
+ }
132
+ // add the active status to a user if its not provided
133
+ if (user.status == null) {
134
+ user.status = constants.UserStatus.ACTIVE
135
+ }
136
+
137
+ return user
138
+ }
139
+
90
140
  export const save = async (
91
141
  user: any,
92
142
  opts: SaveUserOpts = {
@@ -97,7 +147,7 @@ export const save = async (
97
147
  ) => {
98
148
  const tenantId = tenancy.getTenantId()
99
149
  const db = tenancy.getGlobalDB()
100
- let { email, password, _id } = user
150
+ let { email, _id } = user
101
151
  // make sure another user isn't using the same email
102
152
  let dbUser: any
103
153
  if (opts.bulkCreate) {
@@ -128,36 +178,19 @@ export const save = async (
128
178
  dbUser = await db.get(_id)
129
179
  }
130
180
 
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,
181
+ let builtUser = await buildUser(
182
+ user,
183
+ {
184
+ hashPassword: true,
185
+ requirePassword: user.requirePassword,
186
+ },
148
187
  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
- }
188
+ dbUser
189
+ )
190
+
158
191
  try {
159
192
  const putOpts = {
160
- password: hashedPassword,
193
+ password: builtUser.password,
161
194
  ...user,
162
195
  }
163
196
  if (opts.bulkCreate) {
@@ -166,28 +199,21 @@ export const save = async (
166
199
  // save the user to db
167
200
  let response
168
201
  const putUserFn = () => {
169
- return db.put(user)
202
+ return db.put(builtUser)
170
203
  }
171
- if (eventHelpers.isAddingBuilder(user, dbUser)) {
204
+
205
+ if (eventHelpers.isAddingBuilder(builtUser, dbUser)) {
172
206
  response = await quotas.addDeveloper(putUserFn)
173
207
  } else {
174
208
  response = await putUserFn()
175
209
  }
176
- user._rev = response.rev
210
+ builtUser._rev = response.rev
177
211
 
178
- await eventHelpers.handleSaveEvents(user, dbUser)
179
-
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
- }
212
+ await eventHelpers.handleSaveEvents(builtUser, dbUser)
213
+ await addTenant(tenantId, _id, email)
188
214
  await cache.user.invalidateUser(response.id)
189
215
  // let server know to sync user
190
- await apps.syncUserInApps(user._id)
216
+ await apps.syncUserInApps(builtUser._id)
191
217
 
192
218
  return {
193
219
  _id: response.id,
@@ -203,9 +229,143 @@ export const save = async (
203
229
  }
204
230
  }
205
231
 
232
+ export const addTenant = async (
233
+ tenantId: string,
234
+ _id: string,
235
+ email: string
236
+ ) => {
237
+ if (env.MULTI_TENANCY) {
238
+ const afterCreateTenant = () =>
239
+ migrations.backPopulateMigrations({
240
+ type: MigrationType.GLOBAL,
241
+ tenantId,
242
+ })
243
+ await tenancy.tryAddTenant(tenantId, _id, email, afterCreateTenant)
244
+ }
245
+ }
246
+
247
+ export const bulkCreate = async (
248
+ newUsersRequested: User[],
249
+ groups: string[]
250
+ ) => {
251
+ const db = tenancy.getGlobalDB()
252
+ const tenantId = tenancy.getTenantId()
253
+
254
+ let usersToSave: any[] = []
255
+ let newUsers: any[] = []
256
+
257
+ const allUsers = await db.allDocs(
258
+ dbUtils.getGlobalUserParams(null, {
259
+ include_docs: true,
260
+ })
261
+ )
262
+ let mapped = allUsers.rows.map((row: any) => row.id)
263
+
264
+ const currentUserEmails = mapped.map((x: any) => x.email) || []
265
+ for (const newUser of newUsersRequested) {
266
+ if (
267
+ newUsers.find((x: any) => x.email === newUser.email) ||
268
+ currentUserEmails.includes(newUser.email)
269
+ ) {
270
+ continue
271
+ }
272
+ newUser.userGroups = groups
273
+ newUsers.push(newUser)
274
+ }
275
+
276
+ // Figure out how many builders we are adding and create the promises
277
+ // array that will be called by bulkDocs
278
+ let builderCount = 0
279
+ newUsers.forEach((user: any) => {
280
+ if (eventHelpers.isAddingBuilder(user, null)) {
281
+ builderCount++
282
+ }
283
+ usersToSave.push(
284
+ buildUser(
285
+ user,
286
+ {
287
+ hashPassword: true,
288
+ requirePassword: user.requirePassword,
289
+ bulkCreate: false,
290
+ },
291
+ tenantId
292
+ )
293
+ )
294
+ })
295
+
296
+ const usersToBulkSave = await Promise.all(usersToSave)
297
+ await quotas.addDevelopers(() => db.bulkDocs(usersToBulkSave), builderCount)
298
+
299
+ // Post processing of bulk added users, i.e events and cache operations
300
+ for (const user of usersToBulkSave) {
301
+ await eventHelpers.handleSaveEvents(user, null)
302
+ await apps.syncUserInApps(user._id)
303
+ }
304
+
305
+ return usersToBulkSave.map(user => {
306
+ return {
307
+ _id: user._id,
308
+ email: user.email,
309
+ }
310
+ })
311
+ }
312
+
313
+ export const bulkDelete = async (userIds: any) => {
314
+ const db = tenancy.getGlobalDB()
315
+
316
+ let groupsToModify: any = {}
317
+ let builderCount = 0
318
+ // Get users and delete
319
+ let usersToDelete = (
320
+ await db.allDocs({
321
+ include_docs: true,
322
+ keys: userIds,
323
+ })
324
+ ).rows.map((user: any) => {
325
+ // if we find a user that has an associated group, add it to
326
+ // an array so we can easily use allDocs on them later.
327
+ // This prevents us having to re-loop over all the users
328
+ if (user.doc.userGroups) {
329
+ for (let groupId of user.doc.userGroups) {
330
+ if (!Object.keys(groupsToModify).includes(groupId)) {
331
+ groupsToModify[groupId] = [user.id]
332
+ } else {
333
+ groupsToModify[groupId] = [...groupsToModify[groupId], user.id]
334
+ }
335
+ }
336
+ }
337
+
338
+ // Also figure out how many builders are being deleted
339
+ if (eventHelpers.isAddingBuilder(user.doc, null)) {
340
+ builderCount++
341
+ }
342
+
343
+ return user.doc
344
+ })
345
+
346
+ const response = await db.bulkDocs(
347
+ usersToDelete.map((user: any) => ({
348
+ ...user,
349
+ _deleted: true,
350
+ }))
351
+ )
352
+
353
+ await groupUtils.bulkDeleteGroupUsers(groupsToModify)
354
+
355
+ //Deletion post processing
356
+ for (let user of usersToDelete) {
357
+ await bulkDeleteProcessing(user)
358
+ }
359
+
360
+ await quotas.removeDevelopers(builderCount)
361
+
362
+ return response
363
+ }
364
+
206
365
  export const destroy = async (id: string, currentUser: any) => {
207
366
  const db = tenancy.getGlobalDB()
208
367
  const dbUser = await db.get(id)
368
+ let groups = dbUser.userGroups
209
369
 
210
370
  if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
211
371
  // root account holder can't be deleted from inside budibase
@@ -221,7 +381,13 @@ export const destroy = async (id: string, currentUser: any) => {
221
381
  }
222
382
 
223
383
  await deprovisioning.removeUserFromInfoDB(dbUser)
384
+
224
385
  await db.remove(dbUser._id, dbUser._rev)
386
+
387
+ if (groups) {
388
+ await groupUtils.deleteGroupUsers(groups, dbUser)
389
+ }
390
+
225
391
  await eventHelpers.handleDeleteEvents(dbUser)
226
392
  await quotas.removeUser(dbUser)
227
393
  await cache.user.invalidateUser(dbUser._id)
@@ -229,3 +395,12 @@ export const destroy = async (id: string, currentUser: any) => {
229
395
  // let server know to sync user
230
396
  await apps.syncUserInApps(dbUser._id)
231
397
  }
398
+
399
+ const bulkDeleteProcessing = async (dbUser: User) => {
400
+ await deprovisioning.removeUserFromInfoDB(dbUser)
401
+ await eventHelpers.handleDeleteEvents(dbUser)
402
+ await cache.user.invalidateUser(dbUser._id)
403
+ await sessions.invalidateSessions(dbUser._id)
404
+ // let server know to sync user
405
+ await apps.syncUserInApps(dbUser._id)
406
+ }
@@ -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
  }