@budibase/worker 1.1.25-alpha.2 → 1.1.27
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 +1 -81
- 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 -31
- 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 +53 -228
- 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.
|
|
4
|
+
"version": "1.1.27",
|
|
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.
|
|
39
|
-
"@budibase/pro": "1.1.
|
|
40
|
-
"@budibase/string-templates": "^1.1.
|
|
41
|
-
"@budibase/types": "^1.1.
|
|
38
|
+
"@budibase/backend-core": "^1.1.27",
|
|
39
|
+
"@budibase/pro": "1.1.26",
|
|
40
|
+
"@budibase/string-templates": "^1.1.27",
|
|
41
|
+
"@budibase/types": "^1.1.27",
|
|
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": "ce7b4989f2a9e575370f4ccfd7fa9d8db27dfc13"
|
|
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
|
|
6
|
+
import { User, CloudAccount } from "@budibase/types"
|
|
7
7
|
import {
|
|
8
8
|
events,
|
|
9
9
|
errors,
|
|
@@ -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
|
}
|
|
@@ -116,27 +84,12 @@ export const adminUser = async (ctx: any) => {
|
|
|
116
84
|
|
|
117
85
|
export const destroy = async (ctx: any) => {
|
|
118
86
|
const id = ctx.params.id
|
|
119
|
-
|
|
120
87
|
await users.destroy(id, ctx.user)
|
|
121
|
-
|
|
122
88
|
ctx.body = {
|
|
123
89
|
message: `User ${id} deleted.`,
|
|
124
90
|
}
|
|
125
91
|
}
|
|
126
92
|
|
|
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
|
-
|
|
140
93
|
export const search = async (ctx: any) => {
|
|
141
94
|
const paginated = await users.paginatedUsers(ctx.request.body)
|
|
142
95
|
// user hashed password shouldn't ever be returned
|
|
@@ -196,39 +149,6 @@ export const invite = async (ctx: any) => {
|
|
|
196
149
|
await events.user.invited()
|
|
197
150
|
}
|
|
198
151
|
|
|
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
|
-
|
|
232
152
|
export const inviteAccept = async (ctx: any) => {
|
|
233
153
|
const { inviteCode, password, firstName, lastName } = ctx.request.body
|
|
234
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,17 +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
51
|
.get("/api/global/roles/:appId")
|
|
68
52
|
.post(
|
|
69
53
|
"/api/global/users/invite",
|
|
@@ -71,19 +55,6 @@ router
|
|
|
71
55
|
buildInviteValidation(),
|
|
72
56
|
controller.invite
|
|
73
57
|
)
|
|
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
|
-
|
|
87
58
|
// non-global endpoints
|
|
88
59
|
.post(
|
|
89
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,12 +15,11 @@ 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
|
|
|
23
|
-
export const allUsers = async (
|
|
22
|
+
export const allUsers = async () => {
|
|
24
23
|
const db = tenancy.getGlobalDB()
|
|
25
24
|
const response = await db.allDocs(
|
|
26
25
|
dbUtils.getGlobalUserParams(null, {
|
|
@@ -32,9 +31,8 @@ export const allUsers = async (newDb?: any) => {
|
|
|
32
31
|
|
|
33
32
|
export const paginatedUsers = async ({
|
|
34
33
|
page,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}: { page?: string; email?: string; appId?: string } = {}) => {
|
|
34
|
+
search,
|
|
35
|
+
}: { page?: string; search?: string } = {}) => {
|
|
38
36
|
const db = tenancy.getGlobalDB()
|
|
39
37
|
// get one extra document, to have the next page
|
|
40
38
|
const opts: any = {
|
|
@@ -46,24 +44,19 @@ export const paginatedUsers = async ({
|
|
|
46
44
|
opts.startkey = page
|
|
47
45
|
}
|
|
48
46
|
// property specifies what to use for the page/anchor
|
|
49
|
-
let userList,
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
47
|
+
let userList, property
|
|
48
|
+
// no search, query allDocs
|
|
49
|
+
if (!search) {
|
|
60
50
|
const response = await db.allDocs(dbUtils.getGlobalUserParams(null, opts))
|
|
61
51
|
userList = response.rows.map((row: any) => row.doc)
|
|
52
|
+
property = "_id"
|
|
53
|
+
} else {
|
|
54
|
+
userList = await usersCore.searchGlobalUsersByEmail(search, opts)
|
|
55
|
+
property = "email"
|
|
62
56
|
}
|
|
63
57
|
return dbUtils.pagination(userList, PAGE_LIMIT, {
|
|
64
58
|
paginate: true,
|
|
65
59
|
property,
|
|
66
|
-
getKey,
|
|
67
60
|
})
|
|
68
61
|
}
|
|
69
62
|
|
|
@@ -94,49 +87,6 @@ interface SaveUserOpts {
|
|
|
94
87
|
bulkCreate?: boolean
|
|
95
88
|
}
|
|
96
89
|
|
|
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
|
-
|
|
140
90
|
export const save = async (
|
|
141
91
|
user: any,
|
|
142
92
|
opts: SaveUserOpts = {
|
|
@@ -147,7 +97,7 @@ export const save = async (
|
|
|
147
97
|
) => {
|
|
148
98
|
const tenantId = tenancy.getTenantId()
|
|
149
99
|
const db = tenancy.getGlobalDB()
|
|
150
|
-
let { email, _id } = user
|
|
100
|
+
let { email, password, _id } = user
|
|
151
101
|
// make sure another user isn't using the same email
|
|
152
102
|
let dbUser: any
|
|
153
103
|
if (opts.bulkCreate) {
|
|
@@ -178,19 +128,36 @@ export const save = async (
|
|
|
178
128
|
dbUser = await db.get(_id)
|
|
179
129
|
}
|
|
180
130
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
+
}
|
|
190
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
|
+
}
|
|
191
158
|
try {
|
|
192
159
|
const putOpts = {
|
|
193
|
-
password:
|
|
160
|
+
password: hashedPassword,
|
|
194
161
|
...user,
|
|
195
162
|
}
|
|
196
163
|
if (opts.bulkCreate) {
|
|
@@ -199,21 +166,28 @@ export const save = async (
|
|
|
199
166
|
// save the user to db
|
|
200
167
|
let response
|
|
201
168
|
const putUserFn = () => {
|
|
202
|
-
return db.put(
|
|
169
|
+
return db.put(user)
|
|
203
170
|
}
|
|
204
|
-
|
|
205
|
-
if (eventHelpers.isAddingBuilder(builtUser, dbUser)) {
|
|
171
|
+
if (eventHelpers.isAddingBuilder(user, dbUser)) {
|
|
206
172
|
response = await quotas.addDeveloper(putUserFn)
|
|
207
173
|
} else {
|
|
208
174
|
response = await putUserFn()
|
|
209
175
|
}
|
|
210
|
-
|
|
176
|
+
user._rev = response.rev
|
|
211
177
|
|
|
212
|
-
await eventHelpers.handleSaveEvents(
|
|
213
|
-
|
|
178
|
+
await eventHelpers.handleSaveEvents(user, dbUser)
|
|
179
|
+
|
|
180
|
+
if (env.MULTI_TENANCY) {
|
|
181
|
+
const afterCreateTenant = () =>
|
|
182
|
+
migrations.backPopulateMigrations({
|
|
183
|
+
type: MigrationType.GLOBAL,
|
|
184
|
+
tenantId,
|
|
185
|
+
})
|
|
186
|
+
await tenancy.tryAddTenant(tenantId, _id, email, afterCreateTenant)
|
|
187
|
+
}
|
|
214
188
|
await cache.user.invalidateUser(response.id)
|
|
215
189
|
// let server know to sync user
|
|
216
|
-
await apps.syncUserInApps(
|
|
190
|
+
await apps.syncUserInApps(user._id)
|
|
217
191
|
|
|
218
192
|
return {
|
|
219
193
|
_id: response.id,
|
|
@@ -229,143 +203,9 @@ export const save = async (
|
|
|
229
203
|
}
|
|
230
204
|
}
|
|
231
205
|
|
|
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
|
-
|
|
365
206
|
export const destroy = async (id: string, currentUser: any) => {
|
|
366
207
|
const db = tenancy.getGlobalDB()
|
|
367
208
|
const dbUser = await db.get(id)
|
|
368
|
-
let groups = dbUser.userGroups
|
|
369
209
|
|
|
370
210
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
371
211
|
// root account holder can't be deleted from inside budibase
|
|
@@ -381,13 +221,7 @@ export const destroy = async (id: string, currentUser: any) => {
|
|
|
381
221
|
}
|
|
382
222
|
|
|
383
223
|
await deprovisioning.removeUserFromInfoDB(dbUser)
|
|
384
|
-
|
|
385
224
|
await db.remove(dbUser._id, dbUser._rev)
|
|
386
|
-
|
|
387
|
-
if (groups) {
|
|
388
|
-
await groupUtils.deleteGroupUsers(groups, dbUser)
|
|
389
|
-
}
|
|
390
|
-
|
|
391
225
|
await eventHelpers.handleDeleteEvents(dbUser)
|
|
392
226
|
await quotas.removeUser(dbUser)
|
|
393
227
|
await cache.user.invalidateUser(dbUser._id)
|
|
@@ -395,12 +229,3 @@ export const destroy = async (id: string, currentUser: any) => {
|
|
|
395
229
|
// let server know to sync user
|
|
396
230
|
await apps.syncUserInApps(dbUser._id)
|
|
397
231
|
}
|
|
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
|
+
|
|
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
|
}
|