@budibase/worker 2.3.18-alpha.2 → 2.3.18-alpha.20
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/jest.config.ts +1 -1
- package/package.json +8 -7
- package/scripts/test.sh +12 -0
- package/src/api/controllers/global/auth.ts +38 -47
- package/src/api/controllers/global/configs.ts +150 -142
- package/src/api/controllers/global/users.ts +149 -10
- package/src/api/routes/global/configs.ts +6 -12
- package/src/api/routes/global/tests/auditLogs.spec.ts +111 -0
- package/src/api/routes/global/tests/auth.spec.ts +3 -3
- package/src/api/routes/global/tests/configs.spec.ts +88 -60
- package/src/api/routes/global/tests/realEmail.spec.ts +1 -0
- package/src/api/routes/global/tests/users.spec.ts +5 -1
- package/src/api/routes/global/users.ts +17 -12
- package/src/api/routes/index.ts +3 -0
- package/src/db/index.ts +8 -2
- package/src/environment.ts +1 -11
- package/src/index.ts +18 -2
- package/src/sdk/auth/auth.ts +2 -2
- package/src/sdk/users/tests/users.spec.ts +31 -7
- package/src/sdk/users/users.ts +31 -10
- package/src/tests/api/auditLogs.ts +26 -0
- package/src/tests/api/configs.ts +8 -0
- package/src/tests/api/email.ts +1 -0
- package/src/tests/api/index.ts +3 -0
- package/src/tests/structures/configs.ts +20 -13
- package/src/utilities/email.ts +19 -65
- package/src/utilities/redis.ts +39 -1
- package/src/utilities/templates.ts +4 -9
|
@@ -38,13 +38,6 @@ function buildInviteMultipleValidation() {
|
|
|
38
38
|
))
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function buildInviteLookupValidation() {
|
|
42
|
-
// prettier-ignore
|
|
43
|
-
return auth.joiValidator.params(Joi.object({
|
|
44
|
-
code: Joi.string().required()
|
|
45
|
-
}).unknown(true))
|
|
46
|
-
}
|
|
47
|
-
|
|
48
41
|
const createUserAdminOnly = (ctx: any, next: any) => {
|
|
49
42
|
if (!ctx.request.body._id) {
|
|
50
43
|
return auth.adminOnly(ctx, next)
|
|
@@ -88,22 +81,34 @@ router
|
|
|
88
81
|
.get("/api/global/roles/:appId")
|
|
89
82
|
.post(
|
|
90
83
|
"/api/global/users/invite",
|
|
91
|
-
auth.
|
|
84
|
+
auth.builderOrAdmin,
|
|
92
85
|
buildInviteValidation(),
|
|
93
86
|
controller.invite
|
|
94
87
|
)
|
|
88
|
+
.post(
|
|
89
|
+
"/api/global/users/onboard",
|
|
90
|
+
auth.builderOrAdmin,
|
|
91
|
+
buildInviteMultipleValidation(),
|
|
92
|
+
controller.onboardUsers
|
|
93
|
+
)
|
|
95
94
|
.post(
|
|
96
95
|
"/api/global/users/multi/invite",
|
|
97
|
-
auth.
|
|
96
|
+
auth.builderOrAdmin,
|
|
98
97
|
buildInviteMultipleValidation(),
|
|
99
98
|
controller.inviteMultiple
|
|
100
99
|
)
|
|
101
100
|
|
|
102
101
|
// non-global endpoints
|
|
102
|
+
.get("/api/global/users/invite/:code", controller.checkInvite)
|
|
103
|
+
.post(
|
|
104
|
+
"/api/global/users/invite/update/:code",
|
|
105
|
+
auth.builderOrAdmin,
|
|
106
|
+
controller.updateInvite
|
|
107
|
+
)
|
|
103
108
|
.get(
|
|
104
|
-
"/api/global/users/
|
|
105
|
-
|
|
106
|
-
controller.
|
|
109
|
+
"/api/global/users/invites",
|
|
110
|
+
auth.builderOrAdmin,
|
|
111
|
+
controller.getUserInvites
|
|
107
112
|
)
|
|
108
113
|
.post(
|
|
109
114
|
"/api/global/users/invite/accept",
|
package/src/api/routes/index.ts
CHANGED
|
@@ -18,6 +18,8 @@ import accountRoutes from "./system/accounts"
|
|
|
18
18
|
import restoreRoutes from "./system/restore"
|
|
19
19
|
|
|
20
20
|
let userGroupRoutes = api.groups
|
|
21
|
+
let auditLogRoutes = api.auditLogs
|
|
22
|
+
|
|
21
23
|
export const routes: Router[] = [
|
|
22
24
|
configRoutes,
|
|
23
25
|
userRoutes,
|
|
@@ -32,6 +34,7 @@ export const routes: Router[] = [
|
|
|
32
34
|
selfRoutes,
|
|
33
35
|
licenseRoutes,
|
|
34
36
|
userGroupRoutes,
|
|
37
|
+
auditLogRoutes,
|
|
35
38
|
migrationRoutes,
|
|
36
39
|
accountRoutes,
|
|
37
40
|
restoreRoutes,
|
package/src/db/index.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import * as core from "@budibase/backend-core"
|
|
2
2
|
import env from "../environment"
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
const dbConfig: any = {
|
|
4
|
+
export function init() {
|
|
5
|
+
const dbConfig: any = {
|
|
6
|
+
replication: true,
|
|
7
|
+
find: true,
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
if (env.isTest() && !env.COUCH_DB_URL) {
|
|
7
11
|
dbConfig.inMemory = true
|
|
12
|
+
dbConfig.allDbs = true
|
|
8
13
|
}
|
|
14
|
+
|
|
9
15
|
core.init({ db: dbConfig })
|
|
10
16
|
}
|
package/src/environment.ts
CHANGED
|
@@ -26,8 +26,6 @@ function parseIntSafe(number: any) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const selfHosted = !!parseInt(process.env.SELF_HOSTED || "")
|
|
30
|
-
|
|
31
29
|
const environment = {
|
|
32
30
|
// auth
|
|
33
31
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
|
@@ -51,7 +49,7 @@ const environment = {
|
|
|
51
49
|
CLUSTER_PORT: process.env.CLUSTER_PORT,
|
|
52
50
|
// flags
|
|
53
51
|
NODE_ENV: process.env.NODE_ENV,
|
|
54
|
-
SELF_HOSTED:
|
|
52
|
+
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
|
|
55
53
|
LOG_LEVEL: process.env.LOG_LEVEL,
|
|
56
54
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
|
57
55
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
|
@@ -71,14 +69,6 @@ const environment = {
|
|
|
71
69
|
* Mock the email service in use - links to ethereal hosted emails are logged instead.
|
|
72
70
|
*/
|
|
73
71
|
ENABLE_EMAIL_TEST_MODE: process.env.ENABLE_EMAIL_TEST_MODE,
|
|
74
|
-
/**
|
|
75
|
-
* Enable to allow an admin user to login using a password.
|
|
76
|
-
* This can be useful to prevent lockout when configuring SSO.
|
|
77
|
-
* However, this should be turned OFF by default for security purposes.
|
|
78
|
-
*/
|
|
79
|
-
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
|
80
|
-
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
|
81
|
-
: false,
|
|
82
72
|
_set(key: any, value: any) {
|
|
83
73
|
process.env[key] = value
|
|
84
74
|
// @ts-ignore
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,15 @@ import { Event } from "@sentry/types/dist/event"
|
|
|
13
13
|
import Application from "koa"
|
|
14
14
|
import { bootstrap } from "global-agent"
|
|
15
15
|
import * as db from "./db"
|
|
16
|
-
import {
|
|
16
|
+
import { sdk as proSdk } from "@budibase/pro"
|
|
17
|
+
import {
|
|
18
|
+
auth,
|
|
19
|
+
logging,
|
|
20
|
+
events,
|
|
21
|
+
middleware,
|
|
22
|
+
queue,
|
|
23
|
+
env as coreEnv,
|
|
24
|
+
} from "@budibase/backend-core"
|
|
17
25
|
db.init()
|
|
18
26
|
import Koa from "koa"
|
|
19
27
|
import koaBody from "koa-body"
|
|
@@ -23,9 +31,15 @@ import * as redis from "./utilities/redis"
|
|
|
23
31
|
const Sentry = require("@sentry/node")
|
|
24
32
|
const koaSession = require("koa-session")
|
|
25
33
|
const logger = require("koa-pino-logger")
|
|
34
|
+
const { userAgent } = require("koa-useragent")
|
|
35
|
+
|
|
26
36
|
import destroyable from "server-destroy"
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
// configure events to use the pro audit log write
|
|
39
|
+
// can't integrate directly into backend-core due to cyclic issues
|
|
40
|
+
events.processors.init(proSdk.auditLogs.write)
|
|
41
|
+
|
|
42
|
+
if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) {
|
|
29
43
|
console.warn(
|
|
30
44
|
"Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress"
|
|
31
45
|
)
|
|
@@ -43,6 +57,7 @@ app.use(koaBody({ multipart: true }))
|
|
|
43
57
|
app.use(koaSession(app))
|
|
44
58
|
app.use(middleware.logging)
|
|
45
59
|
app.use(logger(logging.pinoSettings()))
|
|
60
|
+
app.use(userAgent)
|
|
46
61
|
|
|
47
62
|
// authentication
|
|
48
63
|
app.use(auth.passport.initialize())
|
|
@@ -78,6 +93,7 @@ server.on("close", async () => {
|
|
|
78
93
|
console.log("Server Closed")
|
|
79
94
|
await redis.shutdown()
|
|
80
95
|
await events.shutdown()
|
|
96
|
+
await queue.shutdown()
|
|
81
97
|
if (!env.isTest()) {
|
|
82
98
|
process.exit(errCode)
|
|
83
99
|
}
|
package/src/sdk/auth/auth.ts
CHANGED
|
@@ -58,8 +58,8 @@ export const reset = async (email: string) => {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
// exit if user has sso
|
|
61
|
-
if (await userSdk.
|
|
62
|
-
throw new HTTPError("
|
|
61
|
+
if (await userSdk.isPreventPasswordActions(user)) {
|
|
62
|
+
throw new HTTPError("Password reset is disabled for this user", 400)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// send password reset
|
|
@@ -1,26 +1,50 @@
|
|
|
1
1
|
import { structures } from "../../../tests"
|
|
2
|
-
import * as users from "../users"
|
|
3
|
-
import env from "../../../environment"
|
|
4
2
|
import { mocks } from "@budibase/backend-core/tests"
|
|
3
|
+
import { env } from "@budibase/backend-core"
|
|
4
|
+
import * as users from "../users"
|
|
5
5
|
import { CloudAccount } from "@budibase/types"
|
|
6
|
+
import { isPreventPasswordActions } from "../users"
|
|
7
|
+
|
|
8
|
+
jest.mock("@budibase/pro")
|
|
9
|
+
import * as _pro from "@budibase/pro"
|
|
10
|
+
const pro = jest.mocked(_pro, true)
|
|
6
11
|
|
|
7
12
|
describe("users", () => {
|
|
8
|
-
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.clearAllMocks()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe("isPreventPasswordActions", () => {
|
|
18
|
+
it("returns false for non sso user", async () => {
|
|
19
|
+
const user = structures.users.user()
|
|
20
|
+
const result = await users.isPreventPasswordActions(user)
|
|
21
|
+
expect(result).toBe(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
9
24
|
it("returns true for sso account user", async () => {
|
|
10
25
|
const user = structures.users.user()
|
|
11
26
|
mocks.accounts.getAccount.mockReturnValue(
|
|
12
27
|
Promise.resolve(structures.accounts.ssoAccount() as CloudAccount)
|
|
13
28
|
)
|
|
14
|
-
const result = await users.
|
|
29
|
+
const result = await users.isPreventPasswordActions(user)
|
|
15
30
|
expect(result).toBe(true)
|
|
16
31
|
})
|
|
17
32
|
|
|
18
33
|
it("returns true for sso user", async () => {
|
|
19
34
|
const user = structures.users.ssoUser()
|
|
20
|
-
const result = await users.
|
|
35
|
+
const result = await users.isPreventPasswordActions(user)
|
|
21
36
|
expect(result).toBe(true)
|
|
22
37
|
})
|
|
23
38
|
|
|
39
|
+
describe("enforced sso", () => {
|
|
40
|
+
it("returns true for all users when sso is enforced", async () => {
|
|
41
|
+
const user = structures.users.user()
|
|
42
|
+
pro.features.isSSOEnforced.mockReturnValue(Promise.resolve(true))
|
|
43
|
+
const result = await users.isPreventPasswordActions(user)
|
|
44
|
+
expect(result).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
24
48
|
describe("sso maintenance mode", () => {
|
|
25
49
|
beforeEach(() => {
|
|
26
50
|
env._set("ENABLE_SSO_MAINTENANCE_MODE", true)
|
|
@@ -33,7 +57,7 @@ describe("users", () => {
|
|
|
33
57
|
describe("non-admin user", () => {
|
|
34
58
|
it("returns true", async () => {
|
|
35
59
|
const user = structures.users.ssoUser()
|
|
36
|
-
const result = await users.
|
|
60
|
+
const result = await users.isPreventPasswordActions(user)
|
|
37
61
|
expect(result).toBe(true)
|
|
38
62
|
})
|
|
39
63
|
})
|
|
@@ -43,7 +67,7 @@ describe("users", () => {
|
|
|
43
67
|
const user = structures.users.ssoUser({
|
|
44
68
|
user: structures.users.adminUser(),
|
|
45
69
|
})
|
|
46
|
-
const result = await users.
|
|
70
|
+
const result = await users.isPreventPasswordActions(user)
|
|
47
71
|
expect(result).toBe(false)
|
|
48
72
|
})
|
|
49
73
|
})
|
package/src/sdk/users/users.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
users as usersCore,
|
|
15
15
|
utils,
|
|
16
16
|
ViewName,
|
|
17
|
+
env as coreEnv,
|
|
17
18
|
} from "@budibase/backend-core"
|
|
18
19
|
import {
|
|
19
20
|
AccountMetadata,
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
} from "@budibase/types"
|
|
35
36
|
import { sendEmail } from "../../utilities/email"
|
|
36
37
|
import { EmailTemplatePurpose } from "../../constants"
|
|
37
|
-
import
|
|
38
|
+
import * as pro from "@budibase/pro"
|
|
38
39
|
import * as accountSdk from "../accounts"
|
|
39
40
|
|
|
40
41
|
const PAGE_LIMIT = 8
|
|
@@ -56,11 +57,22 @@ export const countUsersByApp = async (appId: string) => {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
export const getUsersByAppAccess = async (appId?: string) => {
|
|
61
|
+
const opts: any = {
|
|
62
|
+
include_docs: true,
|
|
63
|
+
limit: 50,
|
|
64
|
+
}
|
|
65
|
+
let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
|
|
66
|
+
appId,
|
|
67
|
+
opts
|
|
68
|
+
)
|
|
69
|
+
return response
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
export const paginatedUsers = async ({
|
|
60
73
|
page,
|
|
61
74
|
email,
|
|
62
75
|
appId,
|
|
63
|
-
userIds,
|
|
64
76
|
}: SearchUsersRequest = {}) => {
|
|
65
77
|
const db = tenancy.getGlobalDB()
|
|
66
78
|
// get one extra document, to have the next page
|
|
@@ -122,8 +134,8 @@ const buildUser = async (
|
|
|
122
134
|
|
|
123
135
|
let hashedPassword
|
|
124
136
|
if (password) {
|
|
125
|
-
if (await
|
|
126
|
-
throw new HTTPError("
|
|
137
|
+
if (await isPreventPasswordActions(user)) {
|
|
138
|
+
throw new HTTPError("Password change is disabled for this user", 400)
|
|
127
139
|
}
|
|
128
140
|
hashedPassword = opts.hashPassword ? await utils.hash(password) : password
|
|
129
141
|
} else if (dbUser) {
|
|
@@ -188,13 +200,18 @@ const validateUniqueUser = async (email: string, tenantId: string) => {
|
|
|
188
200
|
}
|
|
189
201
|
}
|
|
190
202
|
|
|
191
|
-
export async function
|
|
203
|
+
export async function isPreventPasswordActions(user: User) {
|
|
192
204
|
// when in maintenance mode we allow sso users with the admin role
|
|
193
205
|
// to perform any password action - this prevents lockout
|
|
194
|
-
if (
|
|
206
|
+
if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) {
|
|
195
207
|
return false
|
|
196
208
|
}
|
|
197
209
|
|
|
210
|
+
// SSO is enforced for all users
|
|
211
|
+
if (await pro.features.isSSOEnforced()) {
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
|
|
198
215
|
// Check local sso
|
|
199
216
|
if (isSSOUser(user)) {
|
|
200
217
|
return true
|
|
@@ -228,7 +245,7 @@ export const save = async (
|
|
|
228
245
|
const tenantId = tenancy.getTenantId()
|
|
229
246
|
const db = tenancy.getGlobalDB()
|
|
230
247
|
|
|
231
|
-
let { email, _id, userGroups = [] } = user
|
|
248
|
+
let { email, _id, userGroups = [], roles } = user
|
|
232
249
|
|
|
233
250
|
if (!email && !_id) {
|
|
234
251
|
throw new Error("_id or email is required")
|
|
@@ -270,6 +287,10 @@ export const save = async (
|
|
|
270
287
|
builtUser.roles = dbUser.roles
|
|
271
288
|
}
|
|
272
289
|
|
|
290
|
+
if (!dbUser && roles?.length) {
|
|
291
|
+
builtUser.roles = { ...roles }
|
|
292
|
+
}
|
|
293
|
+
|
|
273
294
|
// make sure we set the _id field for a new user
|
|
274
295
|
// Also if this is a new user, associate groups with them
|
|
275
296
|
let groupPromises = []
|
|
@@ -278,7 +299,7 @@ export const save = async (
|
|
|
278
299
|
|
|
279
300
|
if (userGroups.length > 0) {
|
|
280
301
|
for (let groupId of userGroups) {
|
|
281
|
-
groupPromises.push(
|
|
302
|
+
groupPromises.push(pro.groups.addUsers(groupId, [_id]))
|
|
282
303
|
}
|
|
283
304
|
}
|
|
284
305
|
}
|
|
@@ -456,7 +477,7 @@ export const bulkCreate = async (
|
|
|
456
477
|
const groupPromises = []
|
|
457
478
|
const createdUserIds = saved.map(user => user._id)
|
|
458
479
|
for (let groupId of groups) {
|
|
459
|
-
groupPromises.push(
|
|
480
|
+
groupPromises.push(pro.groups.addUsers(groupId, createdUserIds))
|
|
460
481
|
}
|
|
461
482
|
await Promise.all(groupPromises)
|
|
462
483
|
}
|
|
@@ -631,7 +652,7 @@ export const invite = async (
|
|
|
631
652
|
}
|
|
632
653
|
await sendEmail(user.email, EmailTemplatePurpose.INVITATION, opts)
|
|
633
654
|
response.successful.push({ email: user.email })
|
|
634
|
-
await events.user.invited()
|
|
655
|
+
await events.user.invited(user.email)
|
|
635
656
|
} catch (e) {
|
|
636
657
|
console.error(`Failed to send email invitation email=${user.email}`, e)
|
|
637
658
|
response.unsuccessful.push({
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AuditLogSearchParams, SearchAuditLogsResponse } from "@budibase/types"
|
|
2
|
+
import TestConfiguration from "../TestConfiguration"
|
|
3
|
+
import { TestAPI } from "./base"
|
|
4
|
+
|
|
5
|
+
export class AuditLogAPI extends TestAPI {
|
|
6
|
+
constructor(config: TestConfiguration) {
|
|
7
|
+
super(config)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
search = async (search: AuditLogSearchParams) => {
|
|
11
|
+
const res = await this.request
|
|
12
|
+
.post("/api/global/auditlogs/search")
|
|
13
|
+
.send(search)
|
|
14
|
+
.set(this.config.defaultHeaders())
|
|
15
|
+
.expect("Content-Type", /json/)
|
|
16
|
+
.expect(200)
|
|
17
|
+
return res.body as SearchAuditLogsResponse
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
download = (search: AuditLogSearchParams) => {
|
|
21
|
+
const query = encodeURIComponent(JSON.stringify(search))
|
|
22
|
+
return this.request
|
|
23
|
+
.get(`/api/global/auditlogs/download?query=${query}`)
|
|
24
|
+
.set(this.config.defaultHeaders())
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/tests/api/configs.ts
CHANGED
|
@@ -14,6 +14,14 @@ export class ConfigAPI extends TestAPI {
|
|
|
14
14
|
.expect("Content-Type", /json/)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
getPublicSettings = () => {
|
|
18
|
+
return this.request
|
|
19
|
+
.get(`/api/global/configs/public`)
|
|
20
|
+
.set(this.config.defaultHeaders())
|
|
21
|
+
.expect(200)
|
|
22
|
+
.expect("Content-Type", /json/)
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
saveConfig = (data: any) => {
|
|
18
26
|
return this.request
|
|
19
27
|
.post(`/api/global/configs`)
|
package/src/tests/api/email.ts
CHANGED
package/src/tests/api/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { GroupsAPI } from "./groups"
|
|
|
14
14
|
import { RolesAPI } from "./roles"
|
|
15
15
|
import { TemplatesAPI } from "./templates"
|
|
16
16
|
import { LicenseAPI } from "./license"
|
|
17
|
+
import { AuditLogAPI } from "./auditLogs"
|
|
17
18
|
export default class API {
|
|
18
19
|
accounts: AccountAPI
|
|
19
20
|
auth: AuthAPI
|
|
@@ -30,6 +31,7 @@ export default class API {
|
|
|
30
31
|
roles: RolesAPI
|
|
31
32
|
templates: TemplatesAPI
|
|
32
33
|
license: LicenseAPI
|
|
34
|
+
auditLogs: AuditLogAPI
|
|
33
35
|
|
|
34
36
|
constructor(config: TestConfiguration) {
|
|
35
37
|
this.accounts = new AccountAPI(config)
|
|
@@ -47,5 +49,6 @@ export default class API {
|
|
|
47
49
|
this.roles = new RolesAPI(config)
|
|
48
50
|
this.templates = new TemplatesAPI(config)
|
|
49
51
|
this.license = new LicenseAPI(config)
|
|
52
|
+
this.auditLogs = new AuditLogAPI(config)
|
|
50
53
|
}
|
|
51
54
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { Config } from "../../constants"
|
|
2
1
|
import { utils } from "@budibase/backend-core"
|
|
2
|
+
import {
|
|
3
|
+
SettingsConfig,
|
|
4
|
+
ConfigType,
|
|
5
|
+
SMTPConfig,
|
|
6
|
+
GoogleConfig,
|
|
7
|
+
OIDCConfig,
|
|
8
|
+
} from "@budibase/types"
|
|
3
9
|
|
|
4
|
-
export function oidc(conf?: any) {
|
|
10
|
+
export function oidc(conf?: any): OIDCConfig {
|
|
5
11
|
return {
|
|
6
|
-
type:
|
|
12
|
+
type: ConfigType.OIDC,
|
|
7
13
|
config: {
|
|
8
14
|
configs: [
|
|
9
15
|
{
|
|
@@ -21,9 +27,9 @@ export function oidc(conf?: any) {
|
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
export function google(conf?: any) {
|
|
30
|
+
export function google(conf?: any): GoogleConfig {
|
|
25
31
|
return {
|
|
26
|
-
type:
|
|
32
|
+
type: ConfigType.GOOGLE,
|
|
27
33
|
config: {
|
|
28
34
|
clientID: "clientId",
|
|
29
35
|
clientSecret: "clientSecret",
|
|
@@ -33,9 +39,9 @@ export function google(conf?: any) {
|
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
export function smtp(conf?: any) {
|
|
42
|
+
export function smtp(conf?: any): SMTPConfig {
|
|
37
43
|
return {
|
|
38
|
-
type:
|
|
44
|
+
type: ConfigType.SMTP,
|
|
39
45
|
config: {
|
|
40
46
|
port: 12345,
|
|
41
47
|
host: "smtptesthost.com",
|
|
@@ -47,25 +53,26 @@ export function smtp(conf?: any) {
|
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
export function smtpEthereal() {
|
|
56
|
+
export function smtpEthereal(): SMTPConfig {
|
|
51
57
|
return {
|
|
52
|
-
type:
|
|
58
|
+
type: ConfigType.SMTP,
|
|
53
59
|
config: {
|
|
54
60
|
port: 587,
|
|
55
61
|
host: "smtp.ethereal.email",
|
|
62
|
+
from: "testfrom@test.com",
|
|
56
63
|
secure: false,
|
|
57
64
|
auth: {
|
|
58
|
-
user: "
|
|
59
|
-
pass: "
|
|
65
|
+
user: "wyatt.zulauf29@ethereal.email",
|
|
66
|
+
pass: "tEwDtHBWWxusVWAPfa",
|
|
60
67
|
},
|
|
61
68
|
connectionTimeout: 1000, // must be less than the jest default of 5000
|
|
62
69
|
},
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
export function settings(conf?: any) {
|
|
73
|
+
export function settings(conf?: any): SettingsConfig {
|
|
67
74
|
return {
|
|
68
|
-
type:
|
|
75
|
+
type: ConfigType.SETTINGS,
|
|
69
76
|
config: {
|
|
70
77
|
platformUrl: "http://localhost:10000",
|
|
71
78
|
logoUrl: "",
|
package/src/utilities/email.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import env from "../environment"
|
|
2
|
-
import { EmailTemplatePurpose, TemplateType
|
|
2
|
+
import { EmailTemplatePurpose, TemplateType } from "../constants"
|
|
3
3
|
import { getTemplateByPurpose } from "../constants/templates"
|
|
4
4
|
import { getSettingsTemplateContext } from "./templates"
|
|
5
5
|
import { processString } from "@budibase/string-templates"
|
|
6
6
|
import { getResetPasswordCode, getInviteCode } from "./redis"
|
|
7
|
-
import { User,
|
|
8
|
-
import {
|
|
7
|
+
import { User, SMTPInnerConfig } from "@budibase/types"
|
|
8
|
+
import { configs } from "@budibase/backend-core"
|
|
9
9
|
const nodemailer = require("nodemailer")
|
|
10
10
|
|
|
11
11
|
type SendEmailOpts = {
|
|
@@ -36,24 +36,24 @@ const FULL_EMAIL_PURPOSES = [
|
|
|
36
36
|
EmailTemplatePurpose.CUSTOM,
|
|
37
37
|
]
|
|
38
38
|
|
|
39
|
-
function createSMTPTransport(config
|
|
39
|
+
function createSMTPTransport(config?: SMTPInnerConfig) {
|
|
40
40
|
let options: any
|
|
41
|
-
let secure = config
|
|
41
|
+
let secure = config?.secure
|
|
42
42
|
// default it if not specified
|
|
43
43
|
if (secure == null) {
|
|
44
|
-
secure = config
|
|
44
|
+
secure = config?.port === 465
|
|
45
45
|
}
|
|
46
46
|
if (!TEST_MODE) {
|
|
47
47
|
options = {
|
|
48
|
-
port: config
|
|
49
|
-
host: config
|
|
48
|
+
port: config?.port,
|
|
49
|
+
host: config?.host,
|
|
50
50
|
secure: secure,
|
|
51
|
-
auth: config
|
|
51
|
+
auth: config?.auth,
|
|
52
52
|
}
|
|
53
53
|
options.tls = {
|
|
54
54
|
rejectUnauthorized: false,
|
|
55
55
|
}
|
|
56
|
-
if (config
|
|
56
|
+
if (config?.connectionTimeout) {
|
|
57
57
|
options.connectionTimeout = config.connectionTimeout
|
|
58
58
|
}
|
|
59
59
|
} else {
|
|
@@ -134,57 +134,16 @@ async function buildEmail(
|
|
|
134
134
|
})
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/**
|
|
138
|
-
* Utility function for finding most valid SMTP configuration.
|
|
139
|
-
* @param {object} db The CouchDB database which is to be looked up within.
|
|
140
|
-
* @param {string|null} workspaceId If using finer grain control of configs a workspace can be used.
|
|
141
|
-
* @param {boolean|null} automation Whether or not the configuration is being fetched for an email automation.
|
|
142
|
-
* @return {Promise<object|null>} returns the SMTP configuration if it exists
|
|
143
|
-
*/
|
|
144
|
-
async function getSmtpConfiguration(
|
|
145
|
-
db: Database,
|
|
146
|
-
workspaceId?: string,
|
|
147
|
-
automation?: boolean
|
|
148
|
-
) {
|
|
149
|
-
const params: any = {
|
|
150
|
-
type: Config.SMTP,
|
|
151
|
-
}
|
|
152
|
-
if (workspaceId) {
|
|
153
|
-
params.workspace = workspaceId
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const customConfig = await dbCore.getScopedConfig(db, params)
|
|
157
|
-
|
|
158
|
-
if (customConfig) {
|
|
159
|
-
return customConfig
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Use an SMTP fallback configuration from env variables
|
|
163
|
-
if (!automation && env.SMTP_FALLBACK_ENABLED) {
|
|
164
|
-
return {
|
|
165
|
-
port: env.SMTP_PORT,
|
|
166
|
-
host: env.SMTP_HOST,
|
|
167
|
-
secure: false,
|
|
168
|
-
from: env.SMTP_FROM_ADDRESS,
|
|
169
|
-
auth: {
|
|
170
|
-
user: env.SMTP_USER,
|
|
171
|
-
pass: env.SMTP_PASSWORD,
|
|
172
|
-
},
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
137
|
/**
|
|
178
138
|
* Checks if a SMTP config exists based on passed in parameters.
|
|
179
139
|
* @return {Promise<boolean>} returns true if there is a configuration that can be used.
|
|
180
140
|
*/
|
|
181
|
-
export async function isEmailConfigured(
|
|
141
|
+
export async function isEmailConfigured() {
|
|
182
142
|
// when "testing" or smtp fallback is enabled simply return true
|
|
183
143
|
if (TEST_MODE || env.SMTP_FALLBACK_ENABLED) {
|
|
184
144
|
return true
|
|
185
145
|
}
|
|
186
|
-
const
|
|
187
|
-
const config = await getSmtpConfiguration(db, workspaceId)
|
|
146
|
+
const config = await configs.getSMTPConfig()
|
|
188
147
|
return config != null
|
|
189
148
|
}
|
|
190
149
|
|
|
@@ -202,22 +161,17 @@ export async function sendEmail(
|
|
|
202
161
|
purpose: EmailTemplatePurpose,
|
|
203
162
|
opts: SendEmailOpts
|
|
204
163
|
) {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
(await getSmtpConfiguration(db, opts?.workspaceId, opts?.automation)) || {}
|
|
208
|
-
if (Object.keys(config).length === 0 && !TEST_MODE) {
|
|
164
|
+
const config = await configs.getSMTPConfig(opts?.automation)
|
|
165
|
+
if (!config && !TEST_MODE) {
|
|
209
166
|
throw "Unable to find SMTP configuration."
|
|
210
167
|
}
|
|
211
168
|
const transport = createSMTPTransport(config)
|
|
212
169
|
// if there is a link code needed this will retrieve it
|
|
213
170
|
const code = await getLinkCode(purpose, email, opts.user, opts?.info)
|
|
214
|
-
let context
|
|
215
|
-
if (code) {
|
|
216
|
-
context = await getSettingsTemplateContext(purpose, code)
|
|
217
|
-
}
|
|
171
|
+
let context = await getSettingsTemplateContext(purpose, code)
|
|
218
172
|
|
|
219
173
|
let message: any = {
|
|
220
|
-
from: opts?.from || config
|
|
174
|
+
from: opts?.from || config?.from,
|
|
221
175
|
html: await buildEmail(purpose, email, context, {
|
|
222
176
|
user: opts?.user,
|
|
223
177
|
contents: opts?.contents,
|
|
@@ -231,9 +185,9 @@ export async function sendEmail(
|
|
|
231
185
|
bcc: opts?.bcc,
|
|
232
186
|
}
|
|
233
187
|
|
|
234
|
-
if (opts?.subject || config
|
|
188
|
+
if (opts?.subject || config?.subject) {
|
|
235
189
|
message.subject = await processString(
|
|
236
|
-
opts?.subject || config
|
|
190
|
+
(opts?.subject || config?.subject) as string,
|
|
237
191
|
context
|
|
238
192
|
)
|
|
239
193
|
}
|
|
@@ -249,7 +203,7 @@ export async function sendEmail(
|
|
|
249
203
|
* @param {object} config an SMTP configuration - this is based on the nodemailer API.
|
|
250
204
|
* @return {Promise<boolean>} returns true if the configuration is valid.
|
|
251
205
|
*/
|
|
252
|
-
export async function verifyConfig(config:
|
|
206
|
+
export async function verifyConfig(config: SMTPInnerConfig) {
|
|
253
207
|
const transport = createSMTPTransport(config)
|
|
254
208
|
await transport.verify()
|
|
255
209
|
}
|