@budibase/worker 1.2.58 → 1.2.59-alpha.0
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 +7 -6
- package/scripts/jestSetup.js +6 -0
- package/src/api/controllers/global/auth.ts +14 -1
- package/src/api/controllers/global/users.ts +26 -55
- package/src/api/controllers/system/accounts.ts +21 -0
- package/src/api/{index.js → index.ts} +12 -16
- package/src/api/routes/global/configs.js +1 -0
- package/src/api/routes/{tests/auth.spec.js → global/tests/auth.spec.ts} +32 -56
- package/src/api/routes/{tests/configs.spec.js → global/tests/configs.spec.ts} +76 -47
- package/src/api/routes/{tests/email.spec.js → global/tests/email.spec.ts} +7 -15
- package/src/api/routes/{tests/realEmail.spec.js → global/tests/realEmail.spec.ts} +20 -21
- package/src/api/routes/{tests/self.spec.js → global/tests/self.spec.ts} +10 -18
- package/src/api/routes/global/tests/users.spec.ts +512 -0
- package/src/api/routes/index.js +2 -0
- package/src/api/routes/system/accounts.ts +19 -0
- package/src/api/routes/system/tests/accounts.spec.ts +57 -0
- package/src/{environment.js → environment.ts} +10 -9
- package/src/index.ts +6 -4
- package/src/sdk/accounts/accounts.ts +53 -0
- package/src/sdk/accounts/index.ts +1 -0
- package/src/sdk/index.ts +1 -0
- package/src/sdk/users/users.ts +271 -77
- package/src/tests/TestConfiguration.ts +273 -0
- package/src/tests/api/accounts.ts +28 -0
- package/src/tests/api/auth.ts +48 -0
- package/src/tests/api/configs.ts +40 -0
- package/src/tests/api/email.ts +24 -0
- package/src/tests/api/index.ts +25 -0
- package/src/tests/api/self.ts +21 -0
- package/src/tests/api/users.ts +111 -0
- package/src/tests/index.ts +14 -0
- package/src/tests/mocks/index.ts +7 -0
- package/src/tests/structures/accounts.ts +24 -0
- package/src/tests/structures/index.ts +20 -0
- package/src/tests/structures/users.ts +12 -4
- package/src/utilities/redis.js +1 -0
- package/scripts/load/users.js +0 -97
- package/src/api/routes/tests/users.spec.js +0 -390
- package/src/tests/TestConfiguration.js +0 -231
- package/src/tests/index.js +0 -12
- package/src/tests/mocks/index.js +0 -5
- package/src/tests/structures/index.js +0 -14
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.59-alpha.0",
|
|
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.2.
|
|
40
|
-
"@budibase/string-templates": "
|
|
41
|
-
"@budibase/types": "
|
|
38
|
+
"@budibase/backend-core": "1.2.59-alpha.0",
|
|
39
|
+
"@budibase/pro": "1.2.58",
|
|
40
|
+
"@budibase/string-templates": "1.2.59-alpha.0",
|
|
41
|
+
"@budibase/types": "1.2.59-alpha.0",
|
|
42
42
|
"@koa/router": "8.0.8",
|
|
43
43
|
"@sentry/node": "6.17.7",
|
|
44
44
|
"@techpass/passport-openidconnect": "0.3.2",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"@types/koa-router": "7.4.4",
|
|
75
75
|
"@types/koa__router": "8.0.11",
|
|
76
76
|
"@types/node": "14.18.20",
|
|
77
|
+
"@types/uuid": "8.3.4",
|
|
77
78
|
"@typescript-eslint/parser": "5.12.0",
|
|
78
79
|
"copyfiles": "2.4.1",
|
|
79
80
|
"eslint": "6.8.0",
|
|
@@ -102,5 +103,5 @@
|
|
|
102
103
|
"./scripts/jestSetup.js"
|
|
103
104
|
]
|
|
104
105
|
},
|
|
105
|
-
"gitHead": "
|
|
106
|
+
"gitHead": "365df003a72dfe4de8575334b5302ccbf6b2ce4f"
|
|
106
107
|
}
|
package/scripts/jestSetup.js
CHANGED
|
@@ -14,3 +14,9 @@ const tk = require("timekeeper")
|
|
|
14
14
|
tk.freeze(mocks.date.MOCK_DATE)
|
|
15
15
|
|
|
16
16
|
global.console.log = jest.fn() // console.log are ignored in tests
|
|
17
|
+
|
|
18
|
+
if (!process.env.CI) {
|
|
19
|
+
// set a longer timeout in dev for debugging
|
|
20
|
+
// 100 seconds
|
|
21
|
+
jest.setTimeout(100000)
|
|
22
|
+
}
|
|
@@ -227,9 +227,22 @@ export const oidcPreAuth = async (ctx: any, next: any) => {
|
|
|
227
227
|
|
|
228
228
|
setCookie(ctx, configId, Cookies.OIDC_CONFIG)
|
|
229
229
|
|
|
230
|
+
const db = getGlobalDB()
|
|
231
|
+
const config = await core.db.getScopedConfig(db, {
|
|
232
|
+
type: Configs.OIDC,
|
|
233
|
+
group: ctx.query.group,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
|
|
237
|
+
|
|
238
|
+
let authScopes =
|
|
239
|
+
chosenConfig.scopes?.length > 0
|
|
240
|
+
? chosenConfig.scopes
|
|
241
|
+
: ["profile", "email", "offline_access"]
|
|
242
|
+
|
|
230
243
|
return passport.authenticate(strategy, {
|
|
231
244
|
// required 'openid' scope is added by oidc strategy factory
|
|
232
|
-
scope:
|
|
245
|
+
scope: authScopes,
|
|
233
246
|
})(ctx, next)
|
|
234
247
|
}
|
|
235
248
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { EmailTemplatePurpose } from "../../../constants"
|
|
2
1
|
import { checkInviteCode } from "../../../utilities/redis"
|
|
3
|
-
import { sendEmail } from "../../../utilities/email"
|
|
4
2
|
import { users } from "../../../sdk"
|
|
5
3
|
import env from "../../../environment"
|
|
6
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BulkDeleteUsersRequest,
|
|
6
|
+
CloudAccount,
|
|
7
|
+
InviteUserRequest,
|
|
8
|
+
InviteUsersRequest,
|
|
9
|
+
User,
|
|
10
|
+
} from "@budibase/types"
|
|
7
11
|
import {
|
|
8
12
|
accounts,
|
|
9
13
|
cache,
|
|
@@ -46,8 +50,8 @@ export const bulkCreate = async (ctx: any) => {
|
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
try {
|
|
49
|
-
|
|
50
|
-
await groupUtils.bulkSaveGroupUsers(groupsToSave, response)
|
|
53
|
+
const response = await users.bulkCreate(newUsersRequested, groups)
|
|
54
|
+
await groupUtils.bulkSaveGroupUsers(groupsToSave, response.successful)
|
|
51
55
|
|
|
52
56
|
ctx.body = response
|
|
53
57
|
} catch (err: any) {
|
|
@@ -138,17 +142,15 @@ export const destroy = async (ctx: any) => {
|
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
export const bulkDelete = async (ctx: any) => {
|
|
141
|
-
const { userIds } = ctx.request.body
|
|
145
|
+
const { userIds } = ctx.request.body as BulkDeleteUsersRequest
|
|
142
146
|
if (userIds?.indexOf(ctx.user._id) !== -1) {
|
|
143
147
|
ctx.throw(400, "Unable to delete self.")
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
try {
|
|
147
|
-
let
|
|
151
|
+
let response = await users.bulkDelete(userIds)
|
|
148
152
|
|
|
149
|
-
ctx.body =
|
|
150
|
-
message: `${usersResponse.length} user(s) deleted`,
|
|
151
|
-
}
|
|
153
|
+
ctx.body = response
|
|
152
154
|
} catch (err) {
|
|
153
155
|
ctx.throw(err)
|
|
154
156
|
}
|
|
@@ -193,58 +195,27 @@ export const tenantUserLookup = async (ctx: any) => {
|
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
export const invite = async (ctx: any) => {
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
info: userInfo,
|
|
198
|
+
const request = ctx.request.body as InviteUserRequest
|
|
199
|
+
const response = await users.invite([request])
|
|
200
|
+
|
|
201
|
+
// explicitly throw for single user invite
|
|
202
|
+
if (response.unsuccessful.length) {
|
|
203
|
+
const reason = response.unsuccessful[0].reason
|
|
204
|
+
if (reason === "Unavailable") {
|
|
205
|
+
ctx.throw(400, reason)
|
|
206
|
+
} else {
|
|
207
|
+
ctx.throw(500, reason)
|
|
208
|
+
}
|
|
208
209
|
}
|
|
209
|
-
|
|
210
|
+
|
|
210
211
|
ctx.body = {
|
|
211
212
|
message: "Invitation has been sent.",
|
|
212
213
|
}
|
|
213
|
-
await events.user.invited()
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
export const inviteMultiple = async (ctx: any) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
let existingEmail
|
|
220
|
-
for (let user of users) {
|
|
221
|
-
if (await usersCore.getGlobalUserByEmail(user.email)) {
|
|
222
|
-
existing = true
|
|
223
|
-
existingEmail = user.email
|
|
224
|
-
break
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (existing) {
|
|
229
|
-
ctx.throw(400, `${existingEmail} already exists`)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
for (let i = 0; i < users.length; i++) {
|
|
233
|
-
let userInfo = users[i].userInfo
|
|
234
|
-
if (!userInfo) {
|
|
235
|
-
userInfo = {}
|
|
236
|
-
}
|
|
237
|
-
userInfo.tenantId = tenancy.getTenantId()
|
|
238
|
-
const opts: any = {
|
|
239
|
-
subject: "{{ company }} platform invitation",
|
|
240
|
-
info: userInfo,
|
|
241
|
-
}
|
|
242
|
-
await sendEmail(users[i].email, EmailTemplatePurpose.INVITATION, opts)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
ctx.body = {
|
|
246
|
-
message: "Invitations have been sent.",
|
|
247
|
-
}
|
|
217
|
+
const request = ctx.request.body as InviteUsersRequest
|
|
218
|
+
ctx.body = await users.invite(request)
|
|
248
219
|
}
|
|
249
220
|
|
|
250
221
|
export const inviteAccept = async (ctx: any) => {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Account, AccountMetadata } from "@budibase/types"
|
|
2
|
+
import { accounts } from "../../../sdk"
|
|
3
|
+
|
|
4
|
+
export const save = async (ctx: any) => {
|
|
5
|
+
const account = ctx.request.body as Account
|
|
6
|
+
let metadata: AccountMetadata = {
|
|
7
|
+
_id: accounts.formatAccountMetadataId(account.accountId),
|
|
8
|
+
email: account.email,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
metadata = await accounts.saveMetadata(metadata)
|
|
12
|
+
|
|
13
|
+
ctx.body = metadata
|
|
14
|
+
ctx.status = 200
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const destroy = async (ctx: any) => {
|
|
18
|
+
const accountId = accounts.formatAccountMetadataId(ctx.params.accountId)
|
|
19
|
+
await accounts.destroyMetadata(accountId)
|
|
20
|
+
ctx.status = 204
|
|
21
|
+
}
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import Router from "@koa/router"
|
|
2
2
|
const compress = require("koa-compress")
|
|
3
3
|
const zlib = require("zlib")
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
buildTenancyMiddleware,
|
|
9
|
-
buildCsrfMiddleware,
|
|
10
|
-
} = require("@budibase/backend-core/auth")
|
|
11
|
-
const { middleware: pro } = require("@budibase/pro")
|
|
12
|
-
const { errors } = require("@budibase/backend-core")
|
|
4
|
+
import { routes } from "./routes"
|
|
5
|
+
import { middleware as pro } from "@budibase/pro"
|
|
6
|
+
import { errors, auth, middleware } from "@budibase/backend-core"
|
|
7
|
+
import { APIError } from "@budibase/types"
|
|
13
8
|
|
|
14
9
|
const PUBLIC_ENDPOINTS = [
|
|
15
10
|
// old deprecated endpoints kept for backwards compat
|
|
@@ -97,9 +92,9 @@ router
|
|
|
97
92
|
})
|
|
98
93
|
)
|
|
99
94
|
.use("/health", ctx => (ctx.status = 200))
|
|
100
|
-
.use(buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
|
101
|
-
.use(buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
|
|
102
|
-
.use(buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS }))
|
|
95
|
+
.use(auth.buildAuthMiddleware(PUBLIC_ENDPOINTS))
|
|
96
|
+
.use(auth.buildTenancyMiddleware(PUBLIC_ENDPOINTS, NO_TENANCY_ENDPOINTS))
|
|
97
|
+
.use(auth.buildCsrfMiddleware({ noCsrfPatterns: NO_CSRF_ENDPOINTS }))
|
|
103
98
|
.use(pro.licensing())
|
|
104
99
|
// for now no public access is allowed to worker (bar health check)
|
|
105
100
|
.use((ctx, next) => {
|
|
@@ -114,21 +109,22 @@ router
|
|
|
114
109
|
}
|
|
115
110
|
return next()
|
|
116
111
|
})
|
|
117
|
-
.use(auditLog)
|
|
112
|
+
.use(middleware.auditLog)
|
|
118
113
|
|
|
119
114
|
// error handling middleware - TODO: This could be moved to backend-core
|
|
120
115
|
router.use(async (ctx, next) => {
|
|
121
116
|
try {
|
|
122
117
|
await next()
|
|
123
|
-
} catch (err) {
|
|
118
|
+
} catch (err: any) {
|
|
124
119
|
ctx.log.error(err)
|
|
125
120
|
ctx.status = err.status || err.statusCode || 500
|
|
126
121
|
const error = errors.getPublicError(err)
|
|
127
|
-
|
|
122
|
+
const body: APIError = {
|
|
128
123
|
message: err.message,
|
|
129
124
|
status: ctx.status,
|
|
130
125
|
error,
|
|
131
126
|
}
|
|
127
|
+
ctx.body = body
|
|
132
128
|
}
|
|
133
129
|
})
|
|
134
130
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
jest.mock("nodemailer")
|
|
2
|
-
|
|
2
|
+
import { TestConfiguration, mocks, API } from "../../../../tests"
|
|
3
3
|
const sendMailMock = mocks.email.mock()
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const TENANT_ID = structures.TENANT_ID
|
|
4
|
+
import { events } from "@budibase/backend-core"
|
|
7
5
|
|
|
8
6
|
describe("/api/global/auth", () => {
|
|
7
|
+
const config = new TestConfiguration()
|
|
8
|
+
const api = new API(config)
|
|
9
9
|
|
|
10
10
|
beforeAll(async () => {
|
|
11
11
|
await config.beforeAll()
|
|
@@ -19,56 +19,32 @@ describe("/api/global/auth", () => {
|
|
|
19
19
|
jest.clearAllMocks()
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
const requestPasswordReset = async () => {
|
|
23
|
-
await config.saveSmtpConfig()
|
|
24
|
-
await config.saveSettingsConfig()
|
|
25
|
-
await config.createUser()
|
|
26
|
-
const res = await request
|
|
27
|
-
.post(`/api/global/auth/${TENANT_ID}/reset`)
|
|
28
|
-
.send({
|
|
29
|
-
email: "test@test.com",
|
|
30
|
-
})
|
|
31
|
-
.expect("Content-Type", /json/)
|
|
32
|
-
.expect(200)
|
|
33
|
-
const emailCall = sendMailMock.mock.calls[0][0]
|
|
34
|
-
const parts = emailCall.html.split(`http://localhost:10000/builder/auth/reset?code=`)
|
|
35
|
-
const code = parts[1].split("\"")[0].split("&")[0]
|
|
36
|
-
return { code, res }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
22
|
it("should logout", async () => {
|
|
40
|
-
await
|
|
41
|
-
.post("/api/global/auth/logout")
|
|
42
|
-
.set(config.defaultHeaders())
|
|
43
|
-
.expect(200)
|
|
23
|
+
await api.auth.logout()
|
|
44
24
|
expect(events.auth.logout).toBeCalledTimes(1)
|
|
45
25
|
})
|
|
46
26
|
|
|
47
27
|
it("should be able to generate password reset email", async () => {
|
|
48
|
-
const { res, code } = await requestPasswordReset()
|
|
28
|
+
const { res, code } = await api.auth.requestPasswordReset(sendMailMock)
|
|
49
29
|
const user = await config.getUser("test@test.com")
|
|
50
30
|
|
|
51
|
-
expect(res.body).toEqual({
|
|
31
|
+
expect(res.body).toEqual({
|
|
32
|
+
message: "Please check your email for a reset link.",
|
|
33
|
+
})
|
|
52
34
|
expect(sendMailMock).toHaveBeenCalled()
|
|
53
|
-
|
|
35
|
+
|
|
54
36
|
expect(code).toBeDefined()
|
|
55
37
|
expect(events.user.passwordResetRequested).toBeCalledTimes(1)
|
|
56
38
|
expect(events.user.passwordResetRequested).toBeCalledWith(user)
|
|
57
39
|
})
|
|
58
40
|
|
|
59
41
|
it("should allow resetting user password with code", async () => {
|
|
60
|
-
const { code } = await requestPasswordReset()
|
|
42
|
+
const { code } = await api.auth.requestPasswordReset(sendMailMock)
|
|
61
43
|
const user = await config.getUser("test@test.com")
|
|
62
|
-
delete user.password
|
|
44
|
+
delete user.password
|
|
45
|
+
|
|
46
|
+
const res = await api.auth.updatePassword(code)
|
|
63
47
|
|
|
64
|
-
const res = await request
|
|
65
|
-
.post(`/api/global/auth/${TENANT_ID}/reset/update`)
|
|
66
|
-
.send({
|
|
67
|
-
password: "newpassword",
|
|
68
|
-
resetCode: code,
|
|
69
|
-
})
|
|
70
|
-
.expect("Content-Type", /json/)
|
|
71
|
-
.expect(200)
|
|
72
48
|
expect(res.body).toEqual({ message: "password reset successfully." })
|
|
73
49
|
expect(events.user.passwordReset).toBeCalledTimes(1)
|
|
74
50
|
expect(events.user.passwordReset).toBeCalledWith(user)
|
|
@@ -79,15 +55,15 @@ describe("/api/global/auth", () => {
|
|
|
79
55
|
|
|
80
56
|
const passportSpy = jest.spyOn(auth.passport, "authenticate")
|
|
81
57
|
let oidcConf
|
|
82
|
-
let chosenConfig
|
|
83
|
-
let configId
|
|
58
|
+
let chosenConfig: any
|
|
59
|
+
let configId: string
|
|
84
60
|
|
|
85
61
|
// mock the oidc strategy implementation and return value
|
|
86
62
|
let strategyFactory = jest.fn()
|
|
87
63
|
let mockStrategyReturn = jest.fn()
|
|
88
64
|
let mockStrategyConfig = jest.fn()
|
|
89
65
|
auth.oidc.fetchStrategyConfig = mockStrategyConfig
|
|
90
|
-
|
|
66
|
+
|
|
91
67
|
strategyFactory.mockReturnValue(mockStrategyReturn)
|
|
92
68
|
auth.oidc.strategyFactory = strategyFactory
|
|
93
69
|
|
|
@@ -99,34 +75,34 @@ describe("/api/global/auth", () => {
|
|
|
99
75
|
})
|
|
100
76
|
|
|
101
77
|
afterEach(() => {
|
|
102
|
-
expect(strategyFactory).toBeCalledWith(
|
|
103
|
-
chosenConfig,
|
|
104
|
-
expect.any(Function)
|
|
105
|
-
)
|
|
78
|
+
expect(strategyFactory).toBeCalledWith(chosenConfig, expect.any(Function))
|
|
106
79
|
})
|
|
107
80
|
|
|
108
81
|
describe("oidc configs", () => {
|
|
109
82
|
it("should load strategy and delegate to passport", async () => {
|
|
110
|
-
await
|
|
83
|
+
await api.configs.getOIDCConfig(configId)
|
|
111
84
|
|
|
112
85
|
expect(passportSpy).toBeCalledWith(mockStrategyReturn, {
|
|
113
|
-
scope: ["profile", "email", "offline_access"]
|
|
86
|
+
scope: ["profile", "email", "offline_access"],
|
|
114
87
|
})
|
|
115
|
-
expect(passportSpy.mock.calls.length).toBe(1)
|
|
88
|
+
expect(passportSpy.mock.calls.length).toBe(1)
|
|
116
89
|
})
|
|
117
90
|
})
|
|
118
91
|
|
|
119
92
|
describe("oidc callback", () => {
|
|
120
93
|
it("should load strategy and delegate to passport", async () => {
|
|
121
|
-
await
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
94
|
+
await api.configs.OIDCCallback(configId)
|
|
95
|
+
|
|
96
|
+
expect(passportSpy).toBeCalledWith(
|
|
97
|
+
mockStrategyReturn,
|
|
98
|
+
{
|
|
99
|
+
successRedirect: "/",
|
|
100
|
+
failureRedirect: "/error",
|
|
101
|
+
},
|
|
102
|
+
expect.anything()
|
|
103
|
+
)
|
|
104
|
+
expect(passportSpy.mock.calls.length).toBe(1)
|
|
128
105
|
})
|
|
129
106
|
})
|
|
130
|
-
|
|
131
107
|
})
|
|
132
108
|
})
|