From e6612b2a9cfcd68fcd68ca32d9ab7fc7b6eb2dd6 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 19 Mar 2025 15:43:43 +0100 Subject: [PATCH 1/5] Validate endpoint --- packages/server/src/api/controllers/oauth2.ts | 17 ++++++++++++ packages/server/src/api/routes/oauth2.ts | 5 ++++ packages/server/src/sdk/app/oauth2/utils.ts | 27 +++++++++++++++++++ packages/types/src/api/web/app/oauth2.ts | 11 ++++++++ 4 files changed, 60 insertions(+) diff --git a/packages/server/src/api/controllers/oauth2.ts b/packages/server/src/api/controllers/oauth2.ts index 6447d60f0b..ff741b0065 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,18 @@ 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, + } + + 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..5f005fc408 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/:id/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..517b648379 100644 --- a/packages/server/src/sdk/app/oauth2/utils.ts +++ b/packages/server/src/sdk/app/oauth2/utils.ts @@ -31,3 +31,30 @@ 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 }> { + 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 } +} diff --git a/packages/types/src/api/web/app/oauth2.ts b/packages/types/src/api/web/app/oauth2.ts index 1790676c0d..9c915007b9 100644 --- a/packages/types/src/api/web/app/oauth2.ts +++ b/packages/types/src/api/web/app/oauth2.ts @@ -20,3 +20,14 @@ export interface UpsertOAuth2ConfigRequest { export interface UpsertOAuth2ConfigResponse { config: OAuth2ConfigResponse } + +export interface ValidateConfigRequest { + url: string + clientId: string + clientSecret: string +} + +export interface ValidateConfigResponse { + valid: boolean + message?: string +} From 71aecfb51bb261985240d1fe36aed7af9a430ca2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 19 Mar 2025 15:47:53 +0100 Subject: [PATCH 2/5] Validate existing passwords --- packages/server/src/api/controllers/oauth2.ts | 9 +++++++++ packages/server/src/api/routes/oauth2.ts | 2 +- packages/types/src/api/web/app/oauth2.ts | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/oauth2.ts b/packages/server/src/api/controllers/oauth2.ts index ff741b0065..dc95cbda3d 100644 --- a/packages/server/src/api/controllers/oauth2.ts +++ b/packages/server/src/api/controllers/oauth2.ts @@ -88,6 +88,15 @@ export async function validate( 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 5f005fc408..a0e68eff84 100644 --- a/packages/server/src/api/routes/oauth2.ts +++ b/packages/server/src/api/routes/oauth2.ts @@ -39,7 +39,7 @@ router.delete( controller.remove ) router.post( - "/api/oauth2/:id/validate", + "/api/oauth2/validate", authorized(PermissionType.BUILDER), controller.validate ) diff --git a/packages/types/src/api/web/app/oauth2.ts b/packages/types/src/api/web/app/oauth2.ts index 9c915007b9..9c0e431b7e 100644 --- a/packages/types/src/api/web/app/oauth2.ts +++ b/packages/types/src/api/web/app/oauth2.ts @@ -22,6 +22,7 @@ export interface UpsertOAuth2ConfigResponse { } export interface ValidateConfigRequest { + id?: string url: string clientId: string clientSecret: string From 36060af731d938fd1479af67d0768e03f7b164d6 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 19 Mar 2025 15:50:18 +0100 Subject: [PATCH 3/5] Validate before saving --- .../oauth2/OAuth2ConfigModalContent.svelte | 20 +++++++++++++++++-- packages/builder/src/stores/builder/oauth2.ts | 5 +++++ packages/frontend-core/src/api/oauth2.ts | 17 ++++++++++++---- 3 files changed, 36 insertions(+), 6 deletions(-) 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 d2d62521ec..93ef863a7d 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, + }, + }) + }, }) From 7f08f0d0239e38797abb7b7696433b1d874976e2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 19 Mar 2025 15:52:42 +0100 Subject: [PATCH 4/5] Immutable modal content --- .../app/[application]/settings/oauth2/MoreMenuRenderer.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 822ddb4d87..834092c1a9 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 @@ -45,5 +45,5 @@ - + From ccddac901046e70feb63d0c3f2903c8520403cd3 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 19 Mar 2025 16:05:59 +0100 Subject: [PATCH 5/5] Catch fetch errors --- packages/server/src/sdk/app/oauth2/utils.ts | 40 +++++++++++---------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/server/src/sdk/app/oauth2/utils.ts b/packages/server/src/sdk/app/oauth2/utils.ts index 517b648379..1c280241fa 100644 --- a/packages/server/src/sdk/app/oauth2/utils.ts +++ b/packages/server/src/sdk/app/oauth2/utils.ts @@ -37,24 +37,28 @@ export async function validateConfig(config: { clientId: string clientSecret: string }): Promise<{ valid: boolean; message?: string }> { - 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", - }) + 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 } + 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 } } - - return { valid: true } }