diff --git a/lerna.json b/lerna.json index 9332b26f66..f3225ef0e7 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.2.12-alpha.41", + "version": "2.2.12-alpha.45", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 645f11db39..51694cb916 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.2.12-alpha.41", + "version": "2.2.12-alpha.45", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -23,7 +23,7 @@ }, "dependencies": { "@budibase/nano": "10.1.1", - "@budibase/types": "2.2.12-alpha.41", + "@budibase/types": "2.2.12-alpha.45", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index 92392457d6..f7d15b3880 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -77,6 +77,7 @@ export const StaticDatabases = { apiKeys: "apikeys", usageQuota: "usage_quota", licenseInfo: "license_info", + environmentVariables: "environmentvariables", }, }, // contains information about tenancy and so on diff --git a/packages/backend-core/src/context/Context.ts b/packages/backend-core/src/context/Context.ts index f0ccdb97a8..02b7713764 100644 --- a/packages/backend-core/src/context/Context.ts +++ b/packages/backend-core/src/context/Context.ts @@ -1,17 +1,14 @@ import { AsyncLocalStorage } from "async_hooks" +import { ContextMap } from "./mainContext" export default class Context { - static storage = new AsyncLocalStorage>() + static storage = new AsyncLocalStorage() - static run(context: Record, func: any) { + static run(context: ContextMap, func: any) { return Context.storage.run(context, () => func()) } - static get(): Record { - return Context.storage.getStore() as Record - } - - static set(context: Record) { - Context.storage.enterWith(context) + static get(): ContextMap { + return Context.storage.getStore() as ContextMap } } diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index c44ec4e767..9884d25d5a 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -16,6 +16,7 @@ export type ContextMap = { tenantId?: string appId?: string identity?: IdentityContext + environmentVariables?: Record } let TEST_APP_ID: string | null = null @@ -75,7 +76,7 @@ export function getTenantIDFromAppID(appId: string) { } } -function updateContext(updates: ContextMap) { +function updateContext(updates: ContextMap): ContextMap { let context: ContextMap try { context = Context.get() @@ -120,15 +121,23 @@ export async function doInTenant( return newContext(updates, task) } -export async function doInAppContext(appId: string, task: any): Promise { - if (!appId) { +export async function doInAppContext( + appId: string | null, + task: any +): Promise { + if (!appId && !env.isTest()) { throw new Error("appId is required") } - const tenantId = getTenantIDFromAppID(appId) - const updates: ContextMap = { appId } - if (tenantId) { - updates.tenantId = tenantId + let updates: ContextMap + if (!appId) { + updates = { appId: "" } + } else { + const tenantId = getTenantIDFromAppID(appId) + updates = { appId } + if (tenantId) { + updates.tenantId = tenantId + } } return newContext(updates, task) } @@ -189,25 +198,25 @@ export const getProdAppId = () => { return conversions.getProdAppID(appId) } -export function updateTenantId(tenantId?: string) { - let context: ContextMap = updateContext({ - tenantId, - }) - Context.set(context) +export function doInEnvironmentContext( + values: Record, + task: any +) { + if (!values) { + throw new Error("Must supply environment variables.") + } + const updates = { + environmentVariables: values, + } + return newContext(updates, task) } -export function updateAppId(appId: string) { - let context: ContextMap = updateContext({ - appId, - }) - try { - Context.set(context) - } catch (err) { - if (env.isTest()) { - TEST_APP_ID = appId - } else { - throw err - } +export function getEnvironmentVariables() { + const context = Context.get() + if (!context.environmentVariables) { + return null + } else { + return context.environmentVariables } } diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 91556ddcd6..d742ca1cc9 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -37,6 +37,7 @@ const environment = { }, JS_BCRYPT: process.env.JS_BCRYPT, JWT_SECRET: process.env.JWT_SECRET, + ENCRYPTION_KEY: process.env.ENCRYPTION_KEY, COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005", COUCH_DB_USERNAME: process.env.COUCH_DB_USER, COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD, diff --git a/packages/backend-core/src/events/publishers/environmentVariable.ts b/packages/backend-core/src/events/publishers/environmentVariable.ts new file mode 100644 index 0000000000..d28e259b82 --- /dev/null +++ b/packages/backend-core/src/events/publishers/environmentVariable.ts @@ -0,0 +1,38 @@ +import { + Event, + EnvironmentVariableCreatedEvent, + EnvironmentVariableDeletedEvent, + EnvironmentVariableUpgradePanelOpenedEvent, +} from "@budibase/types" +import { publishEvent } from "../events" + +async function created(name: string, environments: string[]) { + const properties: EnvironmentVariableCreatedEvent = { + name, + environments, + } + await publishEvent(Event.ENVIRONMENT_VARIABLE_CREATED, properties) +} + +async function deleted(name: string) { + const properties: EnvironmentVariableDeletedEvent = { + name, + } + await publishEvent(Event.ENVIRONMENT_VARIABLE_DELETED, properties) +} + +async function upgradePanelOpened(userId: string) { + const properties: EnvironmentVariableUpgradePanelOpenedEvent = { + userId, + } + await publishEvent( + Event.ENVIRONMENT_VARIABLE_UPGRADE_PANEL_OPENED, + properties + ) +} + +export default { + created, + deleted, + upgradePanelOpened, +} diff --git a/packages/backend-core/src/events/publishers/index.ts b/packages/backend-core/src/events/publishers/index.ts index 2316785ed7..34e47b2990 100644 --- a/packages/backend-core/src/events/publishers/index.ts +++ b/packages/backend-core/src/events/publishers/index.ts @@ -20,3 +20,4 @@ export { default as backfill } from "./backfill" export { default as group } from "./group" export { default as plugin } from "./plugin" export { default as backup } from "./backup" +export { default as environmentVariable } from "./environmentVariable" diff --git a/packages/backend-core/src/security/encryption.ts b/packages/backend-core/src/security/encryption.ts index a9006f302d..d0707cb850 100644 --- a/packages/backend-core/src/security/encryption.ts +++ b/packages/backend-core/src/security/encryption.ts @@ -2,19 +2,45 @@ import crypto from "crypto" import env from "../environment" const ALGO = "aes-256-ctr" -const SECRET = env.JWT_SECRET const SEPARATOR = "-" const ITERATIONS = 10000 const RANDOM_BYTES = 16 const STRETCH_LENGTH = 32 +export enum SecretOption { + JWT = "jwt", + ENCRYPTION = "encryption", +} + +function getSecret(secretOption: SecretOption): string { + let secret, secretName + switch (secretOption) { + case SecretOption.ENCRYPTION: + secret = env.ENCRYPTION_KEY + secretName = "ENCRYPTION_KEY" + break + case SecretOption.JWT: + default: + secret = env.JWT_SECRET + secretName = "JWT_SECRET" + break + } + if (!secret) { + throw new Error(`Secret "${secretName}" has not been set in environment.`) + } + return secret +} + function stretchString(string: string, salt: Buffer) { return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512") } -export function encrypt(input: string) { +export function encrypt( + input: string, + secretOption: SecretOption = SecretOption.JWT +) { const salt = crypto.randomBytes(RANDOM_BYTES) - const stretched = stretchString(SECRET!, salt) + const stretched = stretchString(getSecret(secretOption), salt) const cipher = crypto.createCipheriv(ALGO, stretched, salt) const base = cipher.update(input) const final = cipher.final() @@ -22,10 +48,13 @@ export function encrypt(input: string) { return `${salt.toString("hex")}${SEPARATOR}${encrypted}` } -export function decrypt(input: string) { +export function decrypt( + input: string, + secretOption: SecretOption = SecretOption.JWT +) { const [salt, encrypted] = input.split(SEPARATOR) const saltBuffer = Buffer.from(salt, "hex") - const stretched = stretchString(SECRET!, saltBuffer) + const stretched = stretchString(getSecret(secretOption), saltBuffer) const decipher = crypto.createDecipheriv(ALGO, stretched, saltBuffer) const base = decipher.update(Buffer.from(encrypted, "hex")) const final = decipher.final() diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 6437628002..9696bd525c 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.2.12-alpha.41", + "version": "2.2.12-alpha.45", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.2.12-alpha.41", + "@budibase/string-templates": "2.2.12-alpha.45", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/bbui/src/Actions/click_outside.js b/packages/bbui/src/Actions/click_outside.js index 6842b94a32..bdcbaa5d88 100644 --- a/packages/bbui/src/Actions/click_outside.js +++ b/packages/bbui/src/Actions/click_outside.js @@ -1,4 +1,4 @@ -const ignoredClasses = [".flatpickr-calendar"] +const ignoredClasses = [".flatpickr-calendar", ".spectrum-Popover"] let clickHandlers = [] /** @@ -19,7 +19,7 @@ const handleClick = event => { } // Ignore clicks for modals, unless the handler is registered from a modal - const sourceInModal = handler.element.closest(".spectrum-Modal") != null + const sourceInModal = handler.anchor.closest(".spectrum-Modal") != null const clickInModal = event.target.closest(".spectrum-Modal") != null if (clickInModal && !sourceInModal) { return @@ -33,10 +33,10 @@ document.documentElement.addEventListener("click", handleClick, true) /** * Adds or updates a click handler */ -const updateHandler = (id, element, callback) => { +const updateHandler = (id, element, anchor, callback) => { let existingHandler = clickHandlers.find(x => x.id === id) if (!existingHandler) { - clickHandlers.push({ id, element, callback }) + clickHandlers.push({ id, element, anchor, callback }) } else { existingHandler.callback = callback } @@ -51,12 +51,22 @@ const removeHandler = id => { /** * Svelte action to apply a click outside handler for a certain element + * opts.anchor is an optional param specifying the real root source of the + * component being observed. This is required for things like popovers, where + * the element using the clickoutside action is the popover, but the popover is + * rendered at the root of the DOM somewhere, whereas the popover anchor is the + * element we actually want to consider when determining the source component. */ -export default (element, callback) => { +export default (element, opts) => { const id = Math.random() - updateHandler(id, element, callback) + const update = newOpts => { + const callback = newOpts?.callback || newOpts + const anchor = newOpts?.anchor || element + updateHandler(id, element, anchor, callback) + } + update(opts) return { - update: newCallback => updateHandler(id, element, newCallback), + update, destroy: () => removeHandler(id), } } diff --git a/packages/bbui/src/FancyForm/FancyButton.svelte b/packages/bbui/src/FancyForm/FancyButton.svelte index 09615df8fa..d794980911 100644 --- a/packages/bbui/src/FancyForm/FancyButton.svelte +++ b/packages/bbui/src/FancyForm/FancyButton.svelte @@ -14,6 +14,7 @@ {/if} {/if} +
diff --git a/packages/bbui/src/Form/Core/EnvDropdown.svelte b/packages/bbui/src/Form/Core/EnvDropdown.svelte new file mode 100644 index 0000000000..f51704248b --- /dev/null +++ b/packages/bbui/src/Form/Core/EnvDropdown.svelte @@ -0,0 +1,282 @@ + + +
+
+ + + +
+ {#if open} +
+
    + {#if !environmentVariablesEnabled} +
    + Upgrade your plan to get environment variables +
    + {:else if variables.length} +
    + {#each variables as variable, idx} +
  • handleVarSelect(variable.name)} + > + +
    + {variable.name} + +
    + +
    +
  • + {/each} +
    + {:else} +
    + You don't have any environment variables yet +
    + {/if} +
+ + {#if environmentVariablesEnabled} +
showModal()} class="add-variable"> + +
Add Variable
+
+ {:else} +
handleUpgradePanel()} class="add-variable"> + +
Upgrade plan
+
+ {/if} +
+ {/if} +
+ + diff --git a/packages/bbui/src/Form/EnvDropdown.svelte b/packages/bbui/src/Form/EnvDropdown.svelte new file mode 100644 index 0000000000..33924af0a5 --- /dev/null +++ b/packages/bbui/src/Form/EnvDropdown.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index 7eb77d90fa..5505b2546d 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -68,7 +68,10 @@