Merge branch 'feature/sql-attachments' of github.com:Budibase/budibase into fix/ts-convert-create-edit-column
This commit is contained in:
commit
6a2329e10a
|
@ -186,7 +186,7 @@ jobs:
|
||||||
id: dotenv
|
id: dotenv
|
||||||
uses: falti/dotenv-action@v1.1.3
|
uses: falti/dotenv-action@v1.1.3
|
||||||
with:
|
with:
|
||||||
path: ./packages/server/datasource-sha.env
|
path: ./packages/server/images-sha.env
|
||||||
|
|
||||||
- name: Pull testcontainers images
|
- name: Pull testcontainers images
|
||||||
run: |
|
run: |
|
||||||
|
@ -213,6 +213,7 @@ jobs:
|
||||||
docker pull redis &
|
docker pull redis &
|
||||||
docker pull testcontainers/ryuk:0.5.1 &
|
docker pull testcontainers/ryuk:0.5.1 &
|
||||||
docker pull budibase/couchdb:v3.3.3-sqs-v2.1.1 &
|
docker pull budibase/couchdb:v3.3.3-sqs-v2.1.1 &
|
||||||
|
docker pull ${{ steps.dotenv.outputs.KEYCLOAK_IMAGE }} &
|
||||||
|
|
||||||
wait $(jobs -p)
|
wait $(jobs -p)
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,9 @@ export default [
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
allowImportExportEverywhere: true,
|
allowImportExportEverywhere: true,
|
||||||
|
svelteFeatures: {
|
||||||
|
experimentalGenerics: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { setContext, getContext } from "svelte"
|
import { setContext, getContext } from "svelte"
|
||||||
import Popover from "../Popover/Popover.svelte"
|
import Popover from "../Popover/Popover.svelte"
|
||||||
import Menu from "../Menu/Menu.svelte"
|
import Menu from "../Menu/Menu.svelte"
|
||||||
|
import type { PopoverAlignment } from "../constants"
|
||||||
|
|
||||||
export let disabled = false
|
export let disabled: boolean = false
|
||||||
export let align = "left"
|
export let align: `${PopoverAlignment}` = "left"
|
||||||
export let portalTarget = undefined
|
export let portalTarget: string | undefined = undefined
|
||||||
export let openOnHover = false
|
export let openOnHover: boolean = false
|
||||||
export let animate = true
|
export let animate: boolean | undefined = true
|
||||||
export let offset = undefined
|
export let offset: number | undefined = undefined
|
||||||
|
|
||||||
const actionMenuContext = getContext("actionMenu")
|
const actionMenuContext = getContext("actionMenu")
|
||||||
|
|
||||||
let anchor
|
let anchor: HTMLElement | undefined
|
||||||
let dropdown
|
let dropdown: Popover
|
||||||
let timeout
|
let timeout: ReturnType<typeof setTimeout>
|
||||||
|
|
||||||
// This is needed because display: contents is considered "invisible".
|
// This is needed because display: contents is considered "invisible".
|
||||||
// It should only ever be an action button, so should be fine.
|
// It should only ever be an action button, so should be fine.
|
||||||
function getAnchor(node) {
|
function getAnchor(node: HTMLDivElement) {
|
||||||
anchor = node.firstChild
|
anchor = (node.firstChild as HTMLElement) ?? undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export const show = () => {
|
export const show = () => {
|
||||||
|
@ -37,7 +38,7 @@
|
||||||
actionMenuContext?.hide()
|
actionMenuContext?.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
const openMenu = event => {
|
const openMenu = (event: Event) => {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
show()
|
show()
|
||||||
|
|
|
@ -89,7 +89,7 @@
|
||||||
/* Selection is only meant for standalone list items (non stacked) so we just set a fixed border radius */
|
/* Selection is only meant for standalone list items (non stacked) so we just set a fixed border radius */
|
||||||
.list-item.selected {
|
.list-item.selected {
|
||||||
background-color: var(--spectrum-global-color-blue-100);
|
background-color: var(--spectrum-global-color-blue-100);
|
||||||
border-color: var(--spectrum-global-color-blue-100);
|
border: none;
|
||||||
}
|
}
|
||||||
.list-item.selected:after {
|
.list-item.selected:after {
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
border-radius: 4px;
|
border-radius: inherit;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { ActionMenu } from "./types"
|
||||||
|
|
||||||
|
declare module "svelte" {
|
||||||
|
export function getContext(key: "actionMenu"): ActionMenu | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Modal = "bbui-modal"
|
||||||
|
export const PopoverRoot = "bbui-popover-root"
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface ActionMenu {
|
||||||
|
hide: () => void
|
||||||
|
}
|
|
@ -1,16 +1,18 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
import { Modal, ModalContent, Body } from "@budibase/bbui"
|
||||||
|
|
||||||
export let title = ""
|
export let title: string = ""
|
||||||
export let body = ""
|
export let body: string = ""
|
||||||
export let okText = "Confirm"
|
export let okText: string = "Confirm"
|
||||||
export let cancelText = "Cancel"
|
export let cancelText: string = "Cancel"
|
||||||
export let onOk = undefined
|
export let size: "S" | "M" | "L" | "XL" | undefined = undefined
|
||||||
export let onCancel = undefined
|
export let onOk: (() => void) | undefined = undefined
|
||||||
export let warning = true
|
export let onCancel: (() => void) | undefined = undefined
|
||||||
export let disabled = false
|
export let onClose: (() => void) | undefined = undefined
|
||||||
|
export let warning: boolean = true
|
||||||
|
export let disabled: boolean = false
|
||||||
|
|
||||||
let modal
|
let modal: Modal
|
||||||
|
|
||||||
export const show = () => {
|
export const show = () => {
|
||||||
modal.show()
|
modal.show()
|
||||||
|
@ -20,14 +22,16 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={modal} on:hide={onCancel}>
|
<Modal bind:this={modal} on:hide={onClose ?? onCancel}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
onConfirm={onOk}
|
onConfirm={onOk}
|
||||||
|
{onCancel}
|
||||||
{title}
|
{title}
|
||||||
confirmText={okText}
|
confirmText={okText}
|
||||||
{cancelText}
|
{cancelText}
|
||||||
{warning}
|
{warning}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
{size}
|
||||||
>
|
>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
{body}
|
{body}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, params } from "@roxi/routify"
|
import { beforeUrlChange, goto, params } from "@roxi/routify"
|
||||||
import { datasources, flags, integrations, queries } from "@/stores/builder"
|
import { datasources, flags, integrations, queries } from "@/stores/builder"
|
||||||
import { environment } from "@/stores/portal"
|
import { environment } from "@/stores/portal"
|
||||||
import {
|
import {
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
EditorModes,
|
EditorModes,
|
||||||
} from "@/components/common/CodeMirrorEditor.svelte"
|
} from "@/components/common/CodeMirrorEditor.svelte"
|
||||||
import RestBodyInput from "./RestBodyInput.svelte"
|
import RestBodyInput from "./RestBodyInput.svelte"
|
||||||
import { capitalise } from "@/helpers"
|
import { capitalise, confirm } from "@/helpers"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import restUtils from "@/helpers/data/utils"
|
import restUtils from "@/helpers/data/utils"
|
||||||
import {
|
import {
|
||||||
|
@ -50,6 +50,7 @@
|
||||||
toBindingsArray,
|
toBindingsArray,
|
||||||
} from "@/dataBinding"
|
} from "@/dataBinding"
|
||||||
import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
|
import ConnectedQueryScreens from "./ConnectedQueryScreens.svelte"
|
||||||
|
import AuthPicker from "./rest/AuthPicker.svelte"
|
||||||
|
|
||||||
export let queryId
|
export let queryId
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@
|
||||||
let nestedSchemaFields = {}
|
let nestedSchemaFields = {}
|
||||||
let saving
|
let saving
|
||||||
let queryNameLabel
|
let queryNameLabel
|
||||||
|
let mounted = false
|
||||||
|
|
||||||
$: staticVariables = datasource?.config?.staticVariables || {}
|
$: staticVariables = datasource?.config?.staticVariables || {}
|
||||||
|
|
||||||
|
@ -104,8 +106,10 @@
|
||||||
|
|
||||||
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
|
$: runtimeUrlQueries = readableToRuntimeMap(mergedBindings, breakQs)
|
||||||
|
|
||||||
$: originalQuery = originalQuery ?? cloneDeep(query)
|
|
||||||
$: builtQuery = buildQuery(query, runtimeUrlQueries, requestBindings)
|
$: builtQuery = buildQuery(query, runtimeUrlQueries, requestBindings)
|
||||||
|
$: originalQuery = mounted
|
||||||
|
? originalQuery ?? cloneDeep(builtQuery)
|
||||||
|
: undefined
|
||||||
$: isModified = JSON.stringify(originalQuery) !== JSON.stringify(builtQuery)
|
$: isModified = JSON.stringify(originalQuery) !== JSON.stringify(builtQuery)
|
||||||
|
|
||||||
function getSelectedQuery() {
|
function getSelectedQuery() {
|
||||||
|
@ -208,11 +212,14 @@
|
||||||
originalQuery = null
|
originalQuery = null
|
||||||
|
|
||||||
queryNameLabel.disableEditingState()
|
queryNameLabel.disableEditingState()
|
||||||
|
return { ok: true }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error saving query`)
|
notifications.error(`Error saving query`)
|
||||||
} finally {
|
} finally {
|
||||||
saving = false
|
saving = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { ok: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateQuery = async () => {
|
const validateQuery = async () => {
|
||||||
|
@ -474,6 +481,38 @@
|
||||||
staticVariables,
|
staticVariables,
|
||||||
restBindings
|
restBindings
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mounted = true
|
||||||
|
})
|
||||||
|
|
||||||
|
$beforeUrlChange(async () => {
|
||||||
|
if (!isModified) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return await confirm({
|
||||||
|
title: "Some updates are not saved",
|
||||||
|
body: "Some of your changes are not yet saved. Do you want to save them before leaving?",
|
||||||
|
okText: "Save and continue",
|
||||||
|
cancelText: "Discard and continue",
|
||||||
|
size: "M",
|
||||||
|
onConfirm: async () => {
|
||||||
|
const saveResult = await saveQuery()
|
||||||
|
if (!saveResult.ok) {
|
||||||
|
// We can't leave as the query was not properly saved
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
// Leave without saving anything
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -642,16 +681,13 @@
|
||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
<div />
|
<div />
|
||||||
<!-- spacer -->
|
<!-- spacer -->
|
||||||
<div class="auth-select">
|
|
||||||
<Select
|
<AuthPicker
|
||||||
label="Auth"
|
bind:authConfigId={query.fields.authConfigId}
|
||||||
labelPosition="left"
|
{authConfigs}
|
||||||
placeholder="None"
|
datasourceId={datasource._id}
|
||||||
bind:value={query.fields.authConfigId}
|
|
||||||
options={authConfigs}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
@ -853,10 +889,6 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-select {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
.pagination {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
ActionButton,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
PopoverAlignment,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { appStore } from "@/stores/builder"
|
||||||
|
import DetailPopover from "@/components/common/DetailPopover.svelte"
|
||||||
|
|
||||||
|
export let authConfigId: string | undefined
|
||||||
|
export let authConfigs: { label: string; value: string }[]
|
||||||
|
export let datasourceId: string
|
||||||
|
|
||||||
|
let popover: DetailPopover
|
||||||
|
|
||||||
|
$: authConfig = authConfigs.find(c => c.value === authConfigId)
|
||||||
|
|
||||||
|
function addBasicConfiguration() {
|
||||||
|
$goto(
|
||||||
|
`/builder/app/${$appStore.appId}/data/datasource/${datasourceId}?&tab=Authentication`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectConfiguration(id: string) {
|
||||||
|
if (authConfigId === id) {
|
||||||
|
authConfigId = undefined
|
||||||
|
} else {
|
||||||
|
authConfigId = id
|
||||||
|
}
|
||||||
|
popover.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
$: title = !authConfig ? "Authentication" : `Auth: ${authConfig.label}`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DetailPopover bind:this={popover} {title} align={PopoverAlignment.Right}>
|
||||||
|
<div slot="anchor">
|
||||||
|
<ActionButton icon="LockClosed" quiet selected>
|
||||||
|
{#if !authConfig}
|
||||||
|
Authentication
|
||||||
|
{:else}
|
||||||
|
Auth: {authConfig.label}
|
||||||
|
{/if}
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Body size="S" color="var(--spectrum-global-color-gray-700)">
|
||||||
|
Basic & Bearer Authentication
|
||||||
|
</Body>
|
||||||
|
|
||||||
|
{#if authConfigs.length}
|
||||||
|
<List>
|
||||||
|
{#each authConfigs as config}
|
||||||
|
<ListItem
|
||||||
|
title={config.label}
|
||||||
|
on:click={() => selectConfiguration(config.value)}
|
||||||
|
selected={config.value === authConfigId}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</List>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<Button secondary icon="Add" on:click={addBasicConfiguration}
|
||||||
|
>Add config</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</DetailPopover>
|
|
@ -0,0 +1,41 @@
|
||||||
|
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
|
export enum ConfirmOutput {}
|
||||||
|
|
||||||
|
export async function confirm(props: {
|
||||||
|
title: string
|
||||||
|
body?: string
|
||||||
|
okText?: string
|
||||||
|
cancelText?: string
|
||||||
|
size?: "S" | "M" | "L" | "XL"
|
||||||
|
onConfirm?: () => void
|
||||||
|
onCancel?: () => void
|
||||||
|
onClose?: () => void
|
||||||
|
}) {
|
||||||
|
return await new Promise(resolve => {
|
||||||
|
const dialog = new ConfirmDialog({
|
||||||
|
target: document.body,
|
||||||
|
props: {
|
||||||
|
title: props.title,
|
||||||
|
body: props.body,
|
||||||
|
okText: props.okText,
|
||||||
|
cancelText: props.cancelText,
|
||||||
|
size: props.size,
|
||||||
|
warning: false,
|
||||||
|
onOk: () => {
|
||||||
|
dialog.$destroy()
|
||||||
|
resolve(props.onConfirm?.() || true)
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
dialog.$destroy()
|
||||||
|
resolve(props.onCancel?.() || false)
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
dialog.$destroy()
|
||||||
|
resolve(props.onClose?.() || false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
dialog.show()
|
||||||
|
})
|
||||||
|
}
|
|
@ -11,3 +11,4 @@ export {
|
||||||
} from "./helpers"
|
} from "./helpers"
|
||||||
export * as featureFlag from "./featureFlags"
|
export * as featureFlag from "./featureFlags"
|
||||||
export * as bindings from "./bindings"
|
export * as bindings from "./bindings"
|
||||||
|
export * from "./confirm"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { params } from "@roxi/routify"
|
||||||
import { Tabs, Tab, Heading, Body, Layout } from "@budibase/bbui"
|
import { Tabs, Tab, Heading, Body, Layout } from "@budibase/bbui"
|
||||||
import { datasources, integrations } from "@/stores/builder"
|
import { datasources, integrations } from "@/stores/builder"
|
||||||
import ICONS from "@/components/backend/DatasourceNavigator/icons"
|
import ICONS from "@/components/backend/DatasourceNavigator/icons"
|
||||||
|
@ -15,7 +16,7 @@
|
||||||
import { admin } from "@/stores/portal"
|
import { admin } from "@/stores/portal"
|
||||||
import { IntegrationTypes } from "@/constants/backend"
|
import { IntegrationTypes } from "@/constants/backend"
|
||||||
|
|
||||||
let selectedPanel = null
|
let selectedPanel = $params.tab ?? null
|
||||||
let panelOptions = []
|
let panelOptions = []
|
||||||
|
|
||||||
$: datasource = $datasources.selected
|
$: datasource = $datasources.selected
|
||||||
|
|
|
@ -5,3 +5,4 @@ MONGODB_SHA=sha256:afa36bca12295b5f9dae68a493c706113922bdab520e901bd5d6c9d7247a1
|
||||||
MARIADB_SHA=sha256:e59ba8783bf7bc02a4779f103bb0d8751ac0e10f9471089709608377eded7aa8
|
MARIADB_SHA=sha256:e59ba8783bf7bc02a4779f103bb0d8751ac0e10f9471089709608377eded7aa8
|
||||||
ELASTICSEARCH_SHA=sha256:9a6443f55243f6acbfeb4a112d15eb3b9aac74bf25e0e39fa19b3ddd3a6879d0
|
ELASTICSEARCH_SHA=sha256:9a6443f55243f6acbfeb4a112d15eb3b9aac74bf25e0e39fa19b3ddd3a6879d0
|
||||||
DYNAMODB_SHA=sha256:cf8cebd061f988628c02daff10fdb950a54478feff9c52f6ddf84710fe3c3906
|
DYNAMODB_SHA=sha256:cf8cebd061f988628c02daff10fdb950a54478feff9c52f6ddf84710fe3c3906
|
||||||
|
KEYCLOAK_IMAGE=keycloak/keycloak@sha256:044a457e04987e1fff756be3d2fa325a4ef420fa356b7034ecc9f1b693c32761
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
CreateOAuth2ConfigRequest,
|
CreateOAuth2ConfigRequest,
|
||||||
|
CreateOAuth2ConfigResponse,
|
||||||
Ctx,
|
Ctx,
|
||||||
FetchOAuth2ConfigsResponse,
|
FetchOAuth2ConfigsResponse,
|
||||||
OAuth2Config,
|
OAuth2Config,
|
||||||
|
@ -12,17 +13,26 @@ export async function fetch(ctx: Ctx<void, FetchOAuth2ConfigsResponse>) {
|
||||||
|
|
||||||
const response: FetchOAuth2ConfigsResponse = {
|
const response: FetchOAuth2ConfigsResponse = {
|
||||||
configs: (configs || []).map(c => ({
|
configs: (configs || []).map(c => ({
|
||||||
|
id: c.id,
|
||||||
name: c.name,
|
name: c.name,
|
||||||
|
url: c.url,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
ctx.body = response
|
ctx.body = response
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(ctx: Ctx<CreateOAuth2ConfigRequest, void>) {
|
export async function create(
|
||||||
const newConfig: RequiredKeys<OAuth2Config> = {
|
ctx: Ctx<CreateOAuth2ConfigRequest, CreateOAuth2ConfigResponse>
|
||||||
name: ctx.request.body.name,
|
) {
|
||||||
|
const { body } = ctx.request
|
||||||
|
const newConfig: RequiredKeys<Omit<OAuth2Config, "id">> = {
|
||||||
|
name: body.name,
|
||||||
|
url: body.url,
|
||||||
|
clientId: body.clientId,
|
||||||
|
clientSecret: body.clientSecret,
|
||||||
}
|
}
|
||||||
|
|
||||||
await sdk.oauth2.create(newConfig)
|
const config = await sdk.oauth2.create(newConfig)
|
||||||
ctx.status = 201
|
ctx.status = 201
|
||||||
|
ctx.body = { config }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CreateOAuth2ConfigRequest } from "@budibase/types"
|
import { CreateOAuth2ConfigRequest, VirtualDocumentType } from "@budibase/types"
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
|
||||||
|
@ -8,6 +8,9 @@ describe("/oauth2", () => {
|
||||||
function makeOAuth2Config(): CreateOAuth2ConfigRequest {
|
function makeOAuth2Config(): CreateOAuth2ConfigRequest {
|
||||||
return {
|
return {
|
||||||
name: generator.guid(),
|
name: generator.guid(),
|
||||||
|
url: generator.url(),
|
||||||
|
clientId: generator.guid(),
|
||||||
|
clientSecret: generator.hash(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +18,10 @@ describe("/oauth2", () => {
|
||||||
|
|
||||||
beforeEach(async () => await config.newTenant())
|
beforeEach(async () => await config.newTenant())
|
||||||
|
|
||||||
|
const expectOAuth2ConfigId = expect.stringMatching(
|
||||||
|
`^${VirtualDocumentType.OAUTH2_CONFIG}_.+$`
|
||||||
|
)
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
it("returns empty when no oauth are created", async () => {
|
it("returns empty when no oauth are created", async () => {
|
||||||
const response = await config.api.oauth2.fetch()
|
const response = await config.api.oauth2.fetch()
|
||||||
|
@ -33,7 +40,9 @@ describe("/oauth2", () => {
|
||||||
expect(response).toEqual({
|
expect(response).toEqual({
|
||||||
configs: [
|
configs: [
|
||||||
{
|
{
|
||||||
|
id: expectOAuth2ConfigId,
|
||||||
name: oauth2Config.name,
|
name: oauth2Config.name,
|
||||||
|
url: oauth2Config.url,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -48,12 +57,17 @@ describe("/oauth2", () => {
|
||||||
const response = await config.api.oauth2.fetch()
|
const response = await config.api.oauth2.fetch()
|
||||||
expect(response.configs).toEqual([
|
expect(response.configs).toEqual([
|
||||||
{
|
{
|
||||||
|
id: expectOAuth2ConfigId,
|
||||||
name: oauth2Config.name,
|
name: oauth2Config.name,
|
||||||
|
url: oauth2Config.url,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: expectOAuth2ConfigId,
|
||||||
name: oauth2Config2.name,
|
name: oauth2Config2.name,
|
||||||
|
url: oauth2Config2.url,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
expect(response.configs[0].id).not.toEqual(response.configs[1].id)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("cannot create configurations with already existing names", async () => {
|
it("cannot create configurations with already existing names", async () => {
|
||||||
|
@ -71,7 +85,9 @@ describe("/oauth2", () => {
|
||||||
const response = await config.api.oauth2.fetch()
|
const response = await config.api.oauth2.fetch()
|
||||||
expect(response.configs).toEqual([
|
expect(response.configs).toEqual([
|
||||||
{
|
{
|
||||||
|
id: expectOAuth2ConfigId,
|
||||||
name: oauth2Config.name,
|
name: oauth2Config.name,
|
||||||
|
url: oauth2Config.url,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,8 +8,6 @@ import {
|
||||||
PaginationValues,
|
PaginationValues,
|
||||||
QueryType,
|
QueryType,
|
||||||
RestAuthType,
|
RestAuthType,
|
||||||
RestBasicAuthConfig,
|
|
||||||
RestBearerAuthConfig,
|
|
||||||
RestConfig,
|
RestConfig,
|
||||||
RestQueryFields as RestQuery,
|
RestQueryFields as RestQuery,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
@ -28,6 +26,8 @@ import { parse } from "content-disposition"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Builder as XmlBuilder } from "xml2js"
|
import { Builder as XmlBuilder } from "xml2js"
|
||||||
import { getAttachmentHeaders } from "./utils/restUtils"
|
import { getAttachmentHeaders } from "./utils/restUtils"
|
||||||
|
import { utils } from "@budibase/shared-core"
|
||||||
|
import sdk from "../sdk"
|
||||||
|
|
||||||
const coreFields = {
|
const coreFields = {
|
||||||
path: {
|
path: {
|
||||||
|
@ -377,29 +377,41 @@ export class RestIntegration implements IntegrationBase {
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthHeaders(authConfigId?: string): { [key: string]: any } {
|
async getAuthHeaders(
|
||||||
let headers: any = {}
|
authConfigId?: string,
|
||||||
|
authConfigType?: RestAuthType
|
||||||
|
): Promise<{ [key: string]: any }> {
|
||||||
|
if (!authConfigId) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.config.authConfigs && authConfigId) {
|
if (authConfigType === RestAuthType.OAUTH2) {
|
||||||
|
return { Authorization: await sdk.oauth2.generateToken(authConfigId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.authConfigs) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers: any = {}
|
||||||
const authConfig = this.config.authConfigs.filter(
|
const authConfig = this.config.authConfigs.filter(
|
||||||
c => c._id === authConfigId
|
c => c._id === authConfigId
|
||||||
)[0]
|
)[0]
|
||||||
// check the config still exists before proceeding
|
// check the config still exists before proceeding
|
||||||
// if not - do nothing
|
// if not - do nothing
|
||||||
if (authConfig) {
|
if (authConfig) {
|
||||||
let config
|
const { type, config } = authConfig
|
||||||
switch (authConfig.type) {
|
switch (type) {
|
||||||
case RestAuthType.BASIC:
|
case RestAuthType.BASIC:
|
||||||
config = authConfig.config as RestBasicAuthConfig
|
|
||||||
headers.Authorization = `Basic ${Buffer.from(
|
headers.Authorization = `Basic ${Buffer.from(
|
||||||
`${config.username}:${config.password}`
|
`${config.username}:${config.password}`
|
||||||
).toString("base64")}`
|
).toString("base64")}`
|
||||||
break
|
break
|
||||||
case RestAuthType.BEARER:
|
case RestAuthType.BEARER:
|
||||||
config = authConfig.config as RestBearerAuthConfig
|
|
||||||
headers.Authorization = `Bearer ${config.token}`
|
headers.Authorization = `Bearer ${config.token}`
|
||||||
break
|
break
|
||||||
}
|
default:
|
||||||
|
throw utils.unreachable(type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,10 +428,11 @@ export class RestIntegration implements IntegrationBase {
|
||||||
bodyType = BodyType.NONE,
|
bodyType = BodyType.NONE,
|
||||||
requestBody,
|
requestBody,
|
||||||
authConfigId,
|
authConfigId,
|
||||||
|
authConfigType,
|
||||||
pagination,
|
pagination,
|
||||||
paginationValues,
|
paginationValues,
|
||||||
} = query
|
} = query
|
||||||
const authHeaders = this.getAuthHeaders(authConfigId)
|
const authHeaders = await this.getAuthHeaders(authConfigId, authConfigType)
|
||||||
|
|
||||||
this.headers = {
|
this.headers = {
|
||||||
...(this.config.defaultHeaders || {}),
|
...(this.config.defaultHeaders || {}),
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
import nock from "nock"
|
import nock from "nock"
|
||||||
import { RestIntegration } from "../rest"
|
|
||||||
import { BodyType, RestAuthType } from "@budibase/types"
|
|
||||||
import { Response } from "node-fetch"
|
|
||||||
import TestConfiguration from "../../../src/tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../src/tests/utilities/TestConfiguration"
|
||||||
|
import { RestIntegration } from "../rest"
|
||||||
|
import {
|
||||||
|
BasicRestAuthConfig,
|
||||||
|
BearerRestAuthConfig,
|
||||||
|
BodyType,
|
||||||
|
RestAuthType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { Response } from "node-fetch"
|
||||||
import { createServer } from "http"
|
import { createServer } from "http"
|
||||||
import { AddressInfo } from "net"
|
import { AddressInfo } from "net"
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
|
||||||
const UUID_REGEX =
|
const UUID_REGEX =
|
||||||
"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
|
"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
|
||||||
|
@ -224,7 +230,7 @@ describe("REST Integration", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("authentication", () => {
|
describe("authentication", () => {
|
||||||
const basicAuth = {
|
const basicAuth: BasicRestAuthConfig = {
|
||||||
_id: "c59c14bd1898a43baa08da68959b24686",
|
_id: "c59c14bd1898a43baa08da68959b24686",
|
||||||
name: "basic-1",
|
name: "basic-1",
|
||||||
type: RestAuthType.BASIC,
|
type: RestAuthType.BASIC,
|
||||||
|
@ -234,7 +240,7 @@ describe("REST Integration", () => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const bearerAuth = {
|
const bearerAuth: BearerRestAuthConfig = {
|
||||||
_id: "0d91d732f34e4befabeff50b392a8ff3",
|
_id: "0d91d732f34e4befabeff50b392a8ff3",
|
||||||
name: "bearer-1",
|
name: "bearer-1",
|
||||||
type: RestAuthType.BEARER,
|
type: RestAuthType.BEARER,
|
||||||
|
@ -269,6 +275,38 @@ describe("REST Integration", () => {
|
||||||
const { data } = await integration.read({ authConfigId: bearerAuth._id })
|
const { data } = await integration.read({ authConfigId: bearerAuth._id })
|
||||||
expect(data).toEqual({ foo: "bar" })
|
expect(data).toEqual({ foo: "bar" })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("adds OAuth2 auth", async () => {
|
||||||
|
const oauth2Url = generator.url()
|
||||||
|
const { config: oauthConfig } = await config.api.oauth2.create({
|
||||||
|
name: generator.guid(),
|
||||||
|
url: oauth2Url,
|
||||||
|
clientId: generator.guid(),
|
||||||
|
clientSecret: generator.hash(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const token = generator.guid()
|
||||||
|
|
||||||
|
const url = new URL(oauth2Url)
|
||||||
|
nock(url.origin)
|
||||||
|
.post(url.pathname)
|
||||||
|
.reply(200, { token_type: "Bearer", access_token: token })
|
||||||
|
|
||||||
|
nock("https://example.com", {
|
||||||
|
reqheaders: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
.get("/")
|
||||||
|
.reply(200, { foo: "bar" })
|
||||||
|
const { data } = await config.doInContext(
|
||||||
|
config.appId,
|
||||||
|
async () =>
|
||||||
|
await integration.read({
|
||||||
|
authConfigId: oauthConfig.id,
|
||||||
|
authConfigType: RestAuthType.OAUTH2,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(data).toEqual({ foo: "bar" })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("page based pagination", () => {
|
describe("page based pagination", () => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import dotenv from "dotenv"
|
import dotenv from "dotenv"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
|
|
||||||
const path = join(__dirname, "..", "..", "..", "..", "datasource-sha.env")
|
const path = join(__dirname, "..", "..", "..", "..", "images-sha.env")
|
||||||
dotenv.config({
|
dotenv.config({
|
||||||
path,
|
path,
|
||||||
})
|
})
|
||||||
|
@ -14,3 +14,4 @@ export const MONGODB_IMAGE = `mongo@${process.env.MONGODB_SHA}`
|
||||||
export const MARIADB_IMAGE = `mariadb@${process.env.MARIADB_SHA}`
|
export const MARIADB_IMAGE = `mariadb@${process.env.MARIADB_SHA}`
|
||||||
export const ELASTICSEARCH_IMAGE = `elasticsearch@${process.env.ELASTICSEARCH_SHA}`
|
export const ELASTICSEARCH_IMAGE = `elasticsearch@${process.env.ELASTICSEARCH_SHA}`
|
||||||
export const DYNAMODB_IMAGE = `amazon/dynamodb-local@${process.env.DYNAMODB_SHA}`
|
export const DYNAMODB_IMAGE = `amazon/dynamodb-local@${process.env.DYNAMODB_SHA}`
|
||||||
|
export const KEYCLOAK_IMAGE = process.env.KEYCLOAK_IMAGE || ""
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
import { context, HTTPError } from "@budibase/backend-core"
|
|
||||||
import { DocumentType, OAuth2Config, OAuth2Configs } from "@budibase/types"
|
|
||||||
|
|
||||||
export async function fetch(): Promise<OAuth2Config[]> {
|
|
||||||
const db = context.getAppDB()
|
|
||||||
const result = await db.tryGet<OAuth2Configs>(DocumentType.OAUTH2_CONFIG)
|
|
||||||
if (!result) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return Object.values(result.configs)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function create(config: OAuth2Config) {
|
|
||||||
const db = context.getAppDB()
|
|
||||||
const doc: OAuth2Configs = (await db.tryGet<OAuth2Configs>(
|
|
||||||
DocumentType.OAUTH2_CONFIG
|
|
||||||
)) ?? {
|
|
||||||
_id: DocumentType.OAUTH2_CONFIG,
|
|
||||||
configs: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doc.configs[config.name]) {
|
|
||||||
throw new HTTPError("Name already used", 400)
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.configs[config.name] = config
|
|
||||||
await db.put(doc)
|
|
||||||
}
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { context, HTTPError, utils } from "@budibase/backend-core"
|
||||||
|
import {
|
||||||
|
Database,
|
||||||
|
DocumentType,
|
||||||
|
OAuth2Config,
|
||||||
|
OAuth2Configs,
|
||||||
|
SEPARATOR,
|
||||||
|
VirtualDocumentType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
async function getDocument(db: Database = context.getAppDB()) {
|
||||||
|
const result = await db.tryGet<OAuth2Configs>(DocumentType.OAUTH2_CONFIG)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetch(): Promise<OAuth2Config[]> {
|
||||||
|
const result = await getDocument()
|
||||||
|
if (!result) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return Object.values(result.configs)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(
|
||||||
|
config: Omit<OAuth2Config, "id">
|
||||||
|
): Promise<OAuth2Config> {
|
||||||
|
const db = context.getAppDB()
|
||||||
|
const doc: OAuth2Configs = (await getDocument(db)) ?? {
|
||||||
|
_id: DocumentType.OAUTH2_CONFIG,
|
||||||
|
configs: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.values(doc.configs).find(c => c.name === config.name)) {
|
||||||
|
throw new HTTPError("Name already used", 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = `${VirtualDocumentType.OAUTH2_CONFIG}${SEPARATOR}${utils.newid()}`
|
||||||
|
doc.configs[id] = {
|
||||||
|
id,
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.put(doc)
|
||||||
|
return doc.configs[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get(id: string): Promise<OAuth2Config | undefined> {
|
||||||
|
const doc = await getDocument()
|
||||||
|
return doc?.configs?.[id]
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./crud"
|
||||||
|
export * from "./utils"
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"id": "myrealm",
|
||||||
|
"realm": "myrealm",
|
||||||
|
"enabled": true,
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"clientId": "my-client",
|
||||||
|
"secret": "my-secret",
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"serviceAccountsEnabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
import { GenericContainer, Wait } from "testcontainers"
|
||||||
|
import sdk from "../../.."
|
||||||
|
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
|
||||||
|
import { generateToken } from "../utils"
|
||||||
|
import path from "path"
|
||||||
|
import { KEYCLOAK_IMAGE } from "../../../../integrations/tests/utils/images"
|
||||||
|
import { startContainer } from "../../../../integrations/tests/utils"
|
||||||
|
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
|
||||||
|
const volumePath = path.resolve(__dirname, "docker-volume")
|
||||||
|
|
||||||
|
jest.setTimeout(60000)
|
||||||
|
|
||||||
|
describe("oauth2 utils", () => {
|
||||||
|
let keycloakUrl: string
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
|
||||||
|
const ports = await startContainer(
|
||||||
|
new GenericContainer(KEYCLOAK_IMAGE)
|
||||||
|
.withName("keycloak_testcontainer")
|
||||||
|
.withExposedPorts(8080)
|
||||||
|
.withBindMounts([
|
||||||
|
{ source: volumePath, target: "/opt/keycloak/data/import/" },
|
||||||
|
])
|
||||||
|
.withCommand(["start-dev", "--import-realm"])
|
||||||
|
.withWaitStrategy(
|
||||||
|
Wait.forLogMessage("Listening on: http://0.0.0.0:8080")
|
||||||
|
)
|
||||||
|
.withStartupTimeout(60000)
|
||||||
|
)
|
||||||
|
|
||||||
|
const port = ports.find(x => x.container === 8080)?.host
|
||||||
|
if (!port) {
|
||||||
|
throw new Error("Keycloak port not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
keycloakUrl = `http://127.0.0.1:${port}`
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("generateToken", () => {
|
||||||
|
it("successfully generates tokens", async () => {
|
||||||
|
const response = await config.doInContext(config.appId, async () => {
|
||||||
|
const oauthConfig = await sdk.oauth2.create({
|
||||||
|
name: generator.guid(),
|
||||||
|
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
|
||||||
|
clientId: "my-client",
|
||||||
|
clientSecret: "my-secret",
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await generateToken(oauthConfig.id)
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response).toEqual(expect.stringMatching(/^Bearer .+/))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles wrong urls", async () => {
|
||||||
|
await expect(
|
||||||
|
config.doInContext(config.appId, async () => {
|
||||||
|
const oauthConfig = await sdk.oauth2.create({
|
||||||
|
name: generator.guid(),
|
||||||
|
url: `${keycloakUrl}/realms/wrong/protocol/openid-connect/token`,
|
||||||
|
clientId: "my-client",
|
||||||
|
clientSecret: "my-secret",
|
||||||
|
})
|
||||||
|
|
||||||
|
await generateToken(oauthConfig.id)
|
||||||
|
})
|
||||||
|
).rejects.toThrow("Error fetching oauth2 token: Not Found")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles wrong client ids", async () => {
|
||||||
|
await expect(
|
||||||
|
config.doInContext(config.appId, async () => {
|
||||||
|
const oauthConfig = await sdk.oauth2.create({
|
||||||
|
name: generator.guid(),
|
||||||
|
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
|
||||||
|
clientId: "wrong-client-id",
|
||||||
|
clientSecret: "my-secret",
|
||||||
|
})
|
||||||
|
|
||||||
|
await generateToken(oauthConfig.id)
|
||||||
|
})
|
||||||
|
).rejects.toThrow(
|
||||||
|
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles wrong secrets", async () => {
|
||||||
|
await expect(
|
||||||
|
config.doInContext(config.appId, async () => {
|
||||||
|
const oauthConfig = await sdk.oauth2.create({
|
||||||
|
name: generator.guid(),
|
||||||
|
url: `${keycloakUrl}/realms/myrealm/protocol/openid-connect/token`,
|
||||||
|
clientId: "my-client",
|
||||||
|
clientSecret: "wrong-secret",
|
||||||
|
})
|
||||||
|
|
||||||
|
await generateToken(oauthConfig.id)
|
||||||
|
})
|
||||||
|
).rejects.toThrow(
|
||||||
|
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,33 @@
|
||||||
|
import fetch from "node-fetch"
|
||||||
|
import { HttpError } from "koa"
|
||||||
|
import { get } from "../oauth2"
|
||||||
|
|
||||||
|
// TODO: check if caching is worth
|
||||||
|
export async function generateToken(id: string) {
|
||||||
|
const config = await get(id)
|
||||||
|
if (!config) {
|
||||||
|
throw new HttpError(`oAuth config ${id} count not be found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(config.url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: "client_credentials",
|
||||||
|
client_id: config.clientId,
|
||||||
|
client_secret: config.clientSecret,
|
||||||
|
}),
|
||||||
|
redirect: "follow",
|
||||||
|
})
|
||||||
|
|
||||||
|
const jsonResponse = await resp.json()
|
||||||
|
if (!resp.ok) {
|
||||||
|
const message = jsonResponse.error_description ?? resp.statusText
|
||||||
|
|
||||||
|
throw new Error(`Error fetching oauth2 token: ${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${jsonResponse.token_type} ${jsonResponse.access_token}`
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
CreateOAuth2ConfigRequest,
|
CreateOAuth2ConfigRequest,
|
||||||
|
CreateOAuth2ConfigResponse,
|
||||||
FetchOAuth2ConfigsResponse,
|
FetchOAuth2ConfigsResponse,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { Expectations, TestAPI } from "./base"
|
import { Expectations, TestAPI } from "./base"
|
||||||
|
@ -15,9 +16,12 @@ export class OAuth2API extends TestAPI {
|
||||||
body: CreateOAuth2ConfigRequest,
|
body: CreateOAuth2ConfigRequest,
|
||||||
expectations?: Expectations
|
expectations?: Expectations
|
||||||
) => {
|
) => {
|
||||||
return await this._post<CreateOAuth2ConfigRequest>("/api/oauth2", {
|
return await this._post<CreateOAuth2ConfigResponse>("/api/oauth2", {
|
||||||
body,
|
body,
|
||||||
expectations,
|
expectations: {
|
||||||
|
status: expectations?.status ?? 201,
|
||||||
|
...expectations,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
interface OAuth2Config {
|
interface OAuth2ConfigResponse {
|
||||||
|
id: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FetchOAuth2ConfigsResponse {
|
export interface FetchOAuth2ConfigsResponse {
|
||||||
configs: OAuth2Config[]
|
configs: OAuth2ConfigResponse[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateOAuth2ConfigRequest extends OAuth2Config {}
|
export interface CreateOAuth2ConfigRequest {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
clientId: string
|
||||||
|
clientSecret: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateOAuth2ConfigResponse {
|
||||||
|
config: OAuth2ConfigResponse
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ export interface Datasource extends Document {
|
||||||
export enum RestAuthType {
|
export enum RestAuthType {
|
||||||
BASIC = "basic",
|
BASIC = "basic",
|
||||||
BEARER = "bearer",
|
BEARER = "bearer",
|
||||||
|
OAUTH2 = "oauth2",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RestBasicAuthConfig {
|
export interface RestBasicAuthConfig {
|
||||||
|
@ -30,13 +31,22 @@ export interface RestBearerAuthConfig {
|
||||||
token: string
|
token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RestAuthConfig {
|
export interface BasicRestAuthConfig {
|
||||||
_id: string
|
_id: string
|
||||||
name: string
|
name: string
|
||||||
type: RestAuthType
|
type: RestAuthType.BASIC
|
||||||
config: RestBasicAuthConfig | RestBearerAuthConfig
|
config: RestBasicAuthConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BearerRestAuthConfig {
|
||||||
|
_id: string
|
||||||
|
name: string
|
||||||
|
type: RestAuthType.BEARER
|
||||||
|
config: RestBearerAuthConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RestAuthConfig = BasicRestAuthConfig | BearerRestAuthConfig
|
||||||
|
|
||||||
export interface DynamicVariable {
|
export interface DynamicVariable {
|
||||||
name: string
|
name: string
|
||||||
queryId: string
|
queryId: string
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { Document } from "../document"
|
import { Document } from "../document"
|
||||||
|
|
||||||
export interface OAuth2Config {
|
export interface OAuth2Config {
|
||||||
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
url: string
|
||||||
|
clientId: string
|
||||||
|
clientSecret: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OAuth2Configs extends Document {
|
export interface OAuth2Configs extends Document {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Document } from "../document"
|
import { Document } from "../document"
|
||||||
|
import { RestAuthType } from "./datasource"
|
||||||
import { Row } from "./row"
|
import { Row } from "./row"
|
||||||
|
|
||||||
export interface QuerySchema {
|
export interface QuerySchema {
|
||||||
|
@ -56,6 +57,7 @@ export interface RestQueryFields {
|
||||||
bodyType?: BodyType
|
bodyType?: BodyType
|
||||||
method?: string
|
method?: string
|
||||||
authConfigId?: string
|
authConfigId?: string
|
||||||
|
authConfigType?: RestAuthType
|
||||||
pagination?: PaginationConfig
|
pagination?: PaginationConfig
|
||||||
paginationValues?: PaginationValues
|
paginationValues?: PaginationValues
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ export enum InternalTable {
|
||||||
export enum VirtualDocumentType {
|
export enum VirtualDocumentType {
|
||||||
VIEW = "view",
|
VIEW = "view",
|
||||||
ROW_ACTION = "row_action",
|
ROW_ACTION = "row_action",
|
||||||
|
OAUTH2_CONFIG = "oauth2",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Because VirtualDocumentTypes can overlap, we need to make sure that we search
|
// Because VirtualDocumentTypes can overlap, we need to make sure that we search
|
||||||
|
|
Loading…
Reference in New Issue