@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 +6 -6
- package/src/api/controllers/global/users.ts +90 -0
- package/src/api/routes/global/auth.js +1 -1
- package/src/api/routes/global/configs.js +5 -3
- package/src/api/routes/global/email.js +2 -2
- package/src/api/routes/global/roles.js +1 -1
- package/src/api/routes/global/sessions.js +1 -1
- package/src/api/routes/global/templates.js +2 -2
- package/src/api/routes/global/users.js +32 -2
- package/src/api/routes/global/workspaces.js +5 -5
- package/src/api/routes/index.js +3 -0
- package/src/api/routes/system/tenants.js +1 -1
- package/src/api/routes/tests/users.spec.js +63 -10
- package/src/api/routes/validation/users.ts +32 -15
- package/src/sdk/users/users.ts +234 -52
- package/src/tests/TestConfiguration.js +17 -1
- package/src/tests/structures/groups.ts +11 -0
- package/src/tests/structures/index.js +2 -0
- package/src/utilities/email.js +8 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "1.
|
|
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.
|
|
39
|
-
"@budibase/pro": "1.1.
|
|
40
|
-
"@budibase/string-templates": "^1.
|
|
41
|
-
"@budibase/types": "^1.
|
|
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": "
|
|
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("
|
|
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("
|
|
4
|
-
const adminOnly = require("
|
|
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
|
-
|
|
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("
|
|
5
|
-
const adminOnly = require("
|
|
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,9 +1,9 @@
|
|
|
1
1
|
const Router = require("@koa/router")
|
|
2
2
|
const controller = require("../../controllers/global/templates")
|
|
3
|
-
const joiValidator = require("
|
|
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("
|
|
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("
|
|
4
|
-
const adminOnly = require("
|
|
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("
|
|
4
|
-
const adminOnly = require("
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
.pattern(/.*/, Joi.string())
|
|
21
|
+
.required()
|
|
22
|
+
.unknown(true),
|
|
23
23
|
}).unknown(true).optional(),
|
|
24
24
|
}).required().unknown(true))
|
|
25
25
|
}
|
package/src/api/routes/index.js
CHANGED
|
@@ -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
|
]
|
|
@@ -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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
}
|
package/src/sdk/users/users.ts
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
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,
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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:
|
|
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(
|
|
209
|
+
return db.put(builtUser)
|
|
170
210
|
}
|
|
171
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
await eventHelpers.handleSaveEvents(user, dbUser)
|
|
217
|
+
builtUser._rev = response.rev
|
|
179
218
|
|
|
180
|
-
|
|
181
|
-
|
|
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(
|
|
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) {
|
|
@@ -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
|
}
|
package/src/utilities/email.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|