@budibase/worker 1.1.31 → 1.1.32-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 +6 -6
- package/src/api/controllers/global/users.ts +81 -1
- 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 +31 -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 +228 -53
- 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.1.
|
|
4
|
+
"version": "1.1.32-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": "
|
|
39
|
-
"@budibase/pro": "1.1.
|
|
40
|
-
"@budibase/string-templates": "
|
|
41
|
-
"@budibase/types": "
|
|
38
|
+
"@budibase/backend-core": "1.1.32-alpha.2",
|
|
39
|
+
"@budibase/pro": "1.1.32-alpha.1",
|
|
40
|
+
"@budibase/string-templates": "1.1.32-alpha.2",
|
|
41
|
+
"@budibase/types": "1.1.32-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": "
|
|
104
|
+
"gitHead": "ec8c17db7d93b365efa683cc511c15648e9d5928"
|
|
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("
|
|
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,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("
|
|
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,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
|
-
|
|
35
|
-
|
|
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,
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
181
|
+
let builtUser = await buildUser(
|
|
182
|
+
user,
|
|
183
|
+
{
|
|
184
|
+
hashPassword: true,
|
|
185
|
+
requirePassword: user.requirePassword,
|
|
186
|
+
},
|
|
148
187
|
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
|
-
}
|
|
188
|
+
dbUser
|
|
189
|
+
)
|
|
190
|
+
|
|
158
191
|
try {
|
|
159
192
|
const putOpts = {
|
|
160
|
-
password:
|
|
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(
|
|
202
|
+
return db.put(builtUser)
|
|
170
203
|
}
|
|
171
|
-
|
|
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
|
-
|
|
210
|
+
builtUser._rev = response.rev
|
|
177
211
|
|
|
178
|
-
await eventHelpers.handleSaveEvents(
|
|
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(
|
|
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) {
|
|
@@ -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
|
}
|