From ee81fd7a5931770f4b1647bed2aac87cfeefd52b Mon Sep 17 00:00:00 2001 From: Adria Navarro <adria@budibase.com> Date: Mon, 29 May 2023 15:39:39 +0200 Subject: [PATCH 01/12] Split auth google section --- .../portal/settings/auth/google.svelte | 235 ++++++++++++++++++ .../builder/portal/settings/auth/index.svelte | 154 +----------- 2 files changed, 237 insertions(+), 152 deletions(-) create mode 100644 packages/builder/src/pages/builder/portal/settings/auth/google.svelte diff --git a/packages/builder/src/pages/builder/portal/settings/auth/google.svelte b/packages/builder/src/pages/builder/portal/settings/auth/google.svelte new file mode 100644 index 0000000000..82ab13cc8e --- /dev/null +++ b/packages/builder/src/pages/builder/portal/settings/auth/google.svelte @@ -0,0 +1,235 @@ +<script> + import GoogleLogo from "./_logos/Google.svelte" + import { isEqual, cloneDeep } from "lodash/fp" + import { + Button, + Heading, + Divider, + Label, + notifications, + Layout, + Input, + Body, + Toggle, + Icon, + Helpers, + Link, + } from "@budibase/bbui" + import { onMount } from "svelte" + import { API } from "api" + import { organisation, admin } from "stores/portal" + + const ConfigTypes = { + Google: "google", + } + + // Some older google configs contain a manually specified value - retain the functionality to edit the field + // When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change + $: googleCallbackUrl = undefined + $: googleCallbackReadonly = $admin.cloud || !googleCallbackUrl + + // Indicate to user that callback is based on platform url + // If there is an existing value, indicate that it may be removed to return to default behaviour + $: googleCallbackTooltip = $admin.cloud + ? null + : googleCallbackReadonly + ? "Visit the organisation page to update the platform URL" + : "Leave blank to use the default callback URL" + $: googleSheetsCallbackUrl = `${$organisation.platformUrl}/api/global/auth/datasource/google/callback` + + $: GoogleConfigFields = { + Google: [ + { name: "clientID", label: "Client ID" }, + { name: "clientSecret", label: "Client secret" }, + { + name: "callbackURL", + label: "Callback URL", + readonly: googleCallbackReadonly, + tooltip: googleCallbackTooltip, + placeholder: $organisation.googleCallbackUrl, + copyButton: true, + }, + { + name: "sheetsURL", + label: "Sheets URL", + readonly: googleCallbackReadonly, + tooltip: googleCallbackTooltip, + placeholder: googleSheetsCallbackUrl, + copyButton: true, + }, + ], + } + + let google + + const providers = { google } + + // control the state of the save button depending on whether form has changed + let originalGoogleDoc + let googleSaveButtonDisabled + $: { + isEqual(providers.google?.config, originalGoogleDoc?.config) + ? (googleSaveButtonDisabled = true) + : (googleSaveButtonDisabled = false) + } + + $: googleComplete = !!( + providers.google?.config?.clientID && providers.google?.config?.clientSecret + ) + + async function saveConfig(config) { + // Delete unsupported fields + delete config.createdAt + delete config.updatedAt + return API.saveConfig(config) + } + + async function saveGoogle() { + if (!googleComplete) { + notifications.error( + `Please fill in all required ${ConfigTypes.Google} fields` + ) + return + } + + const google = providers.google + + try { + const res = await saveConfig(google) + providers[res.type]._rev = res._rev + providers[res.type]._id = res._id + notifications.success(`Settings saved`) + } catch (e) { + notifications.error(e.message) + return + } + + googleSaveButtonDisabled = true + originalGoogleDoc = cloneDeep(providers.google) + } + + const copyToClipboard = async value => { + await Helpers.copyToClipboard(value) + notifications.success("Copied") + } + + onMount(async () => { + try { + await organisation.init() + } catch (error) { + notifications.error("Error getting org config") + } + + // Fetch Google config + let googleDoc + try { + googleDoc = await API.getConfig(ConfigTypes.Google) + } catch (error) { + notifications.error("Error fetching Google OAuth config") + } + if (!googleDoc?._id) { + providers.google = { + type: ConfigTypes.Google, + config: { activated: false }, + } + originalGoogleDoc = cloneDeep(googleDoc) + } else { + // Default activated to true for older configs + if (googleDoc.config.activated === undefined) { + googleDoc.config.activated = true + } + originalGoogleDoc = cloneDeep(googleDoc) + providers.google = googleDoc + } + googleCallbackUrl = providers?.google?.config?.callbackURL + }) +</script> + +{#if providers.google} + <Divider /> + <Layout gap="XS" noPadding> + <Heading size="S"> + <div class="provider-title"> + <GoogleLogo /> + <span>Google</span> + </div> + </Heading> + <Body size="S"> + To allow users to authenticate using their Google accounts, fill out the + fields below. Read the <Link + size="M" + href={"https://docs.budibase.com/docs/sso-with-google"} + >documentation</Link + > for more information. + </Body> + </Layout> + <Layout gap="XS" noPadding> + {#each GoogleConfigFields.Google as field} + <div class="form-row"> + <Label size="L" tooltip={field.tooltip}>{field.label}</Label> + <div class="inputContainer"> + <div class="input"> + <Input + bind:value={providers.google.config[field.name]} + readonly={field.readonly} + placeholder={field.placeholder} + /> + </div> + {#if field.copyButton} + <div + class="copy" + on:click={() => copyToClipboard(field.placeholder)} + > + <Icon size="S" name="Copy" /> + </div> + {/if} + </div> + </div> + {/each} + <div class="form-row"> + <Label size="L">Activated</Label> + <Toggle text="" bind:value={providers.google.config.activated} /> + </div> + </Layout> + <div> + <Button + disabled={googleSaveButtonDisabled} + cta + on:click={() => saveGoogle()} + > + Save + </Button> + </div> +{/if} + +<style> + .form-row { + display: grid; + grid-template-columns: 120px 1fr; + grid-gap: var(--spacing-l); + align-items: center; + } + + .provider-title { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: var(--spacing-m); + } + .provider-title span { + flex: 1 1 auto; + } + .inputContainer { + display: flex; + flex-direction: row; + } + .input { + flex: 1; + } + .copy { + display: flex; + align-items: center; + margin-left: 10px; + } +</style> diff --git a/packages/builder/src/pages/builder/portal/settings/auth/index.svelte b/packages/builder/src/pages/builder/portal/settings/auth/index.svelte index 38f5e0788b..36cf5c13a8 100644 --- a/packages/builder/src/pages/builder/portal/settings/auth/index.svelte +++ b/packages/builder/src/pages/builder/portal/settings/auth/index.svelte @@ -1,5 +1,4 @@ <script> - import GoogleLogo from "./_logos/Google.svelte" import OidcLogo from "./_logos/OIDC.svelte" import MicrosoftLogo from "assets/microsoft-logo.png" import Auth0Logo from "assets/auth0-logo.png" @@ -28,9 +27,9 @@ import { API } from "api" import { organisation, admin, licensing } from "stores/portal" import Scim from "./scim.svelte" + import Google from "./google.svelte" const ConfigTypes = { - Google: "google", OIDC: "oidc", } @@ -38,43 +37,6 @@ $: enforcedSSO = $organisation.isSSOEnforced - // Some older google configs contain a manually specified value - retain the functionality to edit the field - // When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change - $: googleCallbackUrl = undefined - $: googleCallbackReadonly = $admin.cloud || !googleCallbackUrl - - // Indicate to user that callback is based on platform url - // If there is an existing value, indicate that it may be removed to return to default behaviour - $: googleCallbackTooltip = $admin.cloud - ? null - : googleCallbackReadonly - ? "Visit the organisation page to update the platform URL" - : "Leave blank to use the default callback URL" - $: googleSheetsCallbackUrl = `${$organisation.platformUrl}/api/global/auth/datasource/google/callback` - - $: GoogleConfigFields = { - Google: [ - { name: "clientID", label: "Client ID" }, - { name: "clientSecret", label: "Client secret" }, - { - name: "callbackURL", - label: "Callback URL", - readonly: googleCallbackReadonly, - tooltip: googleCallbackTooltip, - placeholder: $organisation.googleCallbackUrl, - copyButton: true, - }, - { - name: "sheetsURL", - label: "Sheets URL", - readonly: googleCallbackReadonly, - tooltip: googleCallbackTooltip, - placeholder: googleSheetsCallbackUrl, - copyButton: true, - }, - ], - } - $: OIDCConfigFields = { Oidc: [ { name: "configUrl", label: "Config URL" }, @@ -133,15 +95,9 @@ const providers = { google, oidc } // control the state of the save button depending on whether form has changed - let originalGoogleDoc let originalOidcDoc - let googleSaveButtonDisabled let oidcSaveButtonDisabled $: { - isEqual(providers.google?.config, originalGoogleDoc?.config) - ? (googleSaveButtonDisabled = true) - : (googleSaveButtonDisabled = false) - // delete the callback url which is never saved to the oidc // config doc, to ensure an accurate comparison delete providers.oidc?.config.configs[0].callbackURL @@ -151,10 +107,6 @@ : (oidcSaveButtonDisabled = false) } - $: googleComplete = !!( - providers.google?.config?.clientID && providers.google?.config?.clientSecret - ) - $: oidcComplete = !!( providers.oidc?.config?.configs[0].configUrl && providers.oidc?.config?.configs[0].clientID && @@ -230,30 +182,6 @@ originalOidcDoc = cloneDeep(providers.oidc) } - async function saveGoogle() { - if (!googleComplete) { - notifications.error( - `Please fill in all required ${ConfigTypes.Google} fields` - ) - return - } - - const google = providers.google - - try { - const res = await saveConfig(google) - providers[res.type]._rev = res._rev - providers[res.type]._id = res._id - notifications.success(`Settings saved`) - } catch (e) { - notifications.error(e.message) - return - } - - googleSaveButtonDisabled = true - originalGoogleDoc = cloneDeep(providers.google) - } - let defaultScopes = ["profile", "email", "offline_access"] const refreshScopes = idx => { @@ -281,29 +209,6 @@ notifications.error("Error getting org config") } - // Fetch Google config - let googleDoc - try { - googleDoc = await API.getConfig(ConfigTypes.Google) - } catch (error) { - notifications.error("Error fetching Google OAuth config") - } - if (!googleDoc?._id) { - providers.google = { - type: ConfigTypes.Google, - config: { activated: false }, - } - originalGoogleDoc = cloneDeep(googleDoc) - } else { - // Default activated to true for older configs - if (googleDoc.config.activated === undefined) { - googleDoc.config.activated = true - } - originalGoogleDoc = cloneDeep(googleDoc) - providers.google = googleDoc - } - googleCallbackUrl = providers?.google?.config?.callbackURL - // Get the list of user uploaded logos and push it to the dropdown options. // This needs to be done before the config call so they're available when // the dropdown renders. @@ -395,62 +300,7 @@ > before enabling this feature. </Body> </Layout> - {#if providers.google} - <Divider /> - <Layout gap="XS" noPadding> - <Heading size="S"> - <div class="provider-title"> - <GoogleLogo /> - <span>Google</span> - </div> - </Heading> - <Body size="S"> - To allow users to authenticate using their Google accounts, fill out the - fields below. Read the <Link - size="M" - href={"https://docs.budibase.com/docs/sso-with-google"} - >documentation</Link - > for more information. - </Body> - </Layout> - <Layout gap="XS" noPadding> - {#each GoogleConfigFields.Google as field} - <div class="form-row"> - <Label size="L" tooltip={field.tooltip}>{field.label}</Label> - <div class="inputContainer"> - <div class="input"> - <Input - bind:value={providers.google.config[field.name]} - readonly={field.readonly} - placeholder={field.placeholder} - /> - </div> - {#if field.copyButton} - <div - class="copy" - on:click={() => copyToClipboard(field.placeholder)} - > - <Icon size="S" name="Copy" /> - </div> - {/if} - </div> - </div> - {/each} - <div class="form-row"> - <Label size="L">Activated</Label> - <Toggle text="" bind:value={providers.google.config.activated} /> - </div> - </Layout> - <div> - <Button - disabled={googleSaveButtonDisabled} - cta - on:click={() => saveGoogle()} - > - Save - </Button> - </div> - {/if} + <Google /> {#if providers.oidc} <Divider /> <Layout gap="XS" noPadding> From 7da9a041243e26b9e415e87a5f3b317e30735cd2 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 6 Jun 2023 09:14:18 +0000 Subject: [PATCH 02/12] Bump version to 2.6.24-alpha.1 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 9f8cd12e31..e44a4b60c7 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.6.24-alpha.0", + "version": "2.6.24-alpha.1", "npmClient": "yarn", "packages": [ "packages/backend-core", From 498eedaf1979306fb7d32aa17888ca0e339ac803 Mon Sep 17 00:00:00 2001 From: Adria Navarro <adria@budibase.com> Date: Mon, 29 May 2023 11:45:40 +0200 Subject: [PATCH 03/12] Remove confirm button while setting up google datasource --- .../modals/GoogleDatasourceConfigModal.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte index 0783a9fe53..bc6afe82c5 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte @@ -22,6 +22,7 @@ title={`Connect to ${IntegrationNames[datasource.type]}`} cancelText="Back" size="L" + showConfirmButton={false} > <!-- check true and false directly, don't render until flag is set --> {#if isGoogleConfigured === true} From c89708cda352c08736424320edb2a6cdee44b516 Mon Sep 17 00:00:00 2001 From: Adria Navarro <adria@budibase.com> Date: Mon, 29 May 2023 11:45:58 +0200 Subject: [PATCH 04/12] Google wizard on the same page --- .../modals/GoogleDatasourceConfigModal.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte index bc6afe82c5..4d82ad187f 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte @@ -33,7 +33,7 @@ ]} integration.</Body > </Layout> - <GoogleButton preAuthStep={() => save(datasource, true)} /> + <GoogleButton preAuthStep={() => save(datasource, true)} samePage /> {:else if isGoogleConfigured === false} <Body size="S" >Google authentication is not enabled, please complete Google SSO From 3a6a3eb8a5b716247ce35da53e492d5fa58ea57f Mon Sep 17 00:00:00 2001 From: Adria Navarro <adria@budibase.com> Date: Wed, 31 May 2023 10:32:11 +0200 Subject: [PATCH 05/12] Store tokens in cache and amend redirect --- .../middleware/passport/datasource/google.ts | 35 ++++++++----------- .../_components/GoogleButton.svelte | 14 +------- .../modals/GoogleDatasourceConfigModal.svelte | 3 +- .../worker/src/api/controllers/global/auth.ts | 1 - 4 files changed, 16 insertions(+), 37 deletions(-) diff --git a/packages/backend-core/src/middleware/passport/datasource/google.ts b/packages/backend-core/src/middleware/passport/datasource/google.ts index 6fd4e9ff32..3e8306c296 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.ts +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -1,10 +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 { BBContext, Database, SSOProfile } from "@budibase/types" +import { BBContext, SSOProfile } from "@budibase/types" import { ssoSaveUserNoOp } from "../sso/sso" +import { cache, utils } from "../../../" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy type Passport = { @@ -36,8 +36,8 @@ export async function preAuth( ssoSaveUserNoOp ) - if (!ctx.query.appId || !ctx.query.datasourceId) { - ctx.throw(400, "appId and datasourceId query params not present.") + if (!ctx.query.appId) { + ctx.throw(400, "appId query param not present.") } return passport.authenticate(strategy, { @@ -69,7 +69,7 @@ export async function postAuth( ( accessToken: string, refreshToken: string, - profile: SSOProfile, + _profile: SSOProfile, done: Function ) => { clearCookie(ctx, Cookie.DatasourceAuth) @@ -79,23 +79,16 @@ export async function postAuth( { successRedirect: "/", failureRedirect: "/error" }, async (err: any, tokens: string[]) => { const baseUrl = `/builder/app/${authStateCookie.appId}/data` - // 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) - } + + const id = utils.newid() + await cache.store( + `datasource:creation:${authStateCookie.appId}:google:${id}`, + { + tokens, } - if (!datasource.config) { - datasource.config = {} - } - datasource.config.auth = { type: "google", ...tokens } - await db.put(datasource) - ctx.redirect(`${baseUrl}/datasource/${authStateCookie.datasourceId}`) - }) + ) + + ctx.redirect(`${baseUrl}/new?type=google&action=continue&id=${id}`) } )(ctx, next) } diff --git a/packages/builder/src/components/backend/DatasourceNavigator/_components/GoogleButton.svelte b/packages/builder/src/components/backend/DatasourceNavigator/_components/GoogleButton.svelte index b7d70d88b7..ceb8fd7f4b 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/_components/GoogleButton.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/_components/GoogleButton.svelte @@ -3,8 +3,6 @@ import { store } from "builderStore" import { auth } from "stores/portal" - export let preAuthStep - export let datasource export let disabled export let samePage @@ -15,18 +13,8 @@ class:disabled {disabled} on:click={async () => { - let ds = datasource let appId = $store.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}` + const url = `/api/global/auth/${tenantId}/datasource/google?appId=${appId}` if (samePage) { window.location = url } else { diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte index 4d82ad187f..6388cfb7b5 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte @@ -3,7 +3,6 @@ import { IntegrationNames } from "constants/backend" import cloneDeep from "lodash/cloneDeepWith" import GoogleButton from "../_components/GoogleButton.svelte" - import { saveDatasource as save } from "builderStore/datasource" import { organisation } from "stores/portal" import { onMount } from "svelte" @@ -33,7 +32,7 @@ ]} integration.</Body > </Layout> - <GoogleButton preAuthStep={() => save(datasource, true)} samePage /> + <GoogleButton samePage /> {:else if isGoogleConfigured === false} <Body size="S" >Google authentication is not enabled, please complete Google SSO diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index c8f75b3610..131601c6ad 100644 --- a/packages/worker/src/api/controllers/global/auth.ts +++ b/packages/worker/src/api/controllers/global/auth.ts @@ -140,7 +140,6 @@ export const datasourcePreAuth = async (ctx: any, next: any) => { { provider, appId: ctx.query.appId, - datasourceId: ctx.query.datasourceId, }, Cookie.DatasourceAuth ) From d4ba73f331273accf8363b0404dca8de346de832 Mon Sep 17 00:00:00 2001 From: Adria Navarro <adria@budibase.com> Date: Wed, 31 May 2023 11:24:06 +0200 Subject: [PATCH 06/12] Open continue --- .../middleware/passport/datasource/google.ts | 2 +- .../modals/GoogleDatasourceConfigModal.svelte | 50 ++++++++++++------- .../builder/app/[application]/data/new.svelte | 22 ++++++-- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/packages/backend-core/src/middleware/passport/datasource/google.ts b/packages/backend-core/src/middleware/passport/datasource/google.ts index 3e8306c296..7f5e7f0d90 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.ts +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -88,7 +88,7 @@ export async function postAuth( } ) - ctx.redirect(`${baseUrl}/new?type=google&action=continue&id=${id}`) + ctx.redirect(`${baseUrl}/new?action=google_continue&id=${id}`) } )(ctx, next) } diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte index 6388cfb7b5..79fb0f6b5b 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte @@ -1,43 +1,55 @@ <script> import { ModalContent, Body, Layout, Link } from "@budibase/bbui" - import { IntegrationNames } from "constants/backend" - import cloneDeep from "lodash/cloneDeepWith" + import { IntegrationNames, IntegrationTypes } from "constants/backend" import GoogleButton from "../_components/GoogleButton.svelte" import { organisation } from "stores/portal" import { onMount } from "svelte" - export let integration + export let continueSetup = false - // kill the reference so the input isn't saved - let datasource = cloneDeep(integration) $: isGoogleConfigured = !!$organisation.googleDatasourceConfigured onMount(async () => { await organisation.init() }) + const integrationName = IntegrationNames[IntegrationTypes.GOOGLE_SHEETS] + + export const GoogleDatasouceConfigStep = { + AUTH: "Auth", + SET_URL: "Set_url", + } + + let step = continueSetup + ? GoogleDatasouceConfigStep.SET_URL + : GoogleDatasouceConfigStep.AUTH </script> <ModalContent - title={`Connect to ${IntegrationNames[datasource.type]}`} + title={`Connect to ${integrationName}`} cancelText="Back" size="L" showConfirmButton={false} > - <!-- check true and false directly, don't render until flag is set --> - {#if isGoogleConfigured === true} - <Layout noPadding> + {#if step === GoogleDatasouceConfigStep.AUTH} + <!-- check true and false directly, don't render until flag is set --> + {#if isGoogleConfigured === true} + <Layout noPadding> + <Body size="S" + >Authenticate with your google account to use the {integrationName} integration.</Body + > + </Layout> + <GoogleButton samePage /> + {:else if isGoogleConfigured === false} <Body size="S" - >Authenticate with your google account to use the {IntegrationNames[ - datasource.type - ]} integration.</Body + >Google authentication is not enabled, please complete Google SSO + configuration.</Body > + <Link href="/builder/portal/settings/auth">Configure Google SSO</Link> + {/if} + {/if} + {#if step === GoogleDatasouceConfigStep.SET_URL} + <Layout noPadding> + <Body size="S">Add the URL of the sheet you want to connect</Body> </Layout> - <GoogleButton samePage /> - {:else if isGoogleConfigured === false} - <Body size="S" - >Google authentication is not enabled, please complete Google SSO - configuration.</Body - > - <Link href="/builder/portal/settings/auth">Configure Google SSO</Link> {/if} </ModalContent> diff --git a/packages/builder/src/pages/builder/app/[application]/data/new.svelte b/packages/builder/src/pages/builder/app/[application]/data/new.svelte index fedaf013da..ed2e7f360d 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/new.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/new.svelte @@ -17,6 +17,7 @@ import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte" import ICONS from "components/backend/DatasourceNavigator/icons/index.js" import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte" + import { onMount } from "svelte" let internalTableModal let externalDatasourceModal @@ -24,6 +25,7 @@ let integration = null let disabled = false let promptUpload = false + let continueGoogleSetup $: hasData = $datasources.list.length > 1 || $tables.list.length > 1 $: hasDefaultData = @@ -135,15 +137,29 @@ } $: fetchIntegrations() + + onMount(() => { + const urlParams = new URLSearchParams(window.location.search) + const action = urlParams.get("action") + if (action === "google_continue") { + continueGoogleSetup = true + externalDatasourceModal.show() + } + }) </script> <Modal bind:this={internalTableModal}> <CreateTableModal {promptUpload} afterSave={handleInternalTableSave} /> </Modal> -<Modal bind:this={externalDatasourceModal}> - {#if integration?.auth?.type === "google"} - <GoogleDatasourceConfigModal {integration} /> +<Modal + bind:this={externalDatasourceModal} + on:hide={() => { + continueGoogleSetup = false + }} +> + {#if integration?.auth?.type === "google" || continueGoogleSetup} + <GoogleDatasourceConfigModal continueSetup={continueGoogleSetup} /> {:else} <DatasourceConfigModal {integration} /> {/if} From 1e238ce69376987e13db2ac1fabfe281a0d7df52 Mon Sep 17 00:00:00 2001 From: Adria Navarro <adria@budibase.com> Date: Wed, 31 May 2023 12:26:01 +0200 Subject: [PATCH 07/12] Validate google sheets url --- .../modals/GoogleDatasourceConfigModal.svelte | 40 +++++++++++++++++-- .../builder/app/[application]/data/new.svelte | 33 +++++++++------ .../server/src/integrations/googlesheets.ts | 2 +- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte index 79fb0f6b5b..a0b0902480 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte @@ -4,9 +4,15 @@ import GoogleButton from "../_components/GoogleButton.svelte" import { organisation } from "stores/portal" import { onMount } from "svelte" + import { validateDatasourceConfig } from "builderStore/datasource" + import cloneDeep from "lodash/cloneDeepWith" + import IntegrationConfigForm from "../TableIntegrationMenu/IntegrationConfigForm.svelte" + export let integration export let continueSetup = false + let datasource = cloneDeep(integration) + $: isGoogleConfigured = !!$organisation.googleDatasourceConfigured onMount(async () => { @@ -22,13 +28,32 @@ let step = continueSetup ? GoogleDatasouceConfigStep.SET_URL : GoogleDatasouceConfigStep.AUTH + + let isValid + + const modalConfig = { + [GoogleDatasouceConfigStep.AUTH]: {}, + [GoogleDatasouceConfigStep.SET_URL]: { + confirmButtonText: "Connect", + onConfirm: async () => { + const resp = await validateDatasourceConfig(datasource) + if (!resp.connected) { + displayError(`Unable to connect - ${resp.error}`) + } + + return false + }, + }, + } </script> <ModalContent title={`Connect to ${integrationName}`} - cancelText="Back" + cancelText="Cancel" size="L" - showConfirmButton={false} + confirmText={modalConfig[step].confirmButtonText} + showConfirmButton={!!modalConfig[step].onConfirm} + onConfirm={modalConfig[step].onConfirm} > {#if step === GoogleDatasouceConfigStep.AUTH} <!-- check true and false directly, don't render until flag is set --> @@ -48,8 +73,15 @@ {/if} {/if} {#if step === GoogleDatasouceConfigStep.SET_URL} - <Layout noPadding> - <Body size="S">Add the URL of the sheet you want to connect</Body> + <Layout noPadding no> + <Body size="S">Add the URL of the sheet you want to connect.</Body> + + <IntegrationConfigForm + schema={datasource.schema} + bind:datasource + creating={true} + on:valid={e => (isValid = e.detail)} + /> </Layout> {/if} </ModalContent> diff --git a/packages/builder/src/pages/builder/app/[application]/data/new.svelte b/packages/builder/src/pages/builder/app/[application]/data/new.svelte index ed2e7f360d..536859d4f9 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/new.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/new.svelte @@ -131,21 +131,25 @@ return integrationsArray } - const fetchIntegrations = async () => { - const unsortedIntegrations = await API.getIntegrations() - integrations = sortIntegrations(unsortedIntegrations) - } - - $: fetchIntegrations() - + let isGoogleContinueAction onMount(() => { const urlParams = new URLSearchParams(window.location.search) const action = urlParams.get("action") - if (action === "google_continue") { - continueGoogleSetup = true - externalDatasourceModal.show() - } + + isGoogleContinueAction = action === "google_continue" }) + + const fetchIntegrations = async () => { + const unsortedIntegrations = await API.getIntegrations() + integrations = sortIntegrations(unsortedIntegrations) + console.log(integrations[IntegrationTypes.GOOGLE_SHEETS]) + + if (isGoogleContinueAction) { + handleIntegrationSelect(IntegrationTypes.GOOGLE_SHEETS) + } + } + + $: fetchIntegrations() </script> <Modal bind:this={internalTableModal}> @@ -158,8 +162,11 @@ continueGoogleSetup = false }} > - {#if integration?.auth?.type === "google" || continueGoogleSetup} - <GoogleDatasourceConfigModal continueSetup={continueGoogleSetup} /> + {#if integration?.auth?.type === "google"} + <GoogleDatasourceConfigModal + continueSetup={isGoogleContinueAction} + {integration} + /> {:else} <DatasourceConfigModal {integration} /> {/if} diff --git a/packages/server/src/integrations/googlesheets.ts b/packages/server/src/integrations/googlesheets.ts index 8863aa0b3a..2598f6db62 100644 --- a/packages/server/src/integrations/googlesheets.ts +++ b/packages/server/src/integrations/googlesheets.ts @@ -72,7 +72,7 @@ const SCHEMA: Integration = { }, datasource: { spreadsheetId: { - display: "Google Sheet URL", + display: "Spreadsheet URL", type: DatasourceFieldType.STRING, required: true, }, From 25c921e3406eb653f3acf21fc5a6878bc945a05c Mon Sep 17 00:00:00 2001 From: Adria Navarro <adria@budibase.com> Date: Wed, 31 May 2023 13:00:33 +0200 Subject: [PATCH 08/12] Validate url --- .../src/middleware/passport/datasource/google.ts | 2 +- .../modals/GoogleDatasourceConfigModal.svelte | 15 +++++++++++---- .../builder/app/[application]/data/new.svelte | 13 +++++-------- packages/server/src/api/controllers/datasource.ts | 3 ++- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/backend-core/src/middleware/passport/datasource/google.ts b/packages/backend-core/src/middleware/passport/datasource/google.ts index 7f5e7f0d90..2f91e01d9a 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.ts +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -88,7 +88,7 @@ export async function postAuth( } ) - ctx.redirect(`${baseUrl}/new?action=google_continue&id=${id}`) + ctx.redirect(`${baseUrl}/new?continue_google_setup=${id}`) } )(ctx, next) } diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte index a0b0902480..f93f7b29da 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte @@ -1,5 +1,11 @@ <script> - import { ModalContent, Body, Layout, Link } from "@budibase/bbui" + import { + ModalContent, + Body, + Layout, + Link, + notifications, + } from "@budibase/bbui" import { IntegrationNames, IntegrationTypes } from "constants/backend" import GoogleButton from "../_components/GoogleButton.svelte" import { organisation } from "stores/portal" @@ -9,9 +15,10 @@ import IntegrationConfigForm from "../TableIntegrationMenu/IntegrationConfigForm.svelte" export let integration - export let continueSetup = false + export let continueSetupId = false let datasource = cloneDeep(integration) + datasource.config.continueSetupId = continueSetupId $: isGoogleConfigured = !!$organisation.googleDatasourceConfigured @@ -25,7 +32,7 @@ SET_URL: "Set_url", } - let step = continueSetup + let step = continueSetupId ? GoogleDatasouceConfigStep.SET_URL : GoogleDatasouceConfigStep.AUTH @@ -38,7 +45,7 @@ onConfirm: async () => { const resp = await validateDatasourceConfig(datasource) if (!resp.connected) { - displayError(`Unable to connect - ${resp.error}`) + notifications.error(`Unable to connect - ${resp.error}`) } return false diff --git a/packages/builder/src/pages/builder/app/[application]/data/new.svelte b/packages/builder/src/pages/builder/app/[application]/data/new.svelte index 536859d4f9..23d719247d 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/new.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/new.svelte @@ -25,7 +25,6 @@ let integration = null let disabled = false let promptUpload = false - let continueGoogleSetup $: hasData = $datasources.list.length > 1 || $tables.list.length > 1 $: hasDefaultData = @@ -131,12 +130,10 @@ return integrationsArray } - let isGoogleContinueAction + let continueGoogleSetup onMount(() => { const urlParams = new URLSearchParams(window.location.search) - const action = urlParams.get("action") - - isGoogleContinueAction = action === "google_continue" + continueGoogleSetup = urlParams.get("continue_google_setup") }) const fetchIntegrations = async () => { @@ -144,7 +141,7 @@ integrations = sortIntegrations(unsortedIntegrations) console.log(integrations[IntegrationTypes.GOOGLE_SHEETS]) - if (isGoogleContinueAction) { + if (continueGoogleSetup) { handleIntegrationSelect(IntegrationTypes.GOOGLE_SHEETS) } } @@ -159,12 +156,12 @@ <Modal bind:this={externalDatasourceModal} on:hide={() => { - continueGoogleSetup = false + continueGoogleSetup = null }} > {#if integration?.auth?.type === "google"} <GoogleDatasourceConfigModal - continueSetup={isGoogleContinueAction} + continueSetupId={continueGoogleSetup} {integration} /> {:else} diff --git a/packages/server/src/api/controllers/datasource.ts b/packages/server/src/api/controllers/datasource.ts index 8fe0ab70da..f3d0c5f83b 100644 --- a/packages/server/src/api/controllers/datasource.ts +++ b/packages/server/src/api/controllers/datasource.ts @@ -11,7 +11,7 @@ import { BuildSchemaErrors, InvalidColumns } from "../../constants" import { getIntegration } from "../../integrations" import { getDatasourceAndQuery } from "./row/utils" import { invalidateDynamicVariables } from "../../threads/utils" -import { db as dbCore, context, events } from "@budibase/backend-core" +import { db as dbCore, context, events, cache } from "@budibase/backend-core" import { UserCtx, Datasource, @@ -25,6 +25,7 @@ import { FetchDatasourceInfoResponse, IntegrationBase, DatasourcePlus, + SourceName, } from "@budibase/types" import sdk from "../../sdk" import { builderSocket } from "../../websockets" From 21e870109c82e1c4ae2c7e96c3b5caeb8525c287 Mon Sep 17 00:00:00 2001 From: Adria Navarro <adria@budibase.com> Date: Wed, 31 May 2023 14:29:45 +0200 Subject: [PATCH 09/12] Save datasource --- .../modals/GoogleDatasourceConfigModal.svelte | 26 +++++++++++++++---- .../server/src/api/controllers/datasource.ts | 11 ++++++++ .../server/src/integrations/googlesheets.ts | 18 ++++++++++++- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte index f93f7b29da..7b4808967d 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte @@ -13,6 +13,10 @@ import { validateDatasourceConfig } from "builderStore/datasource" import cloneDeep from "lodash/cloneDeepWith" import IntegrationConfigForm from "../TableIntegrationMenu/IntegrationConfigForm.svelte" + import { goto } from "@roxi/routify" + + import { saveDatasource } from "builderStore/datasource" + import { DatasourceFeature } from "@budibase/types" export let integration export let continueSetupId = false @@ -36,19 +40,30 @@ ? GoogleDatasouceConfigStep.SET_URL : GoogleDatasouceConfigStep.AUTH - let isValid + let isValid = false const modalConfig = { [GoogleDatasouceConfigStep.AUTH]: {}, [GoogleDatasouceConfigStep.SET_URL]: { confirmButtonText: "Connect", onConfirm: async () => { - const resp = await validateDatasourceConfig(datasource) - if (!resp.connected) { - notifications.error(`Unable to connect - ${resp.error}`) + if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) { + const resp = await validateDatasourceConfig(datasource) + if (!resp.connected) { + notifications.error(`Unable to connect - ${resp.error}`) + return false + } } - return false + try { + const resp = await saveDatasource(datasource) + $goto(`./datasource/${resp._id}`) + notifications.success(`Datasource created successfully.`) + } catch (err) { + notifications.error(err?.message ?? "Error saving datasource") + // prevent the modal from closing + return false + } }, }, } @@ -61,6 +76,7 @@ confirmText={modalConfig[step].confirmButtonText} showConfirmButton={!!modalConfig[step].onConfirm} onConfirm={modalConfig[step].onConfirm} + disabled={!isValid} > {#if step === GoogleDatasouceConfigStep.AUTH} <!-- check true and false directly, don't render until flag is set --> diff --git a/packages/server/src/api/controllers/datasource.ts b/packages/server/src/api/controllers/datasource.ts index f3d0c5f83b..d21db8ad03 100644 --- a/packages/server/src/api/controllers/datasource.ts +++ b/packages/server/src/api/controllers/datasource.ts @@ -29,6 +29,7 @@ import { } from "@budibase/types" import sdk from "../../sdk" import { builderSocket } from "../../websockets" +import { setupCreationAuth as googleSetupCreationAuth } from "src/integrations/googlesheets" function getErrorTables(errors: any, errorType: string) { return Object.entries(errors) @@ -307,6 +308,12 @@ export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) { builderSocket?.emitDatasourceUpdate(ctx, datasource) } +const preSaveAction: Partial<Record<SourceName, any>> = { + [SourceName.GOOGLE_SHEETS]: async (datasource: Datasource) => { + await googleSetupCreationAuth(datasource.config as any) + }, +} + export async function save( ctx: UserCtx<CreateDatasourceRequest, CreateDatasourceResponse> ) { @@ -328,6 +335,10 @@ export async function save( setDefaultDisplayColumns(datasource) } + if (preSaveAction[datasource.source]) { + await preSaveAction[datasource.source](datasource) + } + const dbResp = await db.put(datasource) await events.datasource.created(datasource) datasource._rev = dbResp.rev diff --git a/packages/server/src/integrations/googlesheets.ts b/packages/server/src/integrations/googlesheets.ts index 2598f6db62..a792f49b57 100644 --- a/packages/server/src/integrations/googlesheets.ts +++ b/packages/server/src/integrations/googlesheets.ts @@ -1,5 +1,6 @@ import { ConnectionInfo, + Datasource, DatasourceFeature, DatasourceFieldType, DatasourcePlus, @@ -19,13 +20,15 @@ import { OAuth2Client } from "google-auth-library" import { buildExternalTableId, finaliseExternalTables } from "./utils" import { GoogleSpreadsheet, GoogleSpreadsheetRow } from "google-spreadsheet" import fetch from "node-fetch" -import { configs, HTTPError } from "@budibase/backend-core" +import { cache, configs, context, HTTPError } from "@budibase/backend-core" import { dataFilters } from "@budibase/shared-core" import { GOOGLE_SHEETS_PRIMARY_KEY } from "../constants" +import sdk from "../sdk" interface GoogleSheetsConfig { spreadsheetId: string auth: OAuthClientConfig + continueSetupId?: string } interface OAuthClientConfig { @@ -147,6 +150,7 @@ class GoogleSheetsIntegration implements DatasourcePlus { async testConnection(): Promise<ConnectionInfo> { try { + await setupCreationAuth(this.config) await this.connect() return { connected: true } } catch (e: any) { @@ -566,6 +570,18 @@ class GoogleSheetsIntegration implements DatasourcePlus { } } +export async function setupCreationAuth(datasouce: GoogleSheetsConfig) { + if (datasouce.continueSetupId) { + const appId = context.getAppId() + const tokens = await cache.get( + `datasource:creation:${appId}:google:${datasouce.continueSetupId}` + ) + + datasouce.auth = tokens.tokens + delete datasouce.continueSetupId + } +} + export default { schema: SCHEMA, integration: GoogleSheetsIntegration, From 4247b4425ca72fa9e30a33cb33d6b70c264c0301 Mon Sep 17 00:00:00 2001 From: Adria Navarro <adria@budibase.com> Date: Thu, 1 Jun 2023 07:32:04 +0100 Subject: [PATCH 10/12] Clean code --- .../builder/src/pages/builder/app/[application]/data/new.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/builder/src/pages/builder/app/[application]/data/new.svelte b/packages/builder/src/pages/builder/app/[application]/data/new.svelte index 23d719247d..8ff974112b 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/new.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/new.svelte @@ -139,7 +139,6 @@ const fetchIntegrations = async () => { const unsortedIntegrations = await API.getIntegrations() integrations = sortIntegrations(unsortedIntegrations) - console.log(integrations[IntegrationTypes.GOOGLE_SHEETS]) if (continueGoogleSetup) { handleIntegrationSelect(IntegrationTypes.GOOGLE_SHEETS) From c79b394907f4c31e1631a2de556318ffe7257cb0 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 6 Jun 2023 10:42:24 +0000 Subject: [PATCH 11/12] Bump version to 2.6.24-alpha.2 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index e44a4b60c7..058b0d2e96 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.6.24-alpha.1", + "version": "2.6.24-alpha.2", "npmClient": "yarn", "packages": [ "packages/backend-core", From 57a41e49bfdf0df24b0d61a1c4148847d2910278 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Tue, 6 Jun 2023 10:57:28 +0000 Subject: [PATCH 12/12] Bump version to 2.6.28-alpha.0 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 1352dd768e..ee9375e919 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.6.27", + "version": "2.6.28-alpha.0", "npmClient": "yarn", "packages": [ "packages/backend-core",