@budibase/worker 3.24.1 → 3.24.3
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/__mocks__/@budibase/pro.ts +1 -0
- package/package.json +3 -3
- package/src/api/controllers/global/configs.ts +102 -3
- package/src/api/routes/global/configs.ts +21 -0
- package/src/api/routes/global/tests/configs.spec.ts +122 -1
- package/src/tests/api/configs.ts +9 -0
- package/src/tests/structures/configs.ts +18 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/worker",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "3.24.
|
|
4
|
+
"version": "3.24.3",
|
|
5
5
|
"description": "Budibase background service",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"repository": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"server-destroy": "1.0.1",
|
|
71
71
|
"undici": "^7.16.0",
|
|
72
72
|
"uuid": "^8.3.2",
|
|
73
|
-
"yaml": "^2.8.
|
|
73
|
+
"yaml": "^2.8.2"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@types/jsonwebtoken": "9.0.3",
|
|
@@ -109,5 +109,5 @@
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
|
-
"gitHead": "
|
|
112
|
+
"gitHead": "a85e0ba10b7d19e204062fb63dc306953284e277"
|
|
113
113
|
}
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
tenancy,
|
|
11
11
|
} from "@budibase/backend-core"
|
|
12
12
|
import * as pro from "@budibase/pro"
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
BUILDER_URLS,
|
|
15
|
+
filterValidTranslationOverrides,
|
|
16
|
+
} from "@budibase/shared-core"
|
|
14
17
|
import {
|
|
15
18
|
AIInnerConfig,
|
|
16
19
|
Config,
|
|
@@ -21,13 +24,16 @@ import {
|
|
|
21
24
|
FindConfigResponse,
|
|
22
25
|
GetPublicOIDCConfigResponse,
|
|
23
26
|
GetPublicSettingsResponse,
|
|
27
|
+
GetPublicTranslationsResponse,
|
|
24
28
|
GoogleInnerConfig,
|
|
29
|
+
TranslationOverrides,
|
|
25
30
|
isAIConfig,
|
|
26
31
|
isGoogleConfig,
|
|
27
32
|
isOIDCConfig,
|
|
28
33
|
isRecaptchaConfig,
|
|
29
34
|
isSettingsConfig,
|
|
30
35
|
isSMTPConfig,
|
|
36
|
+
isTranslationsConfig,
|
|
31
37
|
OIDCConfigs,
|
|
32
38
|
OIDCLogosConfig,
|
|
33
39
|
PASSWORD_REPLACEMENT,
|
|
@@ -39,9 +45,12 @@ import {
|
|
|
39
45
|
SMTPInnerConfig,
|
|
40
46
|
SSOConfig,
|
|
41
47
|
SSOConfigType,
|
|
48
|
+
TranslationsConfigInner,
|
|
42
49
|
UploadConfigFileResponse,
|
|
43
50
|
UserCtx,
|
|
44
51
|
} from "@budibase/types"
|
|
52
|
+
|
|
53
|
+
const PUBLIC_TRANSLATION_PREFIXES = ["login.", "forgotPassword."]
|
|
45
54
|
import env from "../../../environment"
|
|
46
55
|
import * as email from "../../../utilities/email"
|
|
47
56
|
import { checkAnyUserExists } from "../../../utilities/users"
|
|
@@ -299,6 +308,77 @@ export async function processRecaptchaConfig(
|
|
|
299
308
|
}
|
|
300
309
|
}
|
|
301
310
|
|
|
311
|
+
function prepareTranslationsConfig(
|
|
312
|
+
ctx: UserCtx,
|
|
313
|
+
config?: TranslationsConfigInner
|
|
314
|
+
): TranslationsConfigInner {
|
|
315
|
+
const defaultLocale = config?.defaultLocale || "en"
|
|
316
|
+
const locales: TranslationsConfigInner["locales"] = {}
|
|
317
|
+
const now = new Date().toISOString()
|
|
318
|
+
const updatedBy = ctx.user?._id
|
|
319
|
+
|
|
320
|
+
Object.entries(config?.locales || {}).forEach(([locale, localeConfig]) => {
|
|
321
|
+
locales[locale] = {
|
|
322
|
+
label: localeConfig?.label,
|
|
323
|
+
overrides: filterValidTranslationOverrides(localeConfig?.overrides),
|
|
324
|
+
updatedAt: localeConfig?.updatedAt || now,
|
|
325
|
+
updatedBy: localeConfig?.updatedBy || updatedBy,
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
if (!locales[defaultLocale]) {
|
|
330
|
+
locales[defaultLocale] = {
|
|
331
|
+
label: defaultLocale === "en" ? "English" : defaultLocale,
|
|
332
|
+
overrides: {},
|
|
333
|
+
updatedAt: now,
|
|
334
|
+
updatedBy,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
defaultLocale,
|
|
340
|
+
locales,
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function filterPublicTranslations(
|
|
345
|
+
config: TranslationsConfigInner
|
|
346
|
+
): TranslationsConfigInner {
|
|
347
|
+
const locales: TranslationsConfigInner["locales"] = {}
|
|
348
|
+
for (const [locale, localeConfig] of Object.entries(config.locales || {})) {
|
|
349
|
+
const overrides = Object.entries(localeConfig?.overrides || {}).reduce(
|
|
350
|
+
(acc, [key, value]) => {
|
|
351
|
+
if (
|
|
352
|
+
PUBLIC_TRANSLATION_PREFIXES.some(prefix => key.startsWith(prefix))
|
|
353
|
+
) {
|
|
354
|
+
acc[key] = value
|
|
355
|
+
}
|
|
356
|
+
return acc
|
|
357
|
+
},
|
|
358
|
+
{} as TranslationOverrides
|
|
359
|
+
)
|
|
360
|
+
locales[locale] = {
|
|
361
|
+
...localeConfig,
|
|
362
|
+
overrides,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
defaultLocale: config.defaultLocale,
|
|
367
|
+
locales,
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function processTranslationsConfig(
|
|
372
|
+
ctx: UserCtx,
|
|
373
|
+
config: TranslationsConfigInner
|
|
374
|
+
) {
|
|
375
|
+
const enabled = await pro.features.isTranslationsEnabled()
|
|
376
|
+
if (!enabled) {
|
|
377
|
+
throw new ForbiddenError("License does not allow translations")
|
|
378
|
+
}
|
|
379
|
+
ctx.request.body.config = prepareTranslationsConfig(ctx, config)
|
|
380
|
+
}
|
|
381
|
+
|
|
302
382
|
export async function save(
|
|
303
383
|
ctx: UserCtx<SaveConfigRequest, SaveConfigResponse>
|
|
304
384
|
) {
|
|
@@ -335,6 +415,9 @@ export async function save(
|
|
|
335
415
|
case ConfigType.RECAPTCHA:
|
|
336
416
|
await processRecaptchaConfig(config, existingConfig?.config)
|
|
337
417
|
break
|
|
418
|
+
case ConfigType.TRANSLATIONS:
|
|
419
|
+
await processTranslationsConfig(ctx, config)
|
|
420
|
+
break
|
|
338
421
|
}
|
|
339
422
|
} catch (err: any) {
|
|
340
423
|
ctx.throw(400, err)
|
|
@@ -436,11 +519,11 @@ export async function find(ctx: UserCtx<void, FindConfigResponse>) {
|
|
|
436
519
|
break
|
|
437
520
|
}
|
|
438
521
|
|
|
439
|
-
stripSecrets(config)
|
|
522
|
+
stripSecrets(config, ctx)
|
|
440
523
|
ctx.body = config
|
|
441
524
|
}
|
|
442
525
|
|
|
443
|
-
function stripSecrets(config: Config) {
|
|
526
|
+
function stripSecrets(config: Config, ctx?: UserCtx) {
|
|
444
527
|
if (isAIConfig(config)) {
|
|
445
528
|
for (const key in config.config) {
|
|
446
529
|
if (config.config[key].apiKey) {
|
|
@@ -459,6 +542,11 @@ function stripSecrets(config: Config) {
|
|
|
459
542
|
}
|
|
460
543
|
} else if (isRecaptchaConfig(config)) {
|
|
461
544
|
config.config.secretKey = PASSWORD_REPLACEMENT
|
|
545
|
+
} else if (isTranslationsConfig(config)) {
|
|
546
|
+
config.config = prepareTranslationsConfig(
|
|
547
|
+
ctx || ({} as UserCtx),
|
|
548
|
+
config.config
|
|
549
|
+
)
|
|
462
550
|
}
|
|
463
551
|
}
|
|
464
552
|
|
|
@@ -582,6 +670,17 @@ export async function publicSettings(
|
|
|
582
670
|
}
|
|
583
671
|
}
|
|
584
672
|
|
|
673
|
+
export async function publicTranslations(
|
|
674
|
+
ctx: Ctx<void, GetPublicTranslationsResponse>
|
|
675
|
+
) {
|
|
676
|
+
try {
|
|
677
|
+
const configDoc = await configs.getTranslationsConfigDoc()
|
|
678
|
+
ctx.body = filterPublicTranslations(configDoc.config)
|
|
679
|
+
} catch (err: any) {
|
|
680
|
+
ctx.throw(err.status, err)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
585
684
|
export async function upload(ctx: UserCtx<void, UploadConfigFileResponse>) {
|
|
586
685
|
if (ctx.request.files == null || Array.isArray(ctx.request.files.file)) {
|
|
587
686
|
ctx.throw(400, "One file must be uploaded.")
|
|
@@ -87,6 +87,25 @@ function recaptchaValidation() {
|
|
|
87
87
|
}).required()
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function translationsValidation() {
|
|
91
|
+
return Joi.object({
|
|
92
|
+
defaultLocale: Joi.string().required(),
|
|
93
|
+
locales: Joi.object()
|
|
94
|
+
.pattern(
|
|
95
|
+
Joi.string(),
|
|
96
|
+
Joi.object({
|
|
97
|
+
label: Joi.string().optional().allow("", null),
|
|
98
|
+
overrides: Joi.object()
|
|
99
|
+
.pattern(Joi.string(), Joi.string().allow(""))
|
|
100
|
+
.default({}),
|
|
101
|
+
updatedAt: Joi.string().optional(),
|
|
102
|
+
updatedBy: Joi.string().optional(),
|
|
103
|
+
}).required()
|
|
104
|
+
)
|
|
105
|
+
.required(),
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
function buildConfigSaveValidation() {
|
|
91
110
|
// prettier-ignore
|
|
92
111
|
return auth.joiValidator.body(Joi.object({
|
|
@@ -107,6 +126,7 @@ function buildConfigSaveValidation() {
|
|
|
107
126
|
{ is: ConfigType.SCIM, then: scimValidation() },
|
|
108
127
|
{ is: ConfigType.AI, then: aiValidation() },
|
|
109
128
|
{ is: ConfigType.RECAPTCHA, then: recaptchaValidation() },
|
|
129
|
+
{ is: ConfigType.TRANSLATIONS, then: translationsValidation() },
|
|
110
130
|
],
|
|
111
131
|
}),
|
|
112
132
|
}).required().unknown(true),
|
|
@@ -141,4 +161,5 @@ loggedInRoutes
|
|
|
141
161
|
.get("/api/global/configs/checklist", controller.configChecklist)
|
|
142
162
|
.get("/api/global/configs/public", controller.publicSettings)
|
|
143
163
|
.get("/api/global/configs/public/oidc", controller.publicOidc)
|
|
164
|
+
.get("/api/global/configs/public/translations", controller.publicTranslations)
|
|
144
165
|
.get("/api/global/configs/:type", buildConfigGetValidation(), controller.find)
|
|
@@ -5,12 +5,15 @@ import {
|
|
|
5
5
|
ConfigType,
|
|
6
6
|
GetPublicSettingsResponse,
|
|
7
7
|
PKCEMethod,
|
|
8
|
+
TranslationsConfig,
|
|
8
9
|
} from "@budibase/types"
|
|
9
10
|
import { TestConfiguration, mocks, structures } from "../../../../tests"
|
|
11
|
+
import { resolveTranslationGroup } from "@budibase/shared-core"
|
|
12
|
+
import { processStringSync } from "@budibase/string-templates"
|
|
10
13
|
|
|
11
14
|
mocks.email.mock()
|
|
12
15
|
|
|
13
|
-
const { google, smtp, settings, oidc } = structures.configs
|
|
16
|
+
const { google, smtp, settings, oidc, translations } = structures.configs
|
|
14
17
|
|
|
15
18
|
describe("configs", () => {
|
|
16
19
|
const config = new TestConfiguration()
|
|
@@ -321,6 +324,84 @@ describe("configs", () => {
|
|
|
321
324
|
})
|
|
322
325
|
})
|
|
323
326
|
|
|
327
|
+
describe("translations", () => {
|
|
328
|
+
beforeEach(async () => {
|
|
329
|
+
await config.deleteConfig(ConfigType.TRANSLATIONS)
|
|
330
|
+
mocks.licenses.useTranslations()
|
|
331
|
+
mocks.pro.features.isTranslationsEnabled.mockResolvedValue(true)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
afterEach(async () => {
|
|
335
|
+
await config.deleteConfig(ConfigType.TRANSLATIONS)
|
|
336
|
+
mocks.pro.features.isTranslationsEnabled.mockReset()
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it("should save translations when feature enabled", async () => {
|
|
340
|
+
await saveConfig(translations({ "login.emailLabel": "Profile test" }))
|
|
341
|
+
const saved = await config.api.configs.getConfig(ConfigType.TRANSLATIONS)
|
|
342
|
+
expect(saved.config.locales.en.overrides["login.emailLabel"]).toEqual(
|
|
343
|
+
"Profile test"
|
|
344
|
+
)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it("should support non-default locales", async () => {
|
|
348
|
+
const spanishConfig: TranslationsConfig = {
|
|
349
|
+
type: ConfigType.TRANSLATIONS,
|
|
350
|
+
config: {
|
|
351
|
+
defaultLocale: "es",
|
|
352
|
+
locales: {
|
|
353
|
+
es: {
|
|
354
|
+
label: "Spanish",
|
|
355
|
+
overrides: {
|
|
356
|
+
"login.emailLabel": "Correo",
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
en: {
|
|
360
|
+
label: "English",
|
|
361
|
+
overrides: {
|
|
362
|
+
"login.emailLabel": "Email",
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await saveConfig(spanishConfig)
|
|
370
|
+
const saved = await config.api.configs.getConfig(ConfigType.TRANSLATIONS)
|
|
371
|
+
expect(saved.config.locales.es.overrides["login.emailLabel"]).toEqual(
|
|
372
|
+
"Correo"
|
|
373
|
+
)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it("should filter invalid override keys", async () => {
|
|
377
|
+
await saveConfig(translations({ invalid: "value" }))
|
|
378
|
+
const saved = await config.api.configs.getConfig(ConfigType.TRANSLATIONS)
|
|
379
|
+
expect(saved.config.locales.en.overrides).toEqual({})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it("should reject when translations feature disabled", async () => {
|
|
383
|
+
mocks.pro.features.isTranslationsEnabled.mockResolvedValue(false)
|
|
384
|
+
await expect(
|
|
385
|
+
saveConfig(translations({ "login.emailLabel": "Profile test" }))
|
|
386
|
+
).rejects.toThrow("License does not allow translations")
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it("should resolve bindings inside translation overrides", async () => {
|
|
390
|
+
await saveConfig(
|
|
391
|
+
translations({ "portal.greeting": "Welcome {{ name }}" })
|
|
392
|
+
)
|
|
393
|
+
const saved = await config.api.configs.getConfig(ConfigType.TRANSLATIONS)
|
|
394
|
+
const portalLabels = resolveTranslationGroup(
|
|
395
|
+
"portal",
|
|
396
|
+
saved.config.locales.en.overrides
|
|
397
|
+
)
|
|
398
|
+
const boundValue = processStringSync(portalLabels.greeting, {
|
|
399
|
+
name: "Budibuddy",
|
|
400
|
+
})
|
|
401
|
+
expect(boundValue).toEqual("Welcome Budibuddy")
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
324
405
|
describe("GET /api/global/configs/checklist", () => {
|
|
325
406
|
it("should return the correct checklist", async () => {
|
|
326
407
|
await config.saveSmtpConfig()
|
|
@@ -372,4 +453,44 @@ describe("configs", () => {
|
|
|
372
453
|
expect(body).toEqual(expected)
|
|
373
454
|
})
|
|
374
455
|
})
|
|
456
|
+
|
|
457
|
+
describe("GET /api/global/configs/public/translations", () => {
|
|
458
|
+
beforeEach(async () => {
|
|
459
|
+
await config.deleteConfig(ConfigType.TRANSLATIONS)
|
|
460
|
+
mocks.licenses.useTranslations()
|
|
461
|
+
mocks.pro.features.isTranslationsEnabled.mockResolvedValue(true)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
afterEach(async () => {
|
|
465
|
+
await config.deleteConfig(ConfigType.TRANSLATIONS)
|
|
466
|
+
mocks.pro.features.isTranslationsEnabled.mockReset()
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it("should return translation overrides", async () => {
|
|
470
|
+
await saveConfig(translations({ "login.emailLabel": "Hello" }))
|
|
471
|
+
const res = await config.api.configs.getPublicTranslations()
|
|
472
|
+
expect(res.body.defaultLocale).toEqual("en")
|
|
473
|
+
expect(res.body.locales.en.overrides["login.emailLabel"]).toEqual("Hello")
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it("should expose login and forgot labels without authentication", async () => {
|
|
477
|
+
await saveConfig(
|
|
478
|
+
translations({
|
|
479
|
+
"login.emailLabel": "Public email",
|
|
480
|
+
"forgotPassword.heading": "Reset password",
|
|
481
|
+
})
|
|
482
|
+
)
|
|
483
|
+
const res = await config
|
|
484
|
+
.getRequest()
|
|
485
|
+
.get(
|
|
486
|
+
`/api/global/configs/public/translations?tenantId=${config.getTenantId()}`
|
|
487
|
+
)
|
|
488
|
+
.expect(200)
|
|
489
|
+
.expect("Content-Type", /json/)
|
|
490
|
+
|
|
491
|
+
const overrides = res.body.locales.en.overrides
|
|
492
|
+
expect(overrides["login.emailLabel"]).toEqual("Public email")
|
|
493
|
+
expect(overrides["forgotPassword.heading"]).toEqual("Reset password")
|
|
494
|
+
})
|
|
495
|
+
})
|
|
375
496
|
})
|
package/src/tests/api/configs.ts
CHANGED
|
@@ -23,6 +23,15 @@ export class ConfigAPI extends TestAPI {
|
|
|
23
23
|
.expect("Content-Type", /json/)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
getPublicTranslations = (tenantId?: string) => {
|
|
27
|
+
const query = tenantId ? `?tenantId=${tenantId}` : ""
|
|
28
|
+
return this.request
|
|
29
|
+
.get(`/api/global/configs/public/translations${query}`)
|
|
30
|
+
.set(this.config.defaultHeaders())
|
|
31
|
+
.expect(200)
|
|
32
|
+
.expect("Content-Type", /json/)
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
getAIConfig = async () => {
|
|
27
36
|
return await this.getConfig(ConfigType.AI)
|
|
28
37
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
OIDCInnerConfig,
|
|
10
10
|
SMTPInnerConfig,
|
|
11
11
|
SettingsInnerConfig,
|
|
12
|
+
TranslationsConfig,
|
|
12
13
|
} from "@budibase/types"
|
|
13
14
|
|
|
14
15
|
export function oidc(conf?: Partial<OIDCInnerConfig>): OIDCConfig {
|
|
@@ -86,3 +87,20 @@ export function settings(conf?: Partial<SettingsInnerConfig>): SettingsConfig {
|
|
|
86
87
|
},
|
|
87
88
|
}
|
|
88
89
|
}
|
|
90
|
+
|
|
91
|
+
export function translations(
|
|
92
|
+
overrides: Record<string, string> = {}
|
|
93
|
+
): TranslationsConfig {
|
|
94
|
+
return {
|
|
95
|
+
type: ConfigType.TRANSLATIONS,
|
|
96
|
+
config: {
|
|
97
|
+
defaultLocale: "en",
|
|
98
|
+
locales: {
|
|
99
|
+
en: {
|
|
100
|
+
label: "English",
|
|
101
|
+
overrides,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|