diff --git a/lerna.json b/lerna.json index 94fceeac7e..0360b2e821 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.2.12-alpha.44", + "version": "2.2.12-alpha.50", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 38054918cd..81898f6d0b 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.44", + "version": "2.2.12-alpha.50", "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.44", + "@budibase/types": "2.2.12-alpha.50", "@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/events/publishers/user.ts b/packages/backend-core/src/events/publishers/user.ts index c296a8bc49..1fe50149b5 100644 --- a/packages/backend-core/src/events/publishers/user.ts +++ b/packages/backend-core/src/events/publishers/user.ts @@ -13,6 +13,7 @@ import { UserPermissionAssignedEvent, UserPermissionRemovedEvent, UserUpdatedEvent, + UserOnboardingEvent, } from "@budibase/types" async function created(user: User, timestamp?: number) { @@ -36,6 +37,13 @@ async function deleted(user: User) { await publishEvent(Event.USER_DELETED, properties) } +export async function onboardingComplete(user: User) { + const properties: UserOnboardingEvent = { + userId: user._id as string, + } + await publishEvent(Event.USER_ONBOARDING_COMPLETE, properties) +} + // PERMISSIONS async function permissionAdminAssigned(user: User, timestamp?: number) { @@ -126,6 +134,7 @@ export default { permissionAdminRemoved, permissionBuilderAssigned, permissionBuilderRemoved, + onboardingComplete, invited, inviteAccepted, passwordForceReset, 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 803e933fe2..18092ae2a5 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.44", + "version": "2.2.12-alpha.50", "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.44", + "@budibase/string-templates": "2.2.12-alpha.50", "@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/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index 463b69169f..09264d5250 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -3,6 +3,9 @@ export default function positionDropdown( { anchor, align, maxWidth, useAnchorWidth } ) { const update = () => { + if (!anchor) { + return + } const anchorBounds = anchor.getBoundingClientRect() const elementBounds = element.getBoundingClientRect() let styles = { @@ -13,6 +16,8 @@ export default function positionDropdown( top: null, } + let popoverLeftPad = 20 + // Determine vertical styles if (window.innerHeight - anchorBounds.bottom < 100) { styles.top = anchorBounds.top - elementBounds.height - 5 @@ -29,7 +34,13 @@ export default function positionDropdown( styles.minWidth = anchorBounds.width } if (align === "right") { - styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width + let left = + anchorBounds.left + anchorBounds.width / 2 - elementBounds.width + // Accommodate margin on popover: 1.25rem; ~20px + if (left + elementBounds.width + popoverLeftPad > window.innerWidth) { + left -= 20 + } + styles.left = left } else if (align === "right-side") { styles.left = anchorBounds.left + anchorBounds.width } else { @@ -54,8 +65,11 @@ export default function positionDropdown( const resizeObserver = new ResizeObserver(entries => { entries.forEach(update) }) - resizeObserver.observe(anchor) + if (anchor) { + resizeObserver.observe(anchor) + } resizeObserver.observe(element) + resizeObserver.observe(document.body) document.addEventListener("scroll", update, true) diff --git a/packages/bbui/src/Button/Button.svelte b/packages/bbui/src/Button/Button.svelte index 979ec6a728..b8ffe9f7e6 100644 --- a/packages/bbui/src/Button/Button.svelte +++ b/packages/bbui/src/Button/Button.svelte @@ -15,11 +15,13 @@ export let tooltip = undefined export let dataCy export let newStyles = true + export let id let showTooltip = false