@budibase/worker 1.1.32-alpha.6 → 1.1.32
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 +0 -90
- package/src/api/routes/global/auth.js +1 -1
- package/src/api/routes/global/configs.js +3 -5
- 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 +2 -32
- package/src/api/routes/global/workspaces.js +5 -5
- package/src/api/routes/index.js +0 -3
- package/src/api/routes/system/tenants.js +1 -1
- package/src/api/routes/tests/users.spec.js +10 -63
- package/src/api/routes/validation/users.ts +15 -32
- package/src/sdk/users/users.ts +52 -234
- package/src/tests/TestConfiguration.js +1 -17
- package/src/tests/structures/index.js +0 -2
- package/src/utilities/email.js +2 -8
- package/src/tests/structures/groups.ts +0 -11
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "1.1.32
|
|
4
|
+
"version": "1.1.32",
|
|
5
5
|
"description": "Budibase background service",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"author": "Budibase",
|
|
36
36
|
"license": "GPL-3.0",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@budibase/backend-core": "1.1.32
|
|
39
|
-
"@budibase/pro": "1.1.
|
|
40
|
-
"@budibase/string-templates": "1.1.32
|
|
41
|
-
"@budibase/types": "1.1.32
|
|
38
|
+
"@budibase/backend-core": "^1.1.32",
|
|
39
|
+
"@budibase/pro": "1.1.31",
|
|
40
|
+
"@budibase/string-templates": "^1.1.32",
|
|
41
|
+
"@budibase/types": "^1.1.32",
|
|
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": "900d5de0723c1b7acf218446507d52755fdf7d83"
|
|
105
105
|
}
|
|
@@ -13,8 +13,6 @@ 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
|
|
18
16
|
|
|
19
17
|
export const save = async (ctx: any) => {
|
|
20
18
|
try {
|
|
@@ -24,36 +22,6 @@ export const save = async (ctx: any) => {
|
|
|
24
22
|
}
|
|
25
23
|
}
|
|
26
24
|
|
|
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
|
-
|
|
57
25
|
const parseBooleanParam = (param: any) => {
|
|
58
26
|
return !(param && param === "false")
|
|
59
27
|
}
|
|
@@ -114,39 +82,14 @@ export const adminUser = async (ctx: any) => {
|
|
|
114
82
|
})
|
|
115
83
|
}
|
|
116
84
|
|
|
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
|
-
|
|
127
85
|
export const destroy = async (ctx: any) => {
|
|
128
86
|
const id = ctx.params.id
|
|
129
|
-
|
|
130
87
|
await users.destroy(id, ctx.user)
|
|
131
|
-
|
|
132
88
|
ctx.body = {
|
|
133
89
|
message: `User ${id} deleted.`,
|
|
134
90
|
}
|
|
135
91
|
}
|
|
136
92
|
|
|
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
|
-
|
|
150
93
|
export const search = async (ctx: any) => {
|
|
151
94
|
const paginated = await users.paginatedUsers(ctx.request.body)
|
|
152
95
|
// user hashed password shouldn't ever be returned
|
|
@@ -206,39 +149,6 @@ export const invite = async (ctx: any) => {
|
|
|
206
149
|
await events.user.invited()
|
|
207
150
|
}
|
|
208
151
|
|
|
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
|
-
|
|
242
152
|
export const inviteAccept = async (ctx: any) => {
|
|
243
153
|
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
|
244
154
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const Router = require("@koa/router")
|
|
2
2
|
const authController = require("../../controllers/global/auth")
|
|
3
|
-
const
|
|
3
|
+
const joiValidator = require("../../../middleware/joi-validator")
|
|
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
|
|
4
|
-
const
|
|
3
|
+
const joiValidator = require("../../../middleware/joi-validator")
|
|
4
|
+
const adminOnly = require("../../../middleware/adminOnly")
|
|
5
5
|
const Joi = require("joi")
|
|
6
6
|
const { Configs } = require("../../../constants")
|
|
7
7
|
|
|
@@ -65,8 +65,6 @@ 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(),
|
|
70
68
|
config: Joi.alternatives()
|
|
71
69
|
.conditional("type", {
|
|
72
70
|
switch: [
|
|
@@ -77,7 +75,7 @@ function buildConfigSaveValidation() {
|
|
|
77
75
|
{ is: Configs.OIDC, then: oidcValidation() }
|
|
78
76
|
],
|
|
79
77
|
}),
|
|
80
|
-
|
|
78
|
+
}).required().unknown(true),
|
|
81
79
|
)
|
|
82
80
|
}
|
|
83
81
|
|
|
@@ -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
|
|
5
|
-
const
|
|
4
|
+
const joiValidator = require("../../../middleware/joi-validator")
|
|
5
|
+
const adminOnly = require("../../../middleware/adminOnly")
|
|
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
|
|
3
|
+
const joiValidator = require("../../../middleware/joi-validator")
|
|
4
4
|
const Joi = require("joi")
|
|
5
5
|
const { TemplatePurpose, TemplateTypes } = require("../../../constants")
|
|
6
|
-
const
|
|
6
|
+
const adminOnly = require("../../../middleware/adminOnly")
|
|
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
|
|
4
|
-
const
|
|
3
|
+
const joiValidator = require("../../../middleware/joi-validator")
|
|
4
|
+
const adminOnly = require("../../../middleware/adminOnly")
|
|
5
5
|
const Joi = require("joi")
|
|
6
6
|
const cloudRestricted = require("../../../middleware/cloudRestricted")
|
|
7
7
|
const { users } = require("../validation")
|
|
@@ -30,14 +30,6 @@ 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
|
-
|
|
41
33
|
function buildInviteAcceptValidation() {
|
|
42
34
|
// prettier-ignore
|
|
43
35
|
return joiValidator.body(Joi.object({
|
|
@@ -53,18 +45,9 @@ router
|
|
|
53
45
|
users.buildUserSaveValidation(),
|
|
54
46
|
controller.save
|
|
55
47
|
)
|
|
56
|
-
.post(
|
|
57
|
-
"/api/global/users/bulkCreate",
|
|
58
|
-
adminOnly,
|
|
59
|
-
users.buildUserBulkSaveValidation(),
|
|
60
|
-
controller.bulkCreate
|
|
61
|
-
)
|
|
62
|
-
|
|
63
48
|
.get("/api/global/users", builderOrAdmin, controller.fetch)
|
|
64
49
|
.post("/api/global/users/search", builderOrAdmin, controller.search)
|
|
65
50
|
.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)
|
|
68
51
|
.get("/api/global/roles/:appId")
|
|
69
52
|
.post(
|
|
70
53
|
"/api/global/users/invite",
|
|
@@ -72,19 +55,6 @@ router
|
|
|
72
55
|
buildInviteValidation(),
|
|
73
56
|
controller.invite
|
|
74
57
|
)
|
|
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
|
-
|
|
88
58
|
// non-global endpoints
|
|
89
59
|
.post(
|
|
90
60
|
"/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
|
|
4
|
-
const
|
|
3
|
+
const joiValidator = require("../../../middleware/joi-validator")
|
|
4
|
+
const adminOnly = require("../../../middleware/adminOnly")
|
|
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,4 +1,3 @@
|
|
|
1
|
-
const { api } = require("@budibase/pro")
|
|
2
1
|
const userRoutes = require("./global/users")
|
|
3
2
|
const configRoutes = require("./global/configs")
|
|
4
3
|
const workspaceRoutes = require("./global/workspaces")
|
|
@@ -13,7 +12,6 @@ const statusRoutes = require("./system/status")
|
|
|
13
12
|
const selfRoutes = require("./global/self")
|
|
14
13
|
const licenseRoutes = require("./global/license")
|
|
15
14
|
|
|
16
|
-
let userGroupRoutes = api.groups
|
|
17
15
|
exports.routes = [
|
|
18
16
|
configRoutes,
|
|
19
17
|
userRoutes,
|
|
@@ -28,5 +26,4 @@ exports.routes = [
|
|
|
28
26
|
statusRoutes,
|
|
29
27
|
selfRoutes,
|
|
30
28
|
licenseRoutes,
|
|
31
|
-
userGroupRoutes,
|
|
32
29
|
]
|
|
@@ -2,6 +2,7 @@ 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
|
+
|
|
5
6
|
describe("/api/global/users", () => {
|
|
6
7
|
|
|
7
8
|
beforeAll(async () => {
|
|
@@ -23,9 +24,9 @@ describe("/api/global/users", () => {
|
|
|
23
24
|
.set(config.defaultHeaders())
|
|
24
25
|
.expect("Content-Type", /json/)
|
|
25
26
|
.expect(200)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
|
|
28
|
+
const emailCall = sendMailMock.mock.calls[0][0]
|
|
29
|
+
// after this URL there should be a code
|
|
29
30
|
const parts = emailCall.html.split("http://localhost:10000/builder/invite?code=")
|
|
30
31
|
const code = parts[1].split("\"")[0].split("&")[0]
|
|
31
32
|
return { code, res }
|
|
@@ -59,7 +60,7 @@ describe("/api/global/users", () => {
|
|
|
59
60
|
expect(events.user.inviteAccepted).toBeCalledWith(user)
|
|
60
61
|
})
|
|
61
62
|
|
|
62
|
-
const createUser = async (user) => {
|
|
63
|
+
const createUser = async (user) => {
|
|
63
64
|
const existing = await config.getUser(user.email)
|
|
64
65
|
if (existing) {
|
|
65
66
|
await deleteUser(existing._id)
|
|
@@ -83,37 +84,14 @@ describe("/api/global/users", () => {
|
|
|
83
84
|
return res.body
|
|
84
85
|
}
|
|
85
86
|
|
|
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
|
-
|
|
109
87
|
const deleteUser = async (email) => {
|
|
110
88
|
const user = await config.getUser(email)
|
|
111
89
|
if (user) {
|
|
112
90
|
await request
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
91
|
+
.delete(`/api/global/users/${user._id}`)
|
|
92
|
+
.set(config.defaultHeaders())
|
|
93
|
+
.expect("Content-Type", /json/)
|
|
94
|
+
.expect(200)
|
|
117
95
|
}
|
|
118
96
|
}
|
|
119
97
|
|
|
@@ -129,25 +107,10 @@ describe("/api/global/users", () => {
|
|
|
129
107
|
expect(events.user.permissionAdminAssigned).not.toBeCalled()
|
|
130
108
|
})
|
|
131
109
|
|
|
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
|
-
|
|
147
110
|
it("should be able to create an admin user", async () => {
|
|
148
111
|
jest.clearAllMocks()
|
|
149
112
|
const user = structures.users.adminUser({ email: "admin@test.com" })
|
|
150
|
-
await createUser(user)
|
|
113
|
+
await createUser(user)
|
|
151
114
|
|
|
152
115
|
expect(events.user.created).toBeCalledTimes(1)
|
|
153
116
|
expect(events.user.updated).not.toBeCalled()
|
|
@@ -370,21 +333,5 @@ describe("/api/global/users", () => {
|
|
|
370
333
|
expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1)
|
|
371
334
|
expect(events.user.permissionAdminRemoved).not.toBeCalled()
|
|
372
335
|
})
|
|
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
|
-
|
|
389
336
|
})
|
|
390
337
|
})
|
|
@@ -1,34 +1,22 @@
|
|
|
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
|
-
|
|
20
4
|
export const buildUserSaveValidation = (isSelf = false) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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),
|
|
27
19
|
}
|
|
28
|
-
return joiValidator.body(Joi.object(schema).required().unknown(true))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const buildUserBulkSaveValidation = (isSelf = false) => {
|
|
32
20
|
if (!isSelf) {
|
|
33
21
|
schema = {
|
|
34
22
|
...schema,
|
|
@@ -36,10 +24,5 @@ export const buildUserBulkSaveValidation = (isSelf = false) => {
|
|
|
36
24
|
_rev: Joi.string(),
|
|
37
25
|
}
|
|
38
26
|
}
|
|
39
|
-
|
|
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))
|
|
27
|
+
return joiValidator.body(Joi.object(schema).required().unknown(true))
|
|
45
28
|
}
|
package/src/sdk/users/users.ts
CHANGED
|
@@ -15,8 +15,7 @@ import {
|
|
|
15
15
|
accounts,
|
|
16
16
|
migrations,
|
|
17
17
|
} from "@budibase/backend-core"
|
|
18
|
-
import { MigrationType
|
|
19
|
-
import { groups as groupUtils } from "@budibase/pro"
|
|
18
|
+
import { MigrationType } from "@budibase/types"
|
|
20
19
|
|
|
21
20
|
const PAGE_LIMIT = 8
|
|
22
21
|
|
|
@@ -30,18 +29,10 @@ export const allUsers = async () => {
|
|
|
30
29
|
return response.rows.map((row: any) => row.doc)
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
export const countUsersByApp = async (appId: string) => {
|
|
34
|
-
let response: any = await usersCore.searchGlobalUsersByApp(appId, {})
|
|
35
|
-
return {
|
|
36
|
-
userCount: response.length,
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
32
|
export const paginatedUsers = async ({
|
|
41
33
|
page,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}: { page?: string; email?: string; appId?: string } = {}) => {
|
|
34
|
+
search,
|
|
35
|
+
}: { page?: string; search?: string } = {}) => {
|
|
45
36
|
const db = tenancy.getGlobalDB()
|
|
46
37
|
// get one extra document, to have the next page
|
|
47
38
|
const opts: any = {
|
|
@@ -53,24 +44,19 @@ export const paginatedUsers = async ({
|
|
|
53
44
|
opts.startkey = page
|
|
54
45
|
}
|
|
55
46
|
// property specifies what to use for the page/anchor
|
|
56
|
-
let userList,
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
47
|
+
let userList, property
|
|
48
|
+
// no search, query allDocs
|
|
49
|
+
if (!search) {
|
|
67
50
|
const response = await db.allDocs(dbUtils.getGlobalUserParams(null, opts))
|
|
68
51
|
userList = response.rows.map((row: any) => row.doc)
|
|
52
|
+
property = "_id"
|
|
53
|
+
} else {
|
|
54
|
+
userList = await usersCore.searchGlobalUsersByEmail(search, opts)
|
|
55
|
+
property = "email"
|
|
69
56
|
}
|
|
70
57
|
return dbUtils.pagination(userList, PAGE_LIMIT, {
|
|
71
58
|
paginate: true,
|
|
72
59
|
property,
|
|
73
|
-
getKey,
|
|
74
60
|
})
|
|
75
61
|
}
|
|
76
62
|
|
|
@@ -101,49 +87,6 @@ interface SaveUserOpts {
|
|
|
101
87
|
bulkCreate?: boolean
|
|
102
88
|
}
|
|
103
89
|
|
|
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
|
-
|
|
147
90
|
export const save = async (
|
|
148
91
|
user: any,
|
|
149
92
|
opts: SaveUserOpts = {
|
|
@@ -154,7 +97,7 @@ export const save = async (
|
|
|
154
97
|
) => {
|
|
155
98
|
const tenantId = tenancy.getTenantId()
|
|
156
99
|
const db = tenancy.getGlobalDB()
|
|
157
|
-
let { email, _id } = user
|
|
100
|
+
let { email, password, _id } = user
|
|
158
101
|
// make sure another user isn't using the same email
|
|
159
102
|
let dbUser: any
|
|
160
103
|
if (opts.bulkCreate) {
|
|
@@ -185,19 +128,36 @@ export const save = async (
|
|
|
185
128
|
dbUser = await db.get(_id)
|
|
186
129
|
}
|
|
187
130
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
}
|
|
197
140
|
|
|
141
|
+
_id = _id || dbUtils.generateGlobalUserID()
|
|
142
|
+
user = {
|
|
143
|
+
createdAt: Date.now(),
|
|
144
|
+
...dbUser,
|
|
145
|
+
...user,
|
|
146
|
+
_id,
|
|
147
|
+
password: hashedPassword,
|
|
148
|
+
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
|
+
}
|
|
198
158
|
try {
|
|
199
159
|
const putOpts = {
|
|
200
|
-
password:
|
|
160
|
+
password: hashedPassword,
|
|
201
161
|
...user,
|
|
202
162
|
}
|
|
203
163
|
if (opts.bulkCreate) {
|
|
@@ -206,21 +166,28 @@ export const save = async (
|
|
|
206
166
|
// save the user to db
|
|
207
167
|
let response
|
|
208
168
|
const putUserFn = () => {
|
|
209
|
-
return db.put(
|
|
169
|
+
return db.put(user)
|
|
210
170
|
}
|
|
211
|
-
|
|
212
|
-
if (eventHelpers.isAddingBuilder(builtUser, dbUser)) {
|
|
171
|
+
if (eventHelpers.isAddingBuilder(user, dbUser)) {
|
|
213
172
|
response = await quotas.addDeveloper(putUserFn)
|
|
214
173
|
} else {
|
|
215
174
|
response = await putUserFn()
|
|
216
175
|
}
|
|
217
|
-
|
|
176
|
+
user._rev = response.rev
|
|
177
|
+
|
|
178
|
+
await eventHelpers.handleSaveEvents(user, dbUser)
|
|
218
179
|
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
}
|
|
221
188
|
await cache.user.invalidateUser(response.id)
|
|
222
189
|
// let server know to sync user
|
|
223
|
-
await apps.syncUserInApps(
|
|
190
|
+
await apps.syncUserInApps(user._id)
|
|
224
191
|
|
|
225
192
|
return {
|
|
226
193
|
_id: response.id,
|
|
@@ -236,143 +203,9 @@ export const save = async (
|
|
|
236
203
|
}
|
|
237
204
|
}
|
|
238
205
|
|
|
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
|
-
|
|
372
206
|
export const destroy = async (id: string, currentUser: any) => {
|
|
373
207
|
const db = tenancy.getGlobalDB()
|
|
374
208
|
const dbUser = await db.get(id)
|
|
375
|
-
let groups = dbUser.userGroups
|
|
376
209
|
|
|
377
210
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
378
211
|
// root account holder can't be deleted from inside budibase
|
|
@@ -388,13 +221,7 @@ export const destroy = async (id: string, currentUser: any) => {
|
|
|
388
221
|
}
|
|
389
222
|
|
|
390
223
|
await deprovisioning.removeUserFromInfoDB(dbUser)
|
|
391
|
-
|
|
392
224
|
await db.remove(dbUser._id, dbUser._rev)
|
|
393
|
-
|
|
394
|
-
if (groups) {
|
|
395
|
-
await groupUtils.deleteGroupUsers(groups, dbUser)
|
|
396
|
-
}
|
|
397
|
-
|
|
398
225
|
await eventHelpers.handleDeleteEvents(dbUser)
|
|
399
226
|
await quotas.removeUser(dbUser)
|
|
400
227
|
await cache.user.invalidateUser(dbUser._id)
|
|
@@ -402,12 +229,3 @@ export const destroy = async (id: string, currentUser: any) => {
|
|
|
402
229
|
// let server know to sync user
|
|
403
230
|
await apps.syncUserInApps(dbUser._id)
|
|
404
231
|
}
|
|
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
|
+
|
|
15
15
|
class TestConfiguration {
|
|
16
16
|
constructor(openServer = true) {
|
|
17
17
|
if (openServer) {
|
|
@@ -116,22 +116,6 @@ 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
|
-
|
|
135
119
|
async createUser(email, password) {
|
|
136
120
|
const user = await this.getUser(structures.users.email)
|
|
137
121
|
if (user) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const configs = require("./configs")
|
|
2
2
|
const users = require("./users")
|
|
3
|
-
const groups = require("./groups")
|
|
4
3
|
|
|
5
4
|
const TENANT_ID = "default"
|
|
6
5
|
const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306"
|
|
@@ -10,5 +9,4 @@ module.exports = {
|
|
|
10
9
|
users,
|
|
11
10
|
TENANT_ID,
|
|
12
11
|
CSRF_TOKEN,
|
|
13
|
-
groups,
|
|
14
12
|
}
|
package/src/utilities/email.js
CHANGED
|
@@ -185,20 +185,14 @@ 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
|
-
|
|
189
|
-
let message = {
|
|
188
|
+
const message = {
|
|
190
189
|
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
|
-
|
|
202
196
|
if (subject || config.subject) {
|
|
203
197
|
message.subject = await processString(subject || config.subject, context)
|
|
204
198
|
}
|