Merge branch 'develop' into master
This commit is contained in:
commit
6f29c978e0
|
@ -1,10 +1,10 @@
|
||||||
import * as google from "../sso/google"
|
import * as google from "../sso/google"
|
||||||
import { Cookie } from "../../../constants"
|
import { Cookie } from "../../../constants"
|
||||||
import { clearCookie, getCookie } from "../../../utils"
|
import { clearCookie, getCookie } from "../../../utils"
|
||||||
import { doWithDB } from "../../../db"
|
|
||||||
import * as configs from "../../../configs"
|
import * as configs from "../../../configs"
|
||||||
import { BBContext, Database, SSOProfile } from "@budibase/types"
|
import { BBContext, SSOProfile } from "@budibase/types"
|
||||||
import { ssoSaveUserNoOp } from "../sso/sso"
|
import { ssoSaveUserNoOp } from "../sso/sso"
|
||||||
|
import { cache, utils } from "../../../"
|
||||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
|
||||||
|
|
||||||
type Passport = {
|
type Passport = {
|
||||||
|
@ -36,8 +36,8 @@ export async function preAuth(
|
||||||
ssoSaveUserNoOp
|
ssoSaveUserNoOp
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!ctx.query.appId || !ctx.query.datasourceId) {
|
if (!ctx.query.appId) {
|
||||||
ctx.throw(400, "appId and datasourceId query params not present.")
|
ctx.throw(400, "appId query param not present.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return passport.authenticate(strategy, {
|
return passport.authenticate(strategy, {
|
||||||
|
@ -69,7 +69,7 @@ export async function postAuth(
|
||||||
(
|
(
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
profile: SSOProfile,
|
_profile: SSOProfile,
|
||||||
done: Function
|
done: Function
|
||||||
) => {
|
) => {
|
||||||
clearCookie(ctx, Cookie.DatasourceAuth)
|
clearCookie(ctx, Cookie.DatasourceAuth)
|
||||||
|
@ -79,23 +79,16 @@ export async function postAuth(
|
||||||
{ successRedirect: "/", failureRedirect: "/error" },
|
{ successRedirect: "/", failureRedirect: "/error" },
|
||||||
async (err: any, tokens: string[]) => {
|
async (err: any, tokens: string[]) => {
|
||||||
const baseUrl = `/builder/app/${authStateCookie.appId}/data`
|
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) => {
|
const id = utils.newid()
|
||||||
let datasource
|
await cache.store(
|
||||||
try {
|
`datasource:creation:${authStateCookie.appId}:google:${id}`,
|
||||||
datasource = await db.get(authStateCookie.datasourceId)
|
{
|
||||||
} catch (err: any) {
|
tokens,
|
||||||
if (err.status === 404) {
|
|
||||||
ctx.redirect(baseUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!datasource.config) {
|
)
|
||||||
datasource.config = {}
|
|
||||||
}
|
ctx.redirect(`${baseUrl}/new?continue_google_setup=${id}`)
|
||||||
datasource.config.auth = { type: "google", ...tokens }
|
|
||||||
await db.put(datasource)
|
|
||||||
ctx.redirect(`${baseUrl}/datasource/${authStateCookie.datasourceId}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
)(ctx, next)
|
)(ctx, next)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
export let preAuthStep
|
|
||||||
export let datasource
|
|
||||||
export let disabled
|
export let disabled
|
||||||
export let samePage
|
export let samePage
|
||||||
|
|
||||||
|
@ -15,18 +13,8 @@
|
||||||
class:disabled
|
class:disabled
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
let ds = datasource
|
|
||||||
let appId = $store.appId
|
let appId = $store.appId
|
||||||
if (!ds) {
|
const url = `/api/global/auth/${tenantId}/datasource/google?appId=${appId}`
|
||||||
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) {
|
if (samePage) {
|
||||||
window.location = url
|
window.location = url
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,43 +1,110 @@
|
||||||
<script>
|
<script>
|
||||||
import { ModalContent, Body, Layout, Link } from "@budibase/bbui"
|
import {
|
||||||
import { IntegrationNames } from "constants/backend"
|
ModalContent,
|
||||||
import cloneDeep from "lodash/cloneDeepWith"
|
Body,
|
||||||
|
Layout,
|
||||||
|
Link,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
||||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||||
import { saveDatasource as save } from "builderStore/datasource"
|
|
||||||
import { organisation } from "stores/portal"
|
import { organisation } from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
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 integration
|
||||||
|
export let continueSetupId = false
|
||||||
|
|
||||||
// kill the reference so the input isn't saved
|
|
||||||
let datasource = cloneDeep(integration)
|
let datasource = cloneDeep(integration)
|
||||||
|
datasource.config.continueSetupId = continueSetupId
|
||||||
|
|
||||||
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await organisation.init()
|
await organisation.init()
|
||||||
})
|
})
|
||||||
|
const integrationName = IntegrationNames[IntegrationTypes.GOOGLE_SHEETS]
|
||||||
|
|
||||||
|
export const GoogleDatasouceConfigStep = {
|
||||||
|
AUTH: "Auth",
|
||||||
|
SET_URL: "Set_url",
|
||||||
|
}
|
||||||
|
|
||||||
|
let step = continueSetupId
|
||||||
|
? GoogleDatasouceConfigStep.SET_URL
|
||||||
|
: GoogleDatasouceConfigStep.AUTH
|
||||||
|
|
||||||
|
let isValid = false
|
||||||
|
|
||||||
|
const modalConfig = {
|
||||||
|
[GoogleDatasouceConfigStep.AUTH]: {},
|
||||||
|
[GoogleDatasouceConfigStep.SET_URL]: {
|
||||||
|
confirmButtonText: "Connect",
|
||||||
|
onConfirm: async () => {
|
||||||
|
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||||
|
const resp = await validateDatasourceConfig(datasource)
|
||||||
|
if (!resp.connected) {
|
||||||
|
notifications.error(`Unable to connect - ${resp.error}`)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={`Connect to ${IntegrationNames[datasource.type]}`}
|
title={`Connect to ${integrationName}`}
|
||||||
cancelText="Back"
|
cancelText="Cancel"
|
||||||
size="L"
|
size="L"
|
||||||
|
confirmText={modalConfig[step].confirmButtonText}
|
||||||
|
showConfirmButton={!!modalConfig[step].onConfirm}
|
||||||
|
onConfirm={modalConfig[step].onConfirm}
|
||||||
|
disabled={!isValid}
|
||||||
>
|
>
|
||||||
<!-- check true and false directly, don't render until flag is set -->
|
{#if step === GoogleDatasouceConfigStep.AUTH}
|
||||||
{#if isGoogleConfigured === true}
|
<!-- check true and false directly, don't render until flag is set -->
|
||||||
<Layout noPadding>
|
{#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"
|
<Body size="S"
|
||||||
>Authenticate with your google account to use the {IntegrationNames[
|
>Google authentication is not enabled, please complete Google SSO
|
||||||
datasource.type
|
configuration.</Body
|
||||||
]} integration.</Body
|
|
||||||
>
|
>
|
||||||
|
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if step === GoogleDatasouceConfigStep.SET_URL}
|
||||||
|
<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>
|
</Layout>
|
||||||
<GoogleButton preAuthStep={() => save(datasource, true)} />
|
|
||||||
{: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}
|
{/if}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||||
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
||||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
let internalTableModal
|
let internalTableModal
|
||||||
let externalDatasourceModal
|
let externalDatasourceModal
|
||||||
|
@ -129,9 +130,19 @@
|
||||||
return integrationsArray
|
return integrationsArray
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let continueGoogleSetup
|
||||||
|
onMount(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
continueGoogleSetup = urlParams.get("continue_google_setup")
|
||||||
|
})
|
||||||
|
|
||||||
const fetchIntegrations = async () => {
|
const fetchIntegrations = async () => {
|
||||||
const unsortedIntegrations = await API.getIntegrations()
|
const unsortedIntegrations = await API.getIntegrations()
|
||||||
integrations = sortIntegrations(unsortedIntegrations)
|
integrations = sortIntegrations(unsortedIntegrations)
|
||||||
|
|
||||||
|
if (continueGoogleSetup) {
|
||||||
|
handleIntegrationSelect(IntegrationTypes.GOOGLE_SHEETS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: fetchIntegrations()
|
$: fetchIntegrations()
|
||||||
|
@ -141,9 +152,17 @@
|
||||||
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
|
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={externalDatasourceModal}>
|
<Modal
|
||||||
|
bind:this={externalDatasourceModal}
|
||||||
|
on:hide={() => {
|
||||||
|
continueGoogleSetup = null
|
||||||
|
}}
|
||||||
|
>
|
||||||
{#if integration?.auth?.type === "google"}
|
{#if integration?.auth?.type === "google"}
|
||||||
<GoogleDatasourceConfigModal {integration} />
|
<GoogleDatasourceConfigModal
|
||||||
|
continueSetupId={continueGoogleSetup}
|
||||||
|
{integration}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<DatasourceConfigModal {integration} />
|
<DatasourceConfigModal {integration} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -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>
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import GoogleLogo from "./_logos/Google.svelte"
|
|
||||||
import OidcLogo from "./_logos/OIDC.svelte"
|
import OidcLogo from "./_logos/OIDC.svelte"
|
||||||
import MicrosoftLogo from "assets/microsoft-logo.png"
|
import MicrosoftLogo from "assets/microsoft-logo.png"
|
||||||
import Auth0Logo from "assets/auth0-logo.png"
|
import Auth0Logo from "assets/auth0-logo.png"
|
||||||
|
@ -28,9 +27,9 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { organisation, admin, licensing } from "stores/portal"
|
import { organisation, admin, licensing } from "stores/portal"
|
||||||
import Scim from "./scim.svelte"
|
import Scim from "./scim.svelte"
|
||||||
|
import Google from "./google.svelte"
|
||||||
|
|
||||||
const ConfigTypes = {
|
const ConfigTypes = {
|
||||||
Google: "google",
|
|
||||||
OIDC: "oidc",
|
OIDC: "oidc",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,43 +37,6 @@
|
||||||
|
|
||||||
$: enforcedSSO = $organisation.isSSOEnforced
|
$: 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 = {
|
$: OIDCConfigFields = {
|
||||||
Oidc: [
|
Oidc: [
|
||||||
{ name: "configUrl", label: "Config URL" },
|
{ name: "configUrl", label: "Config URL" },
|
||||||
|
@ -133,15 +95,9 @@
|
||||||
const providers = { google, oidc }
|
const providers = { google, oidc }
|
||||||
|
|
||||||
// control the state of the save button depending on whether form has changed
|
// control the state of the save button depending on whether form has changed
|
||||||
let originalGoogleDoc
|
|
||||||
let originalOidcDoc
|
let originalOidcDoc
|
||||||
let googleSaveButtonDisabled
|
|
||||||
let oidcSaveButtonDisabled
|
let oidcSaveButtonDisabled
|
||||||
$: {
|
$: {
|
||||||
isEqual(providers.google?.config, originalGoogleDoc?.config)
|
|
||||||
? (googleSaveButtonDisabled = true)
|
|
||||||
: (googleSaveButtonDisabled = false)
|
|
||||||
|
|
||||||
// delete the callback url which is never saved to the oidc
|
// delete the callback url which is never saved to the oidc
|
||||||
// config doc, to ensure an accurate comparison
|
// config doc, to ensure an accurate comparison
|
||||||
delete providers.oidc?.config.configs[0].callbackURL
|
delete providers.oidc?.config.configs[0].callbackURL
|
||||||
|
@ -151,10 +107,6 @@
|
||||||
: (oidcSaveButtonDisabled = false)
|
: (oidcSaveButtonDisabled = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: googleComplete = !!(
|
|
||||||
providers.google?.config?.clientID && providers.google?.config?.clientSecret
|
|
||||||
)
|
|
||||||
|
|
||||||
$: oidcComplete = !!(
|
$: oidcComplete = !!(
|
||||||
providers.oidc?.config?.configs[0].configUrl &&
|
providers.oidc?.config?.configs[0].configUrl &&
|
||||||
providers.oidc?.config?.configs[0].clientID &&
|
providers.oidc?.config?.configs[0].clientID &&
|
||||||
|
@ -230,30 +182,6 @@
|
||||||
originalOidcDoc = cloneDeep(providers.oidc)
|
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"]
|
let defaultScopes = ["profile", "email", "offline_access"]
|
||||||
|
|
||||||
const refreshScopes = idx => {
|
const refreshScopes = idx => {
|
||||||
|
@ -281,29 +209,6 @@
|
||||||
notifications.error("Error getting org config")
|
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.
|
// 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
|
// This needs to be done before the config call so they're available when
|
||||||
// the dropdown renders.
|
// the dropdown renders.
|
||||||
|
@ -395,62 +300,7 @@
|
||||||
> before enabling this feature.
|
> before enabling this feature.
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
{#if providers.google}
|
<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}
|
|
||||||
{#if providers.oidc}
|
{#if providers.oidc}
|
||||||
<Divider />
|
<Divider />
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { BuildSchemaErrors, InvalidColumns } from "../../constants"
|
||||||
import { getIntegration } from "../../integrations"
|
import { getIntegration } from "../../integrations"
|
||||||
import { getDatasourceAndQuery } from "./row/utils"
|
import { getDatasourceAndQuery } from "./row/utils"
|
||||||
import { invalidateDynamicVariables } from "../../threads/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 {
|
import {
|
||||||
UserCtx,
|
UserCtx,
|
||||||
Datasource,
|
Datasource,
|
||||||
|
@ -25,9 +25,11 @@ import {
|
||||||
FetchDatasourceInfoResponse,
|
FetchDatasourceInfoResponse,
|
||||||
IntegrationBase,
|
IntegrationBase,
|
||||||
DatasourcePlus,
|
DatasourcePlus,
|
||||||
|
SourceName,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
import { builderSocket } from "../../websockets"
|
import { builderSocket } from "../../websockets"
|
||||||
|
import { setupCreationAuth as googleSetupCreationAuth } from "src/integrations/googlesheets"
|
||||||
|
|
||||||
function getErrorTables(errors: any, errorType: string) {
|
function getErrorTables(errors: any, errorType: string) {
|
||||||
return Object.entries(errors)
|
return Object.entries(errors)
|
||||||
|
@ -306,6 +308,12 @@ export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) {
|
||||||
builderSocket?.emitDatasourceUpdate(ctx, datasource)
|
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(
|
export async function save(
|
||||||
ctx: UserCtx<CreateDatasourceRequest, CreateDatasourceResponse>
|
ctx: UserCtx<CreateDatasourceRequest, CreateDatasourceResponse>
|
||||||
) {
|
) {
|
||||||
|
@ -327,6 +335,10 @@ export async function save(
|
||||||
setDefaultDisplayColumns(datasource)
|
setDefaultDisplayColumns(datasource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preSaveAction[datasource.source]) {
|
||||||
|
await preSaveAction[datasource.source](datasource)
|
||||||
|
}
|
||||||
|
|
||||||
const dbResp = await db.put(datasource)
|
const dbResp = await db.put(datasource)
|
||||||
await events.datasource.created(datasource)
|
await events.datasource.created(datasource)
|
||||||
datasource._rev = dbResp.rev
|
datasource._rev = dbResp.rev
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
ConnectionInfo,
|
ConnectionInfo,
|
||||||
|
Datasource,
|
||||||
DatasourceFeature,
|
DatasourceFeature,
|
||||||
DatasourceFieldType,
|
DatasourceFieldType,
|
||||||
DatasourcePlus,
|
DatasourcePlus,
|
||||||
|
@ -19,13 +20,15 @@ import { OAuth2Client } from "google-auth-library"
|
||||||
import { buildExternalTableId, finaliseExternalTables } from "./utils"
|
import { buildExternalTableId, finaliseExternalTables } from "./utils"
|
||||||
import { GoogleSpreadsheet, GoogleSpreadsheetRow } from "google-spreadsheet"
|
import { GoogleSpreadsheet, GoogleSpreadsheetRow } from "google-spreadsheet"
|
||||||
import fetch from "node-fetch"
|
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 { dataFilters } from "@budibase/shared-core"
|
||||||
import { GOOGLE_SHEETS_PRIMARY_KEY } from "../constants"
|
import { GOOGLE_SHEETS_PRIMARY_KEY } from "../constants"
|
||||||
|
import sdk from "../sdk"
|
||||||
|
|
||||||
interface GoogleSheetsConfig {
|
interface GoogleSheetsConfig {
|
||||||
spreadsheetId: string
|
spreadsheetId: string
|
||||||
auth: OAuthClientConfig
|
auth: OAuthClientConfig
|
||||||
|
continueSetupId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OAuthClientConfig {
|
interface OAuthClientConfig {
|
||||||
|
@ -72,7 +75,7 @@ const SCHEMA: Integration = {
|
||||||
},
|
},
|
||||||
datasource: {
|
datasource: {
|
||||||
spreadsheetId: {
|
spreadsheetId: {
|
||||||
display: "Google Sheet URL",
|
display: "Spreadsheet URL",
|
||||||
type: DatasourceFieldType.STRING,
|
type: DatasourceFieldType.STRING,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
@ -147,6 +150,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
|
||||||
|
|
||||||
async testConnection(): Promise<ConnectionInfo> {
|
async testConnection(): Promise<ConnectionInfo> {
|
||||||
try {
|
try {
|
||||||
|
await setupCreationAuth(this.config)
|
||||||
await this.connect()
|
await this.connect()
|
||||||
return { connected: true }
|
return { connected: true }
|
||||||
} catch (e: any) {
|
} 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 {
|
export default {
|
||||||
schema: SCHEMA,
|
schema: SCHEMA,
|
||||||
integration: GoogleSheetsIntegration,
|
integration: GoogleSheetsIntegration,
|
||||||
|
|
|
@ -140,7 +140,6 @@ export const datasourcePreAuth = async (ctx: any, next: any) => {
|
||||||
{
|
{
|
||||||
provider,
|
provider,
|
||||||
appId: ctx.query.appId,
|
appId: ctx.query.appId,
|
||||||
datasourceId: ctx.query.datasourceId,
|
|
||||||
},
|
},
|
||||||
Cookie.DatasourceAuth
|
Cookie.DatasourceAuth
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue