diff --git a/lerna.json b/lerna.json index 0606169dd3..6c2252d22f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.2.12-alpha.32", + "version": "2.2.12-alpha.47", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index ca87d6daa4..ad27bad311 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.32", + "version": "2.2.12-alpha.47", "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.32", + "@budibase/types": "2.2.12-alpha.47", "@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 6b67ca77d0..c341f14024 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.32", + "version": "2.2.12-alpha.47", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,8 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "2.2.12-alpha.32", + "@budibase/string-templates": "2.2.12-alpha.47", + "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", "@spectrum-css/avatar": "3.0.2", diff --git a/packages/bbui/src/Accordion/Accordion.svelte b/packages/bbui/src/Accordion/Accordion.svelte new file mode 100644 index 0000000000..1c88450c9a --- /dev/null +++ b/packages/bbui/src/Accordion/Accordion.svelte @@ -0,0 +1,58 @@ + + +
+
+

+ + +

+
+ +
+
+
+ + diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index cfc810807e..cc4417be2a 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -88,6 +88,7 @@ } .is-selected:not(.spectrum-ActionButton--emphasized) { background: var(--spectrum-global-color-gray-300); + border-color: var(--spectrum-global-color-gray-700); } .noPadding { padding: 0; 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 new file mode 100644 index 0000000000..d794980911 --- /dev/null +++ b/packages/bbui/src/FancyForm/FancyButton.svelte @@ -0,0 +1,30 @@ + + + + {#if icon} + {#if icon.includes("/")} + button + {:else} + + {/if} + {/if} + +
+ +
+
+ + diff --git a/packages/bbui/src/FancyForm/FancyButtonRadio.svelte b/packages/bbui/src/FancyForm/FancyButtonRadio.svelte new file mode 100644 index 0000000000..510fd8efb8 --- /dev/null +++ b/packages/bbui/src/FancyForm/FancyButtonRadio.svelte @@ -0,0 +1,70 @@ + + + + {#if label} + {label} + {/if} + +
+ {#each options as option} + onChange(getOptionValue(option))} + > + {getOptionLabel(option)} + + {/each} +
+
+ + diff --git a/packages/bbui/src/FancyForm/FancyCheckbox.svelte b/packages/bbui/src/FancyForm/FancyCheckbox.svelte new file mode 100644 index 0000000000..191cc79485 --- /dev/null +++ b/packages/bbui/src/FancyForm/FancyCheckbox.svelte @@ -0,0 +1,53 @@ + + + + + + +
+ {#if text} + {text} + {/if} + +
+
+ + diff --git a/packages/bbui/src/FancyForm/FancyField.svelte b/packages/bbui/src/FancyForm/FancyField.svelte new file mode 100644 index 0000000000..89f2dec7d0 --- /dev/null +++ b/packages/bbui/src/FancyForm/FancyField.svelte @@ -0,0 +1,126 @@ + + +
+
+
+ +
+ {#if error} +
+ +
+ {/if} +
+ {#if error} +
+ {error} +
+ {/if} +
+ + diff --git a/packages/bbui/src/FancyForm/FancyFieldLabel.svelte b/packages/bbui/src/FancyForm/FancyFieldLabel.svelte new file mode 100644 index 0000000000..181cff50e4 --- /dev/null +++ b/packages/bbui/src/FancyForm/FancyFieldLabel.svelte @@ -0,0 +1,25 @@ + + +
+ +
+ + diff --git a/packages/bbui/src/FancyForm/FancyForm.svelte b/packages/bbui/src/FancyForm/FancyForm.svelte new file mode 100644 index 0000000000..f874238572 --- /dev/null +++ b/packages/bbui/src/FancyForm/FancyForm.svelte @@ -0,0 +1,40 @@ + + +
+ +
+ + diff --git a/packages/bbui/src/FancyForm/FancyInput.svelte b/packages/bbui/src/FancyForm/FancyInput.svelte new file mode 100644 index 0000000000..8735e2c30c --- /dev/null +++ b/packages/bbui/src/FancyForm/FancyInput.svelte @@ -0,0 +1,77 @@ + + + + {#if label} + {label} + {/if} + (focused = true)} + on:blur={() => (focused = false)} + class:placeholder + /> + {#if suffix && !placeholder} +
{suffix}
+ {/if} +
+ + diff --git a/packages/bbui/src/FancyForm/FancySelect.svelte b/packages/bbui/src/FancyForm/FancySelect.svelte new file mode 100644 index 0000000000..ee43ecc3ca --- /dev/null +++ b/packages/bbui/src/FancyForm/FancySelect.svelte @@ -0,0 +1,147 @@ + + + (open = true)} +> + {#if label} + {label} + {/if} + +
+ {selectedLabel || ""} +
+ +
+ +
+
+ + (open = false)} + useAnchorWidth={true} + maxWidth={null} +> +
+ {#if options.length} + {#each options as option, idx} +
onChange(getOptionValue(option, idx))} + > + + {getOptionLabel(option, idx)} + + {#if value === getOptionValue(option, idx)} + + {/if} +
+ {/each} + {/if} +
+
+ + diff --git a/packages/bbui/src/FancyForm/index.js b/packages/bbui/src/FancyForm/index.js new file mode 100644 index 0000000000..241036fb35 --- /dev/null +++ b/packages/bbui/src/FancyForm/index.js @@ -0,0 +1,6 @@ +export { default as FancyInput } from "./FancyInput.svelte" +export { default as FancyCheckbox } from "./FancyCheckbox.svelte" +export { default as FancySelect } from "./FancySelect.svelte" +export { default as FancyButton } from "./FancyButton.svelte" +export { default as FancyForm } from "./FancyForm.svelte" +export { default as FancyButtonRadio } from "./FancyButtonRadio.svelte" 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/Core/Select.svelte b/packages/bbui/src/Form/Core/Select.svelte index 3e15b7f6ef..721083e3a6 100644 --- a/packages/bbui/src/Form/Core/Select.svelte +++ b/packages/bbui/src/Form/Core/Select.svelte @@ -18,8 +18,11 @@ export let autoWidth = false export let autocomplete = false export let sort = false + const dispatch = createEventDispatcher() + let open = false + $: fieldText = getFieldText(value, options, placeholder) $: fieldIcon = getFieldAttribute(getOptionIcon, value, options) $: fieldColour = getFieldAttribute(getOptionColour, value, options) 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/Layout/Page.svelte b/packages/bbui/src/Layout/Page.svelte index 15aabd2c61..01111fda9a 100644 --- a/packages/bbui/src/Layout/Page.svelte +++ b/packages/bbui/src/Layout/Page.svelte @@ -18,6 +18,7 @@
+
import "@spectrum-css/link/dist/index-vars.css" + import { createEventDispatcher } from "svelte" export let href = "#" export let size = "M" @@ -9,10 +10,12 @@ export let overBackground = false export let target export let download + + const dispatch = createEventDispatcher() dispatch("click") && e.stopPropagation()} {href} {target} {download} diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index 7eb77d90fa..225fbfeed9 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -68,7 +68,10 @@