diff --git a/packages/builder/src/pages/builder/app/[application]/settings/oauth2/MoreMenuRenderer.svelte b/packages/builder/src/pages/builder/app/[application]/settings/oauth2/MoreMenuRenderer.svelte index 864bf40d69..7b247bef53 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/oauth2/MoreMenuRenderer.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/oauth2/MoreMenuRenderer.svelte @@ -49,5 +49,5 @@ - + diff --git a/packages/builder/src/pages/builder/app/[application]/settings/oauth2/OAuth2ConfigModalContent.svelte b/packages/builder/src/pages/builder/app/[application]/settings/oauth2/OAuth2ConfigModalContent.svelte index f683adb996..da51d83c5d 100644 --- a/packages/builder/src/pages/builder/app/[application]/settings/oauth2/OAuth2ConfigModalContent.svelte +++ b/packages/builder/src/pages/builder/app/[application]/settings/oauth2/OAuth2ConfigModalContent.svelte @@ -70,12 +70,28 @@ return keepOpen } + const { data: configData } = validationResult try { + const connectionValidation = await oauth2.validate({ + id: config?.id, + url: configData.url, + clientId: configData.clientId, + clientSecret: configData.clientSecret, + }) + if (!connectionValidation.valid) { + let message = "Connection settings could not be validated" + if (connectionValidation.message) { + message += `: ${connectionValidation.message}` + } + notifications.error(message) + return keepOpen + } + if (isCreation) { - await oauth2.create(validationResult.data) + await oauth2.create(configData) notifications.success("Settings created.") } else { - await oauth2.edit(config!.id, validationResult.data) + await oauth2.edit(config!.id, configData) notifications.success("Settings saved.") } } catch (e: any) { diff --git a/packages/builder/src/stores/builder/oauth2.ts b/packages/builder/src/stores/builder/oauth2.ts index 85afe2d238..35b0c1fe6c 100644 --- a/packages/builder/src/stores/builder/oauth2.ts +++ b/packages/builder/src/stores/builder/oauth2.ts @@ -1,6 +1,7 @@ import { API } from "@/api" import { BudiStore } from "@/stores/BudiStore" import { OAuth2Config, UpsertOAuth2Config } from "@/types" +import { ValidateConfigRequest } from "@budibase/types" interface OAuth2StoreState { configs: OAuth2Config[] @@ -57,6 +58,10 @@ export class OAuth2Store extends BudiStore { await API.oauth2.delete(id) await this.fetch() } + + async validate(config: ValidateConfigRequest) { + return await API.oauth2.validate(config) + } } export const oauth2 = new OAuth2Store() diff --git a/packages/frontend-core/src/api/oauth2.ts b/packages/frontend-core/src/api/oauth2.ts index a4589461d8..7467418639 100644 --- a/packages/frontend-core/src/api/oauth2.ts +++ b/packages/frontend-core/src/api/oauth2.ts @@ -3,6 +3,8 @@ import { OAuth2ConfigResponse, UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse, + ValidateConfigRequest, + ValidateConfigResponse, } from "@budibase/types" import { BaseAPIClient } from "./types" @@ -16,6 +18,7 @@ export interface OAuth2Endpoints { config: UpsertOAuth2ConfigRequest ) => Promise delete: (id: string) => Promise + validate: (config: ValidateConfigRequest) => Promise } export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({ @@ -32,8 +35,6 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({ /** * Creates a OAuth2 configuration. - * @param name the name of the row action - * @param tableId the ID of the table */ create: async config => { return await API.post< @@ -49,8 +50,6 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({ /** * Updates an existing OAuth2 configuration. - * @param name the name of the row action - * @param tableId the ID of the table */ update: async (id, config) => { return await API.put( @@ -72,4 +71,14 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({ url: `/api/oauth2/${id}`, }) }, + validate: async function ( + config: ValidateConfigRequest + ): Promise { + return await API.post({ + url: `/api/oauth2/validate`, + body: { + ...config, + }, + }) + }, }) diff --git a/packages/server/src/api/controllers/oauth2.ts b/packages/server/src/api/controllers/oauth2.ts index 6447d60f0b..dc95cbda3d 100644 --- a/packages/server/src/api/controllers/oauth2.ts +++ b/packages/server/src/api/controllers/oauth2.ts @@ -7,6 +7,8 @@ import { RequiredKeys, OAuth2ConfigResponse, PASSWORD_REPLACEMENT, + ValidateConfigResponse, + ValidateConfigRequest, } from "@budibase/types" import sdk from "../../sdk" @@ -75,3 +77,27 @@ export async function remove( await sdk.oauth2.remove(configToRemove) ctx.status = 204 } + +export async function validate( + ctx: Ctx +) { + const { body } = ctx.request + const config = { + url: body.url, + clientId: body.clientId, + clientSecret: body.clientSecret, + } + + if (config.clientSecret === PASSWORD_REPLACEMENT && body.id) { + const existingConfig = await sdk.oauth2.get(body.id) + if (!existingConfig) { + ctx.throw(`OAuth2 config with id '${body.id}' not found.`, 404) + } + + config.clientSecret = existingConfig.clientSecret + } + + const validation = await sdk.oauth2.validateConfig(config) + ctx.status = 201 + ctx.body = validation +} diff --git a/packages/server/src/api/routes/oauth2.ts b/packages/server/src/api/routes/oauth2.ts index 2ae3cbdf82..a0e68eff84 100644 --- a/packages/server/src/api/routes/oauth2.ts +++ b/packages/server/src/api/routes/oauth2.ts @@ -38,5 +38,10 @@ router.delete( authorized(PermissionType.BUILDER), controller.remove ) +router.post( + "/api/oauth2/validate", + authorized(PermissionType.BUILDER), + controller.validate +) export default router diff --git a/packages/server/src/sdk/app/oauth2/utils.ts b/packages/server/src/sdk/app/oauth2/utils.ts index da36cd0478..1c280241fa 100644 --- a/packages/server/src/sdk/app/oauth2/utils.ts +++ b/packages/server/src/sdk/app/oauth2/utils.ts @@ -31,3 +31,34 @@ export async function generateToken(id: string) { return `${jsonResponse.token_type} ${jsonResponse.access_token}` } + +export async function validateConfig(config: { + url: string + clientId: string + clientSecret: string +}): Promise<{ valid: boolean; message?: string }> { + try { + 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 + return { valid: false, message } + } + + return { valid: true } + } catch (e: any) { + return { valid: false, message: e.message } + } +} diff --git a/packages/types/src/api/web/app/oauth2.ts b/packages/types/src/api/web/app/oauth2.ts index 1790676c0d..9c0e431b7e 100644 --- a/packages/types/src/api/web/app/oauth2.ts +++ b/packages/types/src/api/web/app/oauth2.ts @@ -20,3 +20,15 @@ export interface UpsertOAuth2ConfigRequest { export interface UpsertOAuth2ConfigResponse { config: OAuth2ConfigResponse } + +export interface ValidateConfigRequest { + id?: string + url: string + clientId: string + clientSecret: string +} + +export interface ValidateConfigResponse { + valid: boolean + message?: string +}