diff --git a/lerna.json b/lerna.json index 29f6eae86b..21cb99262b 100644 --- a/lerna.json +++ b/lerna.json @@ -16,6 +16,7 @@ "packages/worker", "packages/pro/packages/pro" ], + "useWorkspaces": true, "command": { "publish": { "ignoreChanges": [ diff --git a/package.json b/package.json index 49ffc5fef7..56f015f8c0 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "husky": "^8.0.3", "js-yaml": "^4.1.0", "kill-port": "^1.6.1", - "lerna": "^7.0.1", + "lerna": "7.0.0-alpha.0", "madge": "^6.0.0", "minimist": "^1.2.8", - "nx": "^16.3.2", + "nx": "^16.2.1", "prettier": "^2.3.1", "prettier-plugin-svelte": "^2.3.0", "rimraf": "^3.0.2", diff --git a/packages/backend-core/src/middleware/passport/datasource/google.ts b/packages/backend-core/src/middleware/passport/datasource/google.ts index ae6b3b4913..6fd4e9ff32 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.ts +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -1,11 +1,10 @@ import * as google from "../sso/google" import { Cookie } from "../../../constants" +import { clearCookie, getCookie } from "../../../utils" +import { doWithDB } from "../../../db" import * as configs from "../../../configs" -import * as cache from "../../../cache" -import * as utils from "../../../utils" -import { UserCtx, SSOProfile } from "@budibase/types" +import { BBContext, Database, SSOProfile } from "@budibase/types" import { ssoSaveUserNoOp } from "../sso/sso" - const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy type Passport = { @@ -23,7 +22,7 @@ async function fetchGoogleCreds() { export async function preAuth( passport: Passport, - ctx: UserCtx, + ctx: BBContext, next: Function ) { // get the relevant config @@ -37,8 +36,8 @@ export async function preAuth( ssoSaveUserNoOp ) - if (!ctx.query.appId) { - ctx.throw(400, "appId query param not present.") + if (!ctx.query.appId || !ctx.query.datasourceId) { + ctx.throw(400, "appId and datasourceId query params not present.") } return passport.authenticate(strategy, { @@ -50,7 +49,7 @@ export async function preAuth( export async function postAuth( passport: Passport, - ctx: UserCtx, + ctx: BBContext, next: Function ) { // get the relevant config @@ -58,7 +57,7 @@ export async function postAuth( const platformUrl = await configs.getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` - const authStateCookie = utils.getCookie(ctx, Cookie.DatasourceAuth) + const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth) return passport.authenticate( new GoogleStrategy( @@ -70,26 +69,33 @@ export async function postAuth( ( accessToken: string, refreshToken: string, - _profile: SSOProfile, + profile: SSOProfile, done: Function ) => { - utils.clearCookie(ctx, Cookie.DatasourceAuth) + clearCookie(ctx, Cookie.DatasourceAuth) done(null, { accessToken, refreshToken }) } ), { successRedirect: "/", failureRedirect: "/error" }, async (err: any, tokens: string[]) => { const baseUrl = `/builder/app/${authStateCookie.appId}/data` - - const id = utils.newid() - await cache.store( - `datasource:creation:${authStateCookie.appId}:google:${id}`, - { - tokens, + // update the DB for the datasource with all the user info + await doWithDB(authStateCookie.appId, async (db: Database) => { + let datasource + try { + datasource = await db.get(authStateCookie.datasourceId) + } catch (err: any) { + if (err.status === 404) { + ctx.redirect(baseUrl) + } } - ) - - ctx.redirect(`${baseUrl}/new?continue_google_setup=${id}`) + if (!datasource.config) { + datasource.config = {} + } + datasource.config.auth = { type: "google", ...tokens } + await db.put(datasource) + ctx.redirect(`${baseUrl}/datasource/${authStateCookie.datasourceId}`) + }) } )(ctx, next) } diff --git a/packages/backend-core/src/security/encryption.ts b/packages/backend-core/src/security/encryption.ts index 7a8cfaf04a..f9adb68955 100644 --- a/packages/backend-core/src/security/encryption.ts +++ b/packages/backend-core/src/security/encryption.ts @@ -1,17 +1,12 @@ import crypto from "crypto" -import fs from "fs" -import zlib from "zlib" import env from "../environment" -import { join } from "path" const ALGO = "aes-256-ctr" const SEPARATOR = "-" const ITERATIONS = 10000 +const RANDOM_BYTES = 16 const STRETCH_LENGTH = 32 -const SALT_LENGTH = 16 -const IV_LENGTH = 16 - export enum SecretOption { API = "api", ENCRYPTION = "encryption", @@ -36,15 +31,15 @@ export function getSecret(secretOption: SecretOption): string { return secret } -function stretchString(secret: string, salt: Buffer) { - return crypto.pbkdf2Sync(secret, salt, ITERATIONS, STRETCH_LENGTH, "sha512") +function stretchString(string: string, salt: Buffer) { + return crypto.pbkdf2Sync(string, salt, ITERATIONS, STRETCH_LENGTH, "sha512") } export function encrypt( input: string, secretOption: SecretOption = SecretOption.API ) { - const salt = crypto.randomBytes(SALT_LENGTH) + const salt = crypto.randomBytes(RANDOM_BYTES) const stretched = stretchString(getSecret(secretOption), salt) const cipher = crypto.createCipheriv(ALGO, stretched, salt) const base = cipher.update(input) @@ -65,115 +60,3 @@ export function decrypt( const final = decipher.final() return Buffer.concat([base, final]).toString() } - -export async function encryptFile( - { dir, filename }: { dir: string; filename: string }, - secret: string -) { - const outputFileName = `${filename}.enc` - - const filePath = join(dir, filename) - const inputFile = fs.createReadStream(filePath) - const outputFile = fs.createWriteStream(join(dir, outputFileName)) - - const salt = crypto.randomBytes(SALT_LENGTH) - const iv = crypto.randomBytes(IV_LENGTH) - const stretched = stretchString(secret, salt) - const cipher = crypto.createCipheriv(ALGO, stretched, iv) - - outputFile.write(salt) - outputFile.write(iv) - - inputFile.pipe(zlib.createGzip()).pipe(cipher).pipe(outputFile) - - return new Promise<{ filename: string; dir: string }>(r => { - outputFile.on("finish", () => { - r({ - filename: outputFileName, - dir, - }) - }) - }) -} - -async function getSaltAndIV(path: string) { - const fileStream = fs.createReadStream(path) - - const salt = await readBytes(fileStream, SALT_LENGTH) - const iv = await readBytes(fileStream, IV_LENGTH) - fileStream.close() - return { salt, iv } -} - -export async function decryptFile( - inputPath: string, - outputPath: string, - secret: string -) { - const { salt, iv } = await getSaltAndIV(inputPath) - const inputFile = fs.createReadStream(inputPath, { - start: SALT_LENGTH + IV_LENGTH, - }) - - const outputFile = fs.createWriteStream(outputPath) - - const stretched = stretchString(secret, salt) - const decipher = crypto.createDecipheriv(ALGO, stretched, iv) - - const unzip = zlib.createGunzip() - - inputFile.pipe(decipher).pipe(unzip).pipe(outputFile) - - return new Promise((res, rej) => { - outputFile.on("finish", () => { - outputFile.close() - res() - }) - - inputFile.on("error", e => { - outputFile.close() - rej(e) - }) - - decipher.on("error", e => { - outputFile.close() - rej(e) - }) - - unzip.on("error", e => { - outputFile.close() - rej(e) - }) - - outputFile.on("error", e => { - outputFile.close() - rej(e) - }) - }) -} - -function readBytes(stream: fs.ReadStream, length: number) { - return new Promise((resolve, reject) => { - let bytesRead = 0 - const data: Buffer[] = [] - - stream.on("readable", () => { - let chunk - - while ((chunk = stream.read(length - bytesRead)) !== null) { - data.push(chunk) - bytesRead += chunk.length - } - - resolve(Buffer.concat(data)) - }) - - stream.on("end", () => { - reject(new Error("Insufficient data in the stream.")) - }) - - stream.on("error", error => { - reject(error) - }) - }) -} diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index e8a3c76c0a..bdf7a38726 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -140,13 +140,9 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string { * Gets the role object, this is mainly useful for two purposes, to check if the level exists and * to check if the role inherits any others. * @param {string|null} roleId The level ID to lookup. - * @param {object|null} opts options for the function, like whether to halt errors, instead return public. * @returns {Promise} The role object, which may contain an "inherits" property. */ -export async function getRole( - roleId?: string, - opts?: { defaultPublic?: boolean } -): Promise { +export async function getRole(roleId?: string): Promise { if (!roleId) { return undefined } @@ -165,9 +161,6 @@ export async function getRole( // finalise the ID role._id = getExternalRoleID(role._id) } catch (err) { - if (!isBuiltin(roleId) && opts?.defaultPublic) { - return cloneDeep(BUILTIN_ROLES.PUBLIC) - } // only throw an error if there is no role at all if (Object.keys(role).length === 0) { throw err diff --git a/packages/bbui/src/FancyForm/FancyCheckbox.svelte b/packages/bbui/src/FancyForm/FancyCheckbox.svelte index 0a2e5ac159..191cc79485 100644 --- a/packages/bbui/src/FancyForm/FancyCheckbox.svelte +++ b/packages/bbui/src/FancyForm/FancyCheckbox.svelte @@ -8,8 +8,6 @@ export let disabled = false export let error = null export let validate = null - export let indeterminate = false - export let compact = false const dispatch = createEventDispatcher() @@ -23,19 +21,11 @@ } - + - + -
+
{#if text} {text} {/if} @@ -57,10 +47,6 @@ line-clamp: 2; -webkit-box-orient: vertical; } - .text.compact { - font-size: 13px; - line-height: 15px; - } .text > :global(*) { font-size: inherit !important; } diff --git a/packages/bbui/src/FancyForm/FancyCheckboxGroup.svelte b/packages/bbui/src/FancyForm/FancyCheckboxGroup.svelte deleted file mode 100644 index aaea388c36..0000000000 --- a/packages/bbui/src/FancyForm/FancyCheckboxGroup.svelte +++ /dev/null @@ -1,68 +0,0 @@ - - -{#if options && Array.isArray(options)} -
- - {#if showSelectAll} - - {/if} - {#each options as option, i} - - {/each} - -
-{/if} - - diff --git a/packages/bbui/src/FancyForm/FancyField.svelte b/packages/bbui/src/FancyForm/FancyField.svelte index 455f4b38fb..0c99394599 100644 --- a/packages/bbui/src/FancyForm/FancyField.svelte +++ b/packages/bbui/src/FancyForm/FancyField.svelte @@ -11,7 +11,6 @@ export let value export let ref export let autoHeight - export let compact = false const formContext = getContext("fancy-form") const id = Math.random() @@ -43,7 +42,6 @@ class:disabled class:focused class:clickable - class:compact class:auto-height={autoHeight} >
@@ -63,6 +61,7 @@ diff --git a/packages/builder/src/components/backend/DatasourceNavigator/_components/GoogleButton.svelte b/packages/builder/src/components/backend/DatasourceNavigator/_components/GoogleButton.svelte index ceb8fd7f4b..b7d70d88b7 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/_components/GoogleButton.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/_components/GoogleButton.svelte @@ -3,6 +3,8 @@ import { store } from "builderStore" import { auth } from "stores/portal" + export let preAuthStep + export let datasource export let disabled export let samePage @@ -13,8 +15,18 @@ class:disabled {disabled} on:click={async () => { + let ds = datasource let appId = $store.appId - const url = `/api/global/auth/${tenantId}/datasource/google?appId=${appId}` + if (!ds) { + const resp = await preAuthStep() + if (resp.datasource && resp.appId) { + ds = resp.datasource + appId = resp.appId + } else { + ds = resp + } + } + const url = `/api/global/auth/${tenantId}/datasource/google?datasourceId=${ds._id}&appId=${appId}` if (samePage) { window.location = url } else { diff --git a/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js b/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js index 2486942dea..18aa361570 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js +++ b/packages/builder/src/components/backend/DatasourceNavigator/icons/index.js @@ -44,9 +44,6 @@ export default ICONS export function getIcon(integrationType, schema) { const integrationList = get(integrations) - if (!integrationList) { - return - } if (integrationList[integrationType]?.iconUrl) { return { url: integrationList[integrationType].iconUrl } } else if (schema?.custom || !ICONS[integrationType]) { diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte index 1d84dbbe39..31a0d21cd8 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte @@ -1,19 +1,12 @@ nextStep()} - {confirmText} - cancelText={fetchTableStep ? "Cancel" : "Back"} - showSecondaryButton={datasourcePlus} + title={`Connect to ${name}`} + onConfirm={() => saveDatasource()} + confirmText={datasource.plus ? "Connect" : "Save and continue to query"} + cancelText="Back" + showSecondaryButton={datasource.plus} size="L" disabled={!isValid} > - - {#if !fetchTableStep} - Connect your database to Budibase using the config below - {:else} - Choose what tables you want to sync with Budibase - {/if} + Connect your database to Budibase using the config below. - {#if !fetchTableStep} - (isValid = e.detail)} - /> - {:else} -
- -
- {/if} + (isValid = e.detail)} + />
- - diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte index 14f81f915c..0783a9fe53 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte @@ -1,207 +1,43 @@ - {#if step === GoogleDatasouceConfigStep.AUTH} - - {#if isGoogleConfigured === true} - - Authenticate with your google account to use the {integrationName} integration. - - - {:else if isGoogleConfigured === false} + + {#if isGoogleConfigured === true} + Google authentication is not enabled, please complete Google SSO - configuration.Authenticate with your google account to use the {IntegrationNames[ + datasource.type + ]} integration. - Configure Google SSO - {/if} - {/if} - {#if step === GoogleDatasouceConfigStep.SET_URL} - - Add the URL of the sheet you want to connect. - - (isValid = e.detail)} - /> - - {/if} - {#if step === GoogleDatasouceConfigStep.SET_SHEETS} - - Select which spreadsheets you want to connect. - - - - {#if setSheetsErrorTitle || setSheetsErrorMessage} - - {/if} + save(datasource, true)} /> + {:else if isGoogleConfigured === false} + Google authentication is not enabled, please complete Google SSO + configuration. + Configure Google SSO {/if} diff --git a/packages/builder/src/components/commandPalette/CommandPalette.svelte b/packages/builder/src/components/commandPalette/CommandPalette.svelte index 3a369446a3..ae946dc10c 100644 --- a/packages/builder/src/components/commandPalette/CommandPalette.svelte +++ b/packages/builder/src/components/commandPalette/CommandPalette.svelte @@ -69,7 +69,7 @@ name: "App", description: "", icon: "Play", - action: () => store.update(state => ({ ...state, showPreview: true })), + action: () => window.open(`/${$store.appId}`), }, { type: "Preview", diff --git a/packages/builder/src/components/deploy/AppActions.svelte b/packages/builder/src/components/deploy/AppActions.svelte index a85eb5a154..9813237317 100644 --- a/packages/builder/src/components/deploy/AppActions.svelte +++ b/packages/builder/src/components/deploy/AppActions.svelte @@ -62,10 +62,7 @@ } const previewApp = () => { - store.update(state => ({ - ...state, - showPreview: true, - })) + window.open(`/${application}`) } const viewApp = () => { diff --git a/packages/builder/src/components/start/ExportAppModal.svelte b/packages/builder/src/components/start/ExportAppModal.svelte index 4a69aaef74..948416b192 100644 --- a/packages/builder/src/components/start/ExportAppModal.svelte +++ b/packages/builder/src/components/start/ExportAppModal.svelte @@ -1,128 +1,27 @@ - - {#if currentStep === Step.CONFIG} - - - - - {#if !encypt} - - {/if} - {/if} - {#if currentStep === Step.SET_PASSWORD} - - {/if} + + + Apps can be exported with or without data that is within internal tables - + select this below. + diff --git a/packages/builder/src/helpers/validation/validation.js b/packages/builder/src/helpers/validation/validation.js index f64bf56835..db5dfe4430 100644 --- a/packages/builder/src/helpers/validation/validation.js +++ b/packages/builder/src/helpers/validation/validation.js @@ -6,6 +6,7 @@ export function createValidationStore(initialValue, ...validators) { let touched = false const value = writable(initialValue || "") + const error = derived(value, $v => validate($v, validators)) const touchedStore = derived(value, () => { if (!touched) { touched = true @@ -13,10 +14,6 @@ export function createValidationStore(initialValue, ...validators) { } return touched }) - const error = derived( - [value, touchedStore], - ([$v, $t]) => $t && validate($v, validators) - ) return [value, error, touchedStore] } diff --git a/packages/builder/src/helpers/validation/yup/index.js b/packages/builder/src/helpers/validation/yup/index.js index b5bdf030a5..20ddaebb1a 100644 --- a/packages/builder/src/helpers/validation/yup/index.js +++ b/packages/builder/src/helpers/validation/yup/index.js @@ -5,7 +5,6 @@ import { notifications } from "@budibase/bbui" export const createValidationStore = () => { const DEFAULT = { - values: {}, errors: {}, touched: {}, valid: false, @@ -34,9 +33,6 @@ export const createValidationStore = () => { case "email": propertyValidator = string().email().nullable() break - case "password": - propertyValidator = string().nullable() - break default: propertyValidator = string().nullable() } @@ -45,68 +41,9 @@ export const createValidationStore = () => { propertyValidator = propertyValidator.required() } - // We want to do this after the possible required validation, to prioritise the required error - switch (type) { - case "password": - propertyValidator = propertyValidator.min(8) - break - } - validator[propertyName] = propertyValidator } - const observe = async (propertyName, value) => { - const values = get(validation).values - let fieldIsValid - if (!values.hasOwnProperty(propertyName)) { - // Initial setup - values[propertyName] = value - return - } - - if (value === values[propertyName]) { - return - } - - const obj = object().shape(validator) - try { - validation.update(store => { - store.errors[propertyName] = null - return store - }) - await obj.validateAt(propertyName, { [propertyName]: value }) - fieldIsValid = true - } catch (error) { - const [fieldError] = error.errors - if (fieldError) { - validation.update(store => { - store.errors[propertyName] = capitalise(fieldError) - store.valid = false - return store - }) - } - } - - if (fieldIsValid) { - // Validate the rest of the fields - try { - await obj.validate( - { ...values, [propertyName]: value }, - { abortEarly: false } - ) - validation.update(store => { - store.valid = true - return store - }) - } catch { - validation.update(store => { - store.valid = false - return store - }) - } - } - } - const check = async values => { const obj = object().shape(validator) // clear the previous errors @@ -150,6 +87,5 @@ export const createValidationStore = () => { check, addValidator, addValidatorType, - observe, } } diff --git a/packages/builder/src/pages/builder/app/[application]/_components/PreviewOverlay.svelte b/packages/builder/src/pages/builder/app/[application]/_components/PreviewOverlay.svelte deleted file mode 100644 index d069d1b4c7..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/_components/PreviewOverlay.svelte +++ /dev/null @@ -1,91 +0,0 @@ - - -
-
-
-
- -
-