@budibase/worker 2.3.18-alpha.12 → 2.3.18-alpha.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/worker",
3
3
  "email": "hi@budibase.com",
4
- "version": "2.3.18-alpha.12",
4
+ "version": "2.3.18-alpha.13",
5
5
  "description": "Budibase background service",
6
6
  "main": "src/index.ts",
7
7
  "repository": {
@@ -36,10 +36,10 @@
36
36
  "author": "Budibase",
37
37
  "license": "GPL-3.0",
38
38
  "dependencies": {
39
- "@budibase/backend-core": "2.3.18-alpha.12",
40
- "@budibase/pro": "2.3.18-alpha.10",
41
- "@budibase/string-templates": "2.3.18-alpha.12",
42
- "@budibase/types": "2.3.18-alpha.12",
39
+ "@budibase/backend-core": "2.3.18-alpha.13",
40
+ "@budibase/pro": "2.3.18-alpha.12",
41
+ "@budibase/string-templates": "2.3.18-alpha.13",
42
+ "@budibase/types": "2.3.18-alpha.13",
43
43
  "@koa/router": "8.0.8",
44
44
  "@sentry/node": "6.17.7",
45
45
  "@techpass/passport-openidconnect": "0.3.2",
@@ -100,5 +100,5 @@
100
100
  "typescript": "4.7.3",
101
101
  "update-dotenv": "1.1.1"
102
102
  },
103
- "gitHead": "f77c214b0a0a5fcee6614db7f604ad278acaf246"
103
+ "gitHead": "95ce3bb56c8db6b69b3b9081c0eeb0fff5180518"
104
104
  }
@@ -2,10 +2,9 @@ import {
2
2
  auth as authCore,
3
3
  constants,
4
4
  context,
5
- db as dbCore,
6
5
  events,
7
- tenancy,
8
6
  utils as utilsCore,
7
+ configs,
9
8
  } from "@budibase/backend-core"
10
9
  import {
11
10
  ConfigType,
@@ -15,6 +14,7 @@ import {
15
14
  SSOUser,
16
15
  PasswordResetRequest,
17
16
  PasswordResetUpdateRequest,
17
+ GoogleInnerConfig,
18
18
  } from "@budibase/types"
19
19
  import env from "../../../environment"
20
20
 
@@ -61,8 +61,8 @@ export const login = async (ctx: Ctx<LoginRequest>, next: any) => {
61
61
  const email = ctx.request.body.username
62
62
 
63
63
  const user = await userSdk.getUserByEmail(email)
64
- if (user && (await userSdk.isPreventSSOPasswords(user))) {
65
- ctx.throw(400, "SSO user cannot login using password")
64
+ if (user && (await userSdk.isPreventPasswordActions(user))) {
65
+ ctx.throw(400, "Password login is disabled for this user")
66
66
  }
67
67
 
68
68
  return passport.authenticate(
@@ -163,8 +163,8 @@ export const datasourceAuth = async (ctx: any, next: any) => {
163
163
 
164
164
  // GOOGLE SSO
165
165
 
166
- export async function googleCallbackUrl(config?: { callbackURL?: string }) {
167
- return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.GOOGLE)
166
+ export async function googleCallbackUrl(config?: GoogleInnerConfig) {
167
+ return ssoCallbackUrl(ConfigType.GOOGLE, config)
168
168
  }
169
169
 
170
170
  /**
@@ -172,12 +172,10 @@ export async function googleCallbackUrl(config?: { callbackURL?: string }) {
172
172
  * On a successful login, you will be redirected to the googleAuth callback route.
173
173
  */
174
174
  export const googlePreAuth = async (ctx: any, next: any) => {
175
- const db = tenancy.getGlobalDB()
176
-
177
- const config = await dbCore.getScopedConfig(db, {
178
- type: ConfigType.GOOGLE,
179
- workspace: ctx.query.workspace,
180
- })
175
+ const config = await configs.getGoogleConfig()
176
+ if (!config) {
177
+ return ctx.throw(400, "Google config not found")
178
+ }
181
179
  let callbackUrl = await googleCallbackUrl(config)
182
180
  const strategy = await google.strategyFactory(
183
181
  config,
@@ -193,12 +191,10 @@ export const googlePreAuth = async (ctx: any, next: any) => {
193
191
  }
194
192
 
195
193
  export const googleCallback = async (ctx: any, next: any) => {
196
- const db = tenancy.getGlobalDB()
197
-
198
- const config = await dbCore.getScopedConfig(db, {
199
- type: ConfigType.GOOGLE,
200
- workspace: ctx.query.workspace,
201
- })
194
+ const config = await configs.getGoogleConfig()
195
+ if (!config) {
196
+ return ctx.throw(400, "Google config not found")
197
+ }
202
198
  const callbackUrl = await googleCallbackUrl(config)
203
199
  const strategy = await google.strategyFactory(
204
200
  config,
@@ -221,25 +217,20 @@ export const googleCallback = async (ctx: any, next: any) => {
221
217
 
222
218
  // OIDC SSO
223
219
 
224
- export async function oidcCallbackUrl(config?: { callbackURL?: string }) {
225
- return ssoCallbackUrl(tenancy.getGlobalDB(), config, ConfigType.OIDC)
220
+ export async function oidcCallbackUrl() {
221
+ return ssoCallbackUrl(ConfigType.OIDC)
226
222
  }
227
223
 
228
224
  export const oidcStrategyFactory = async (ctx: any, configId: any) => {
229
- const db = tenancy.getGlobalDB()
230
- const config = await dbCore.getScopedConfig(db, {
231
- type: ConfigType.OIDC,
232
- group: ctx.query.group,
233
- })
225
+ const config = await configs.getOIDCConfig()
226
+ if (!config) {
227
+ return ctx.throw(400, "OIDC config not found")
228
+ }
234
229
 
235
- const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
236
- let callbackUrl = await oidcCallbackUrl(chosenConfig)
230
+ let callbackUrl = await oidcCallbackUrl()
237
231
 
238
232
  //Remote Config
239
- const enrichedConfig = await oidc.fetchStrategyConfig(
240
- chosenConfig,
241
- callbackUrl
242
- )
233
+ const enrichedConfig = await oidc.fetchStrategyConfig(config, callbackUrl)
243
234
  return oidc.strategyFactory(enrichedConfig, userSdk.save)
244
235
  }
245
236
 
@@ -247,23 +238,23 @@ export const oidcStrategyFactory = async (ctx: any, configId: any) => {
247
238
  * The initial call that OIDC authentication makes to take you to the configured OIDC login screen.
248
239
  * On a successful login, you will be redirected to the oidcAuth callback route.
249
240
  */
250
- export const oidcPreAuth = async (ctx: any, next: any) => {
241
+ export const oidcPreAuth = async (ctx: Ctx, next: any) => {
251
242
  const { configId } = ctx.params
243
+ if (!configId) {
244
+ ctx.throw(400, "OIDC config id is required")
245
+ }
252
246
  const strategy = await oidcStrategyFactory(ctx, configId)
253
247
 
254
248
  setCookie(ctx, configId, Cookie.OIDC_CONFIG)
255
249
 
256
- const db = tenancy.getGlobalDB()
257
- const config = await dbCore.getScopedConfig(db, {
258
- type: ConfigType.OIDC,
259
- group: ctx.query.group,
260
- })
261
-
262
- const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
250
+ const config = await configs.getOIDCConfigById(configId)
251
+ if (!config) {
252
+ return ctx.throw(400, "OIDC config not found")
253
+ }
263
254
 
264
255
  let authScopes =
265
- chosenConfig.scopes?.length > 0
266
- ? chosenConfig.scopes
256
+ config.scopes?.length > 0
257
+ ? config.scopes
267
258
  : ["profile", "email", "offline_access"]
268
259
 
269
260
  return passport.authenticate(strategy, {
@@ -2,38 +2,32 @@ import * as email from "../../../utilities/email"
2
2
  import env from "../../../environment"
3
3
  import { googleCallbackUrl, oidcCallbackUrl } from "./auth"
4
4
  import {
5
- events,
6
5
  cache,
7
- objectStore,
8
- tenancy,
6
+ configs,
9
7
  db as dbCore,
10
8
  env as coreEnv,
9
+ events,
10
+ objectStore,
11
+ tenancy,
11
12
  } from "@budibase/backend-core"
12
13
  import { checkAnyUserExists } from "../../../utilities/users"
13
14
  import {
14
- Database,
15
- Config as ConfigDoc,
15
+ Config,
16
16
  ConfigType,
17
- SSOType,
18
- GoogleConfig,
19
- OIDCConfig,
20
- SettingsConfig,
17
+ Ctx,
18
+ GetPublicOIDCConfigResponse,
19
+ GetPublicSettingsResponse,
21
20
  isGoogleConfig,
22
21
  isOIDCConfig,
23
22
  isSettingsConfig,
24
23
  isSMTPConfig,
25
- Ctx,
26
24
  UserCtx,
27
25
  } from "@budibase/types"
26
+ import * as pro from "@budibase/pro"
28
27
 
29
- const getEventFns = async (db: Database, config: ConfigDoc) => {
28
+ const getEventFns = async (config: Config, existing?: Config) => {
30
29
  const fns = []
31
30
 
32
- let existing
33
- if (config._id) {
34
- existing = await db.get(config._id)
35
- }
36
-
37
31
  if (!existing) {
38
32
  if (isSMTPConfig(config)) {
39
33
  fns.push(events.email.SMTPCreated)
@@ -125,21 +119,21 @@ const getEventFns = async (db: Database, config: ConfigDoc) => {
125
119
  return fns
126
120
  }
127
121
 
128
- export async function save(ctx: UserCtx) {
129
- const db = tenancy.getGlobalDB()
130
- const { type, workspace, user, config } = ctx.request.body
131
- let eventFns = await getEventFns(db, ctx.request.body)
132
- // Config does not exist yet
133
- if (!ctx.request.body._id) {
134
- ctx.request.body._id = dbCore.generateConfigID({
135
- type,
136
- workspace,
137
- user,
138
- })
122
+ export async function save(ctx: UserCtx<Config>) {
123
+ const body = ctx.request.body
124
+ const type = body.type
125
+ const config = body.config
126
+
127
+ const existingConfig = await configs.getConfig(type)
128
+ let eventFns = await getEventFns(ctx.request.body, existingConfig)
129
+
130
+ if (existingConfig) {
131
+ body._rev = existingConfig._rev
139
132
  }
133
+
140
134
  try {
141
135
  // verify the configuration
142
- switch (type) {
136
+ switch (config.type) {
143
137
  case ConfigType.SMTP:
144
138
  await email.verifyConfig(config)
145
139
  break
@@ -149,7 +143,8 @@ export async function save(ctx: UserCtx) {
149
143
  }
150
144
 
151
145
  try {
152
- const response = await db.put(ctx.request.body)
146
+ body._id = configs.generateConfigID(type)
147
+ const response = await configs.save(body)
153
148
  await cache.bustCache(cache.CacheKey.CHECKLIST)
154
149
  await cache.bustCache(cache.CacheKey.ANALYTICS_ENABLED)
155
150
 
@@ -167,44 +162,11 @@ export async function save(ctx: UserCtx) {
167
162
  }
168
163
  }
169
164
 
170
- export async function fetch(ctx: UserCtx) {
171
- const db = tenancy.getGlobalDB()
172
- const response = await db.allDocs(
173
- dbCore.getConfigParams(
174
- { type: ctx.params.type },
175
- {
176
- include_docs: true,
177
- }
178
- )
179
- )
180
- ctx.body = response.rows.map(row => row.doc)
181
- }
182
-
183
- /**
184
- * Gets the most granular config for a particular configuration type.
185
- * The hierarchy is type -> workspace -> user.
186
- */
187
165
  export async function find(ctx: UserCtx) {
188
- const db = tenancy.getGlobalDB()
189
-
190
- const { userId, workspaceId } = ctx.query
191
- if (workspaceId && userId) {
192
- const workspace = await db.get(workspaceId as string)
193
- const userInWorkspace = workspace.users.some(
194
- (workspaceUser: any) => workspaceUser === userId
195
- )
196
- if (!ctx.user!.admin && !userInWorkspace) {
197
- ctx.throw(400, `User is not in specified workspace: ${workspace}.`)
198
- }
199
- }
200
-
201
166
  try {
202
167
  // Find the config with the most granular scope based on context
203
- const scopedConfig = await dbCore.getScopedFullConfig(db, {
204
- type: ctx.params.type,
205
- user: userId,
206
- workspace: workspaceId,
207
- })
168
+ const type = ctx.params.type
169
+ const scopedConfig = await configs.getConfig(type)
208
170
 
209
171
  if (scopedConfig) {
210
172
  ctx.body = scopedConfig
@@ -217,85 +179,70 @@ export async function find(ctx: UserCtx) {
217
179
  }
218
180
  }
219
181
 
220
- export async function publicOidc(ctx: Ctx) {
221
- const db = tenancy.getGlobalDB()
182
+ export async function publicOidc(ctx: Ctx<void, GetPublicOIDCConfigResponse>) {
222
183
  try {
223
184
  // Find the config with the most granular scope based on context
224
- const oidcConfig: OIDCConfig = await dbCore.getScopedFullConfig(db, {
225
- type: ConfigType.OIDC,
226
- })
185
+ const config = await configs.getOIDCConfig()
227
186
 
228
- if (!oidcConfig) {
229
- ctx.body = {}
187
+ if (!config) {
188
+ ctx.body = []
230
189
  } else {
231
- ctx.body = oidcConfig.config.configs.map(config => ({
232
- logo: config.logo,
233
- name: config.name,
234
- uuid: config.uuid,
235
- }))
190
+ ctx.body = [
191
+ {
192
+ logo: config.logo,
193
+ name: config.name,
194
+ uuid: config.uuid,
195
+ },
196
+ ]
236
197
  }
237
198
  } catch (err: any) {
238
199
  ctx.throw(err.status, err)
239
200
  }
240
201
  }
241
202
 
242
- export async function publicSettings(ctx: Ctx) {
243
- const db = tenancy.getGlobalDB()
244
-
203
+ export async function publicSettings(
204
+ ctx: Ctx<void, GetPublicSettingsResponse>
205
+ ) {
245
206
  try {
246
- // Find the config with the most granular scope based on context
247
- const publicConfig = await dbCore.getScopedFullConfig(db, {
248
- type: ConfigType.SETTINGS,
249
- })
250
-
251
- const googleConfig = await dbCore.getScopedFullConfig(db, {
252
- type: ConfigType.GOOGLE,
253
- })
254
-
255
- const oidcConfig = await dbCore.getScopedFullConfig(db, {
256
- type: ConfigType.OIDC,
257
- })
258
-
259
- let config
260
- if (!publicConfig) {
261
- config = {
262
- config: {},
263
- }
264
- } else {
265
- config = publicConfig
266
- }
267
-
268
- // enrich the logo url
269
- // empty url means deleted
270
- if (config.config.logoUrl && config.config.logoUrl !== "") {
271
- config.config.logoUrl = objectStore.getGlobalFileUrl(
207
+ // settings
208
+ const configDoc = await configs.getSettingsConfigDoc()
209
+ const config = configDoc.config
210
+ // enrich the logo url - empty url means deleted
211
+ if (config.logoUrl && config.logoUrl !== "") {
212
+ config.logoUrl = objectStore.getGlobalFileUrl(
272
213
  "settings",
273
214
  "logoUrl",
274
- config.config.logoUrlEtag
215
+ config.logoUrlEtag
275
216
  )
276
217
  }
277
218
 
278
- // google button flag
279
- if (googleConfig && googleConfig.config) {
280
- // activated by default for configs pre-activated flag
281
- config.config.google =
282
- googleConfig.config.activated == null || googleConfig.config.activated
283
- } else {
284
- config.config.google = false
285
- }
219
+ // google
220
+ const googleConfig = await configs.getGoogleConfig()
221
+ const preActivated = googleConfig?.activated == null
222
+ const google = preActivated || !!googleConfig?.activated
223
+ const _googleCallbackUrl = await googleCallbackUrl(googleConfig)
286
224
 
287
- // callback urls
288
- config.config.oidcCallbackUrl = await oidcCallbackUrl()
289
- config.config.googleCallbackUrl = await googleCallbackUrl()
225
+ // oidc
226
+ const oidcConfig = await configs.getOIDCConfig()
227
+ const oidc = oidcConfig?.activated || false
228
+ const _oidcCallbackUrl = await oidcCallbackUrl()
290
229
 
291
- // oidc button flag
292
- if (oidcConfig && oidcConfig.config) {
293
- config.config.oidc = oidcConfig.config.configs[0].activated
294
- } else {
295
- config.config.oidc = false
296
- }
230
+ // sso enforced
231
+ const isSSOEnforced = await pro.features.isSSOEnforced({ config })
297
232
 
298
- ctx.body = config
233
+ ctx.body = {
234
+ type: ConfigType.SETTINGS,
235
+ _id: configDoc._id,
236
+ _rev: configDoc._rev,
237
+ config: {
238
+ ...config,
239
+ google,
240
+ oidc,
241
+ isSSOEnforced,
242
+ oidcCallbackUrl: _oidcCallbackUrl,
243
+ googleCallbackUrl: _googleCallbackUrl,
244
+ },
245
+ }
299
246
  } catch (err: any) {
300
247
  ctx.throw(err.status, err)
301
248
  }
@@ -319,12 +266,11 @@ export async function upload(ctx: UserCtx) {
319
266
  })
320
267
 
321
268
  // add to configuration structure
322
- // TODO: right now this only does a global level
323
- const db = tenancy.getGlobalDB()
324
- let cfgStructure = await dbCore.getScopedFullConfig(db, { type })
325
- if (!cfgStructure) {
326
- cfgStructure = {
327
- _id: dbCore.generateConfigID({ type }),
269
+ let config = await configs.getConfig(type)
270
+ if (!config) {
271
+ config = {
272
+ _id: configs.generateConfigID(type),
273
+ type,
328
274
  config: {},
329
275
  }
330
276
  }
@@ -332,14 +278,14 @@ export async function upload(ctx: UserCtx) {
332
278
  // save the Etag for cache bursting
333
279
  const etag = result.ETag
334
280
  if (etag) {
335
- cfgStructure.config[`${name}Etag`] = etag.replace(/"/g, "")
281
+ config.config[`${name}Etag`] = etag.replace(/"/g, "")
336
282
  }
337
283
 
338
284
  // save the file key
339
- cfgStructure.config[`${name}`] = key
285
+ config.config[`${name}`] = key
340
286
 
341
287
  // write back to db
342
- await db.put(cfgStructure)
288
+ await configs.save(config)
343
289
 
344
290
  ctx.body = {
345
291
  message: "File has been uploaded and url stored to config.",
@@ -360,7 +306,6 @@ export async function destroy(ctx: UserCtx) {
360
306
  }
361
307
 
362
308
  export async function configChecklist(ctx: Ctx) {
363
- const db = tenancy.getGlobalDB()
364
309
  const tenantId = tenancy.getTenantId()
365
310
 
366
311
  try {
@@ -375,19 +320,13 @@ export async function configChecklist(ctx: Ctx) {
375
320
  }
376
321
 
377
322
  // They have set up SMTP
378
- const smtpConfig = await dbCore.getScopedFullConfig(db, {
379
- type: ConfigType.SMTP,
380
- })
323
+ const smtpConfig = await configs.getSMTPConfig()
381
324
 
382
325
  // They have set up Google Auth
383
- const googleConfig = await dbCore.getScopedFullConfig(db, {
384
- type: ConfigType.GOOGLE,
385
- })
326
+ const googleConfig = await configs.getGoogleConfig()
386
327
 
387
328
  // They have set up OIDC
388
- const oidcConfig = await dbCore.getScopedFullConfig(db, {
389
- type: ConfigType.OIDC,
390
- })
329
+ const oidcConfig = await configs.getOIDCConfig()
391
330
 
392
331
  // They have set up a global user
393
332
  const userExists = await checkAnyUserExists()
@@ -104,13 +104,7 @@ router
104
104
  controller.save
105
105
  )
106
106
  .delete("/api/global/configs/:id/:rev", auth.adminOnly, controller.destroy)
107
- .get("/api/global/configs", controller.fetch)
108
107
  .get("/api/global/configs/checklist", controller.configChecklist)
109
- .get(
110
- "/api/global/configs/all/:type",
111
- buildConfigGetValidation(),
112
- controller.fetch
113
- )
114
108
  .get("/api/global/configs/public", controller.publicSettings)
115
109
  .get("/api/global/configs/public/oidc", controller.publicOidc)
116
110
  .get("/api/global/configs/:type", buildConfigGetValidation(), controller.find)
@@ -110,7 +110,7 @@ describe("/api/global/auth", () => {
110
110
  )
111
111
 
112
112
  expect(response.body).toEqual({
113
- message: "SSO user cannot login using password",
113
+ message: "Password login is disabled for this user",
114
114
  status: 400,
115
115
  })
116
116
  }
@@ -175,7 +175,7 @@ describe("/api/global/auth", () => {
175
175
  )
176
176
 
177
177
  expect(res.body).toEqual({
178
- message: "SSO user cannot reset password",
178
+ message: "Password reset is disabled for this user",
179
179
  status: 400,
180
180
  error: {
181
181
  code: "http",
@@ -2,7 +2,8 @@
2
2
  jest.mock("nodemailer")
3
3
  import { TestConfiguration, structures, mocks } from "../../../../tests"
4
4
  mocks.email.mock()
5
- import { Config, events } from "@budibase/backend-core"
5
+ import { events } from "@budibase/backend-core"
6
+ import { GetPublicSettingsResponse, Config, ConfigType } from "@budibase/types"
6
7
 
7
8
  describe("configs", () => {
8
9
  const config = new TestConfiguration()
@@ -19,22 +20,29 @@ describe("configs", () => {
19
20
  await config.afterAll()
20
21
  })
21
22
 
22
- describe("post /api/global/configs", () => {
23
- const saveConfig = async (conf: any, _id?: string, _rev?: string) => {
24
- const data = {
25
- ...conf,
26
- _id,
27
- _rev,
28
- }
29
-
30
- const res = await config.api.configs.saveConfig(data)
31
-
32
- return {
33
- ...data,
34
- ...res.body,
35
- }
23
+ const saveConfig = async (conf: Config, _id?: string, _rev?: string) => {
24
+ const data = {
25
+ ...conf,
26
+ _id,
27
+ _rev,
36
28
  }
37
-
29
+ const res = await config.api.configs.saveConfig(data)
30
+ return {
31
+ ...data,
32
+ ...res.body,
33
+ }
34
+ }
35
+
36
+ const saveSettingsConfig = async (
37
+ conf?: any,
38
+ _id?: string,
39
+ _rev?: string
40
+ ) => {
41
+ const settingsConfig = structures.configs.settings(conf)
42
+ return saveConfig(settingsConfig, _id, _rev)
43
+ }
44
+
45
+ describe("POST /api/global/configs", () => {
38
46
  describe("google", () => {
39
47
  const saveGoogleConfig = async (
40
48
  conf?: any,
@@ -49,20 +57,20 @@ describe("configs", () => {
49
57
  it("should create activated google config", async () => {
50
58
  await saveGoogleConfig()
51
59
  expect(events.auth.SSOCreated).toBeCalledTimes(1)
52
- expect(events.auth.SSOCreated).toBeCalledWith(Config.GOOGLE)
60
+ expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.GOOGLE)
53
61
  expect(events.auth.SSODeactivated).not.toBeCalled()
54
62
  expect(events.auth.SSOActivated).toBeCalledTimes(1)
55
- expect(events.auth.SSOActivated).toBeCalledWith(Config.GOOGLE)
56
- await config.deleteConfig(Config.GOOGLE)
63
+ expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.GOOGLE)
64
+ await config.deleteConfig(ConfigType.GOOGLE)
57
65
  })
58
66
 
59
67
  it("should create deactivated google config", async () => {
60
68
  await saveGoogleConfig({ activated: false })
61
69
  expect(events.auth.SSOCreated).toBeCalledTimes(1)
62
- expect(events.auth.SSOCreated).toBeCalledWith(Config.GOOGLE)
70
+ expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.GOOGLE)
63
71
  expect(events.auth.SSOActivated).not.toBeCalled()
64
72
  expect(events.auth.SSODeactivated).not.toBeCalled()
65
- await config.deleteConfig(Config.GOOGLE)
73
+ await config.deleteConfig(ConfigType.GOOGLE)
66
74
  })
67
75
  })
68
76
 
@@ -76,11 +84,11 @@ describe("configs", () => {
76
84
  googleConf._rev
77
85
  )
78
86
  expect(events.auth.SSOUpdated).toBeCalledTimes(1)
79
- expect(events.auth.SSOUpdated).toBeCalledWith(Config.GOOGLE)
87
+ expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.GOOGLE)
80
88
  expect(events.auth.SSOActivated).not.toBeCalled()
81
89
  expect(events.auth.SSODeactivated).toBeCalledTimes(1)
82
- expect(events.auth.SSODeactivated).toBeCalledWith(Config.GOOGLE)
83
- await config.deleteConfig(Config.GOOGLE)
90
+ expect(events.auth.SSODeactivated).toBeCalledWith(ConfigType.GOOGLE)
91
+ await config.deleteConfig(ConfigType.GOOGLE)
84
92
  })
85
93
 
86
94
  it("should update google config to activated", async () => {
@@ -92,11 +100,11 @@ describe("configs", () => {
92
100
  googleConf._rev
93
101
  )
94
102
  expect(events.auth.SSOUpdated).toBeCalledTimes(1)
95
- expect(events.auth.SSOUpdated).toBeCalledWith(Config.GOOGLE)
103
+ expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.GOOGLE)
96
104
  expect(events.auth.SSODeactivated).not.toBeCalled()
97
105
  expect(events.auth.SSOActivated).toBeCalledTimes(1)
98
- expect(events.auth.SSOActivated).toBeCalledWith(Config.GOOGLE)
99
- await config.deleteConfig(Config.GOOGLE)
106
+ expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.GOOGLE)
107
+ await config.deleteConfig(ConfigType.GOOGLE)
100
108
  })
101
109
  })
102
110
  })
@@ -115,20 +123,20 @@ describe("configs", () => {
115
123
  it("should create activated OIDC config", async () => {
116
124
  await saveOIDCConfig()
117
125
  expect(events.auth.SSOCreated).toBeCalledTimes(1)
118
- expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC)
126
+ expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.OIDC)
119
127
  expect(events.auth.SSODeactivated).not.toBeCalled()
120
128
  expect(events.auth.SSOActivated).toBeCalledTimes(1)
121
- expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC)
122
- await config.deleteConfig(Config.OIDC)
129
+ expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.OIDC)
130
+ await config.deleteConfig(ConfigType.OIDC)
123
131
  })
124
132
 
125
133
  it("should create deactivated OIDC config", async () => {
126
134
  await saveOIDCConfig({ activated: false })
127
135
  expect(events.auth.SSOCreated).toBeCalledTimes(1)
128
- expect(events.auth.SSOCreated).toBeCalledWith(Config.OIDC)
136
+ expect(events.auth.SSOCreated).toBeCalledWith(ConfigType.OIDC)
129
137
  expect(events.auth.SSOActivated).not.toBeCalled()
130
138
  expect(events.auth.SSODeactivated).not.toBeCalled()
131
- await config.deleteConfig(Config.OIDC)
139
+ await config.deleteConfig(ConfigType.OIDC)
132
140
  })
133
141
  })
134
142
 
@@ -142,11 +150,11 @@ describe("configs", () => {
142
150
  oidcConf._rev
143
151
  )
144
152
  expect(events.auth.SSOUpdated).toBeCalledTimes(1)
145
- expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC)
153
+ expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.OIDC)
146
154
  expect(events.auth.SSOActivated).not.toBeCalled()
147
155
  expect(events.auth.SSODeactivated).toBeCalledTimes(1)
148
- expect(events.auth.SSODeactivated).toBeCalledWith(Config.OIDC)
149
- await config.deleteConfig(Config.OIDC)
156
+ expect(events.auth.SSODeactivated).toBeCalledWith(ConfigType.OIDC)
157
+ await config.deleteConfig(ConfigType.OIDC)
150
158
  })
151
159
 
152
160
  it("should update OIDC config to activated", async () => {
@@ -158,11 +166,11 @@ describe("configs", () => {
158
166
  oidcConf._rev
159
167
  )
160
168
  expect(events.auth.SSOUpdated).toBeCalledTimes(1)
161
- expect(events.auth.SSOUpdated).toBeCalledWith(Config.OIDC)
169
+ expect(events.auth.SSOUpdated).toBeCalledWith(ConfigType.OIDC)
162
170
  expect(events.auth.SSODeactivated).not.toBeCalled()
163
171
  expect(events.auth.SSOActivated).toBeCalledTimes(1)
164
- expect(events.auth.SSOActivated).toBeCalledWith(Config.OIDC)
165
- await config.deleteConfig(Config.OIDC)
172
+ expect(events.auth.SSOActivated).toBeCalledWith(ConfigType.OIDC)
173
+ await config.deleteConfig(ConfigType.OIDC)
166
174
  })
167
175
  })
168
176
  })
@@ -179,11 +187,11 @@ describe("configs", () => {
179
187
 
180
188
  describe("create", () => {
181
189
  it("should create SMTP config", async () => {
182
- await config.deleteConfig(Config.SMTP)
190
+ await config.deleteConfig(ConfigType.SMTP)
183
191
  await saveSMTPConfig()
184
192
  expect(events.email.SMTPUpdated).not.toBeCalled()
185
193
  expect(events.email.SMTPCreated).toBeCalledTimes(1)
186
- await config.deleteConfig(Config.SMTP)
194
+ await config.deleteConfig(ConfigType.SMTP)
187
195
  })
188
196
  })
189
197
 
@@ -194,24 +202,15 @@ describe("configs", () => {
194
202
  await saveSMTPConfig(smtpConf.config, smtpConf._id, smtpConf._rev)
195
203
  expect(events.email.SMTPCreated).not.toBeCalled()
196
204
  expect(events.email.SMTPUpdated).toBeCalledTimes(1)
197
- await config.deleteConfig(Config.SMTP)
205
+ await config.deleteConfig(ConfigType.SMTP)
198
206
  })
199
207
  })
200
208
  })
201
209
 
202
210
  describe("settings", () => {
203
- const saveSettingsConfig = async (
204
- conf?: any,
205
- _id?: string,
206
- _rev?: string
207
- ) => {
208
- const settingsConfig = structures.configs.settings(conf)
209
- return saveConfig(settingsConfig, _id, _rev)
210
- }
211
-
212
211
  describe("create", () => {
213
212
  it("should create settings config with default settings", async () => {
214
- await config.deleteConfig(Config.SETTINGS)
213
+ await config.deleteConfig(ConfigType.SETTINGS)
215
214
 
216
215
  await saveSettingsConfig()
217
216
 
@@ -222,7 +221,7 @@ describe("configs", () => {
222
221
 
223
222
  it("should create settings config with non-default settings", async () => {
224
223
  config.selfHosted()
225
- await config.deleteConfig(Config.SETTINGS)
224
+ await config.deleteConfig(ConfigType.SETTINGS)
226
225
  const conf = {
227
226
  company: "acme",
228
227
  logoUrl: "http://example.com",
@@ -241,7 +240,7 @@ describe("configs", () => {
241
240
  describe("update", () => {
242
241
  it("should update settings config", async () => {
243
242
  config.selfHosted()
244
- await config.deleteConfig(Config.SETTINGS)
243
+ await config.deleteConfig(ConfigType.SETTINGS)
245
244
  const settingsConfig = await saveSettingsConfig()
246
245
  settingsConfig.config.company = "acme"
247
246
  settingsConfig.config.logoUrl = "http://example.com"
@@ -262,14 +261,43 @@ describe("configs", () => {
262
261
  })
263
262
  })
264
263
 
265
- it("should return the correct checklist status based on the state of the budibase installation", async () => {
266
- await config.saveSmtpConfig()
264
+ describe("GET /api/global/configs/checklist", () => {
265
+ it("should return the correct checklist", async () => {
266
+ await config.saveSmtpConfig()
267
+
268
+ const res = await config.api.configs.getConfigChecklist()
269
+ const checklist = res.body
267
270
 
268
- const res = await config.api.configs.getConfigChecklist()
269
- const checklist = res.body
271
+ expect(checklist.apps.checked).toBeFalsy()
272
+ expect(checklist.smtp.checked).toBeTruthy()
273
+ expect(checklist.adminUser.checked).toBeTruthy()
274
+ })
275
+ })
270
276
 
271
- expect(checklist.apps.checked).toBeFalsy()
272
- expect(checklist.smtp.checked).toBeTruthy()
273
- expect(checklist.adminUser.checked).toBeTruthy()
277
+ describe("GET /api/global/configs/public", () => {
278
+ it("should return the expected public settings", async () => {
279
+ await saveSettingsConfig()
280
+
281
+ const res = await config.api.configs.getPublicSettings()
282
+ const body = res.body as GetPublicSettingsResponse
283
+
284
+ const expected = {
285
+ _id: "config_settings",
286
+ type: "settings",
287
+ config: {
288
+ company: "Budibase",
289
+ logoUrl: "",
290
+ analyticsEnabled: false,
291
+ google: true,
292
+ googleCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/google/callback`,
293
+ isSSOEnforced: false,
294
+ oidc: false,
295
+ oidcCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/oidc/callback`,
296
+ platformUrl: "http://localhost:10000",
297
+ },
298
+ }
299
+ delete body._rev
300
+ expect(body).toEqual(expected)
301
+ })
274
302
  })
275
303
  })
@@ -1,3 +1,4 @@
1
+ jest.unmock("node-fetch")
1
2
  import { TestConfiguration } from "../../../../tests"
2
3
  import { EmailTemplatePurpose } from "../../../../constants"
3
4
  const nodemailer = require("nodemailer")
@@ -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: selfHosted,
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,13 @@ 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 { auth, logging, events, middleware } from "@budibase/backend-core"
16
+ import {
17
+ auth,
18
+ logging,
19
+ events,
20
+ middleware,
21
+ env as coreEnv,
22
+ } from "@budibase/backend-core"
17
23
  db.init()
18
24
  import Koa from "koa"
19
25
  import koaBody from "koa-body"
@@ -25,7 +31,7 @@ const koaSession = require("koa-session")
25
31
  const logger = require("koa-pino-logger")
26
32
  import destroyable from "server-destroy"
27
33
 
28
- if (env.ENABLE_SSO_MAINTENANCE_MODE) {
34
+ if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE) {
29
35
  console.warn(
30
36
  "Warning: ENABLE_SSO_MAINTENANCE_MODE is set. It is recommended this flag is disabled if maintenance is not in progress"
31
37
  )
@@ -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.isPreventSSOPasswords(user)) {
62
- throw new HTTPError("SSO user cannot reset password", 400)
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
- describe("isPreventSSOPasswords", () => {
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.isPreventSSOPasswords(user)
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.isPreventSSOPasswords(user)
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.isPreventSSOPasswords(user)
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.isPreventSSOPasswords(user)
70
+ const result = await users.isPreventPasswordActions(user)
47
71
  expect(result).toBe(false)
48
72
  })
49
73
  })
@@ -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 { groups as groupsSdk } from "@budibase/pro"
38
+ import * as pro from "@budibase/pro"
38
39
  import * as accountSdk from "../accounts"
39
40
 
40
41
  const PAGE_LIMIT = 8
@@ -122,8 +123,8 @@ const buildUser = async (
122
123
 
123
124
  let hashedPassword
124
125
  if (password) {
125
- if (await isPreventSSOPasswords(user)) {
126
- throw new HTTPError("SSO user cannot set password", 400)
126
+ if (await isPreventPasswordActions(user)) {
127
+ throw new HTTPError("Password change is disabled for this user", 400)
127
128
  }
128
129
  hashedPassword = opts.hashPassword ? await utils.hash(password) : password
129
130
  } else if (dbUser) {
@@ -188,13 +189,18 @@ const validateUniqueUser = async (email: string, tenantId: string) => {
188
189
  }
189
190
  }
190
191
 
191
- export async function isPreventSSOPasswords(user: User) {
192
+ export async function isPreventPasswordActions(user: User) {
192
193
  // when in maintenance mode we allow sso users with the admin role
193
194
  // to perform any password action - this prevents lockout
194
- if (env.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) {
195
+ if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE && user.admin?.global) {
195
196
  return false
196
197
  }
197
198
 
199
+ // SSO is enforced for all users
200
+ if (await pro.features.isSSOEnforced()) {
201
+ return true
202
+ }
203
+
198
204
  // Check local sso
199
205
  if (isSSOUser(user)) {
200
206
  return true
@@ -278,7 +284,7 @@ export const save = async (
278
284
 
279
285
  if (userGroups.length > 0) {
280
286
  for (let groupId of userGroups) {
281
- groupPromises.push(groupsSdk.addUsers(groupId, [_id]))
287
+ groupPromises.push(pro.groups.addUsers(groupId, [_id]))
282
288
  }
283
289
  }
284
290
  }
@@ -456,7 +462,7 @@ export const bulkCreate = async (
456
462
  const groupPromises = []
457
463
  const createdUserIds = saved.map(user => user._id)
458
464
  for (let groupId of groups) {
459
- groupPromises.push(groupsSdk.addUsers(groupId, createdUserIds))
465
+ groupPromises.push(pro.groups.addUsers(groupId, createdUserIds))
460
466
  }
461
467
  await Promise.all(groupPromises)
462
468
  }
@@ -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`)
@@ -13,6 +13,7 @@ export class EmailAPI extends TestAPI {
13
13
  email: "test@test.com",
14
14
  purpose,
15
15
  tenantId: this.config.getTenantId(),
16
+ userId: this.config.user?._id!,
16
17
  })
17
18
  .set(this.config.defaultHeaders())
18
19
  .expect("Content-Type", /json/)
@@ -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: Config.OIDC,
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: Config.GOOGLE,
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: Config.SMTP,
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: Config.SMTP,
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: "don.bahringer@ethereal.email",
59
- pass: "yCKSH8rWyUPbnhGYk9",
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: Config.SETTINGS,
75
+ type: ConfigType.SETTINGS,
69
76
  config: {
70
77
  platformUrl: "http://localhost:10000",
71
78
  logoUrl: "",
@@ -1,11 +1,11 @@
1
1
  import env from "../environment"
2
- import { EmailTemplatePurpose, TemplateType, Config } from "../constants"
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, Database } from "@budibase/types"
8
- import { tenancy, db as dbCore } from "@budibase/backend-core"
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: any) {
39
+ function createSMTPTransport(config?: SMTPInnerConfig) {
40
40
  let options: any
41
- let secure = config.secure
41
+ let secure = config?.secure
42
42
  // default it if not specified
43
43
  if (secure == null) {
44
- secure = config.port === 465
44
+ secure = config?.port === 465
45
45
  }
46
46
  if (!TEST_MODE) {
47
47
  options = {
48
- port: config.port,
49
- host: config.host,
48
+ port: config?.port,
49
+ host: config?.host,
50
50
  secure: secure,
51
- auth: config.auth,
51
+ auth: config?.auth,
52
52
  }
53
53
  options.tls = {
54
54
  rejectUnauthorized: false,
55
55
  }
56
- if (config.connectionTimeout) {
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(workspaceId?: string) {
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 db = tenancy.getGlobalDB()
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 db = tenancy.getGlobalDB()
206
- let config =
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.from,
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.subject) {
188
+ if (opts?.subject || config?.subject) {
235
189
  message.subject = await processString(
236
- opts?.subject || config.subject,
190
+ (opts?.subject || config?.subject) as string,
237
191
  context
238
192
  )
239
193
  }
@@ -1,6 +1,5 @@
1
- import { db as dbCore, tenancy } from "@budibase/backend-core"
1
+ import { tenancy, configs } from "@budibase/backend-core"
2
2
  import {
3
- Config,
4
3
  InternalTemplateBinding,
5
4
  LOGO_URL,
6
5
  EmailTemplatePurpose,
@@ -10,20 +9,16 @@ const BASE_COMPANY = "Budibase"
10
9
 
11
10
  export async function getSettingsTemplateContext(
12
11
  purpose: EmailTemplatePurpose,
13
- code?: string
12
+ code?: string | null
14
13
  ) {
15
- const db = tenancy.getGlobalDB()
16
- // TODO: use more granular settings in the future if required
17
- let settings =
18
- (await dbCore.getScopedConfig(db, { type: Config.SETTINGS })) || {}
14
+ let settings = await configs.getSettingsConfig()
19
15
  const URL = settings.platformUrl
20
16
  const context: any = {
21
17
  [InternalTemplateBinding.LOGO_URL]:
22
18
  checkSlashesInUrl(`${URL}/${settings.logoUrl}`) || LOGO_URL,
23
19
  [InternalTemplateBinding.PLATFORM_URL]: URL,
24
20
  [InternalTemplateBinding.COMPANY]: settings.company || BASE_COMPANY,
25
- [InternalTemplateBinding.DOCS_URL]:
26
- settings.docsUrl || "https://docs.budibase.com/",
21
+ [InternalTemplateBinding.DOCS_URL]: "https://docs.budibase.com/",
27
22
  [InternalTemplateBinding.LOGIN_URL]: checkSlashesInUrl(
28
23
  tenancy.addTenantToUrl(`${URL}/login`)
29
24
  ),