Merge branch 'master' into feature/automation-sidebar

This commit is contained in:
deanhannigan 2025-03-21 09:47:10 +00:00 committed by GitHub
commit f7e2eeac6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 273 additions and 227 deletions

View File

@ -200,3 +200,13 @@ export function getStartEndKeyURL(baseKey: any, tenantId?: string) {
export const getPluginParams = (pluginId?: string | null, otherProps = {}) => { export const getPluginParams = (pluginId?: string | null, otherProps = {}) => {
return getDocParams(DocumentType.PLUGIN, pluginId, otherProps) return getDocParams(DocumentType.PLUGIN, pluginId, otherProps)
} }
/**
* Gets parameters for retrieving OAuth2 configs, this is a utility function for the getDocParams function.
*/
export const getOAuth2ConfigParams = (
configId?: string | null,
otherProps: Partial<DatabaseQueryOpts> = {}
) => {
return getDocParams(DocumentType.OAUTH2_CONFIG, configId, otherProps)
}

View File

@ -29,7 +29,7 @@
...authConfigs, ...authConfigs,
...$oauth2.configs.map(c => ({ ...$oauth2.configs.map(c => ({
label: c.name, label: c.name,
value: c.id, value: c._id,
})), })),
] ]
$: authConfig = allConfigs.find(c => c.value === authConfigId) $: authConfig = allConfigs.find(c => c.value === authConfigId)
@ -108,8 +108,9 @@
{#each $oauth2.configs as config} {#each $oauth2.configs as config}
<ListItem <ListItem
title={config.name} title={config.name}
on:click={() => selectConfiguration(config.id, RestAuthType.OAUTH2)} on:click={() =>
selected={config.id === authConfigId} selectConfiguration(config._id, RestAuthType.OAUTH2)}
selected={config._id === authConfigId}
/> />
{/each} {/each}
</List> </List>

View File

@ -7,7 +7,7 @@
Modal, Modal,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import type { OAuth2Config } from "@budibase/types" import type { OAuth2Config } from "@/types"
import OAuth2ConfigModalContent from "./OAuth2ConfigModalContent.svelte" import OAuth2ConfigModalContent from "./OAuth2ConfigModalContent.svelte"
import { confirm } from "@/helpers" import { confirm } from "@/helpers"
@ -26,7 +26,7 @@
warning: true, warning: true,
onConfirm: async () => { onConfirm: async () => {
try { try {
await oauth2.delete(row.id) await oauth2.delete(row._id, row._rev)
notifications.success(`Config '${row.name}' deleted successfully`) notifications.success(`Config '${row.name}' deleted successfully`)
} catch (e: any) { } catch (e: any) {
let message = "Error deleting config" let message = "Error deleting config"

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { oauth2 } from "@/stores/builder" import { oauth2 } from "@/stores/builder"
import type { OAuth2Config, UpsertOAuth2Config } from "@/types" import type { OAuth2Config } from "@/types"
import { import {
Body, Body,
Divider, Divider,
@ -12,6 +12,7 @@
notifications, notifications,
Select, Select,
} from "@budibase/bbui" } from "@budibase/bbui"
import type { InsertOAuth2ConfigRequest } from "@budibase/types"
import { import {
OAuth2CredentialsMethod, OAuth2CredentialsMethod,
PASSWORD_REPLACEMENT, PASSWORD_REPLACEMENT,
@ -50,7 +51,7 @@
name: requiredString("Name is required.").refine( name: requiredString("Name is required.").refine(
val => val =>
!$oauth2.configs !$oauth2.configs
.filter(c => c.id !== config.id) .filter(c => c._id !== config._id)
.map(c => c.name.toLowerCase()) .map(c => c.name.toLowerCase())
.includes(val.toLowerCase()), .includes(val.toLowerCase()),
{ {
@ -63,7 +64,7 @@
method: z.nativeEnum(OAuth2CredentialsMethod, { method: z.nativeEnum(OAuth2CredentialsMethod, {
message: "Authentication method is required.", message: "Authentication method is required.",
}), }),
}) satisfies ZodType<UpsertOAuth2Config> }) satisfies ZodType<InsertOAuth2ConfigRequest>
const validationResult = validator.safeParse(config) const validationResult = validator.safeParse(config)
errors = {} errors = {}
@ -91,7 +92,7 @@
const { data: configData } = validationResult const { data: configData } = validationResult
try { try {
const connectionValidation = await oauth2.validate({ const connectionValidation = await oauth2.validate({
id: config?.id, _id: config?._id,
url: configData.url, url: configData.url,
clientId: configData.clientId, clientId: configData.clientId,
clientSecret: configData.clientSecret, clientSecret: configData.clientSecret,
@ -110,7 +111,11 @@
await oauth2.create(configData) await oauth2.create(configData)
notifications.success("Settings created.") notifications.success("Settings created.")
} else { } else {
await oauth2.edit(config!.id, configData) await oauth2.edit({
...configData,
_id: config!._id,
_rev: config!._rev,
})
notifications.success("Settings saved.") notifications.success("Settings saved.")
} }
} catch (e: any) { } catch (e: any) {

View File

@ -1,7 +1,11 @@
import { API } from "@/api" import { API } from "@/api"
import { BudiStore } from "@/stores/BudiStore" import { BudiStore } from "@/stores/BudiStore"
import { OAuth2Config, UpsertOAuth2Config } from "@/types" import { OAuth2Config } from "@/types"
import { ValidateConfigRequest } from "@budibase/types" import {
InsertOAuth2ConfigRequest,
UpdateOAuth2ConfigRequest,
ValidateConfigRequest,
} from "@budibase/types"
interface OAuth2StoreState { interface OAuth2StoreState {
configs: OAuth2Config[] configs: OAuth2Config[]
@ -27,7 +31,8 @@ export class OAuth2Store extends BudiStore<OAuth2StoreState> {
this.store.update(store => ({ this.store.update(store => ({
...store, ...store,
configs: configs.map(c => ({ configs: configs.map(c => ({
id: c.id, _id: c._id,
_rev: c._rev,
name: c.name, name: c.name,
url: c.url, url: c.url,
clientId: c.clientId, clientId: c.clientId,
@ -45,18 +50,18 @@ export class OAuth2Store extends BudiStore<OAuth2StoreState> {
} }
} }
async create(config: UpsertOAuth2Config) { async create(config: InsertOAuth2ConfigRequest) {
await API.oauth2.create(config) await API.oauth2.create(config)
await this.fetch() await this.fetch()
} }
async edit(id: string, config: UpsertOAuth2Config) { async edit(config: UpdateOAuth2ConfigRequest) {
await API.oauth2.update(id, config) await API.oauth2.update(config)
await this.fetch() await this.fetch()
} }
async delete(id: string) { async delete(id: string, rev: string) {
await API.oauth2.delete(id) await API.oauth2.delete(id, rev)
await this.fetch() await this.fetch()
} }

View File

@ -1,8 +1,3 @@
import { import { OAuth2ConfigResponse } from "@budibase/types"
UpsertOAuth2ConfigRequest,
OAuth2ConfigResponse,
} from "@budibase/types"
export interface OAuth2Config extends OAuth2ConfigResponse {} export interface OAuth2Config extends OAuth2ConfigResponse {}
export interface UpsertOAuth2Config extends UpsertOAuth2ConfigRequest {}

View File

@ -1,8 +1,10 @@
import { import {
FetchOAuth2ConfigsResponse, FetchOAuth2ConfigsResponse,
InsertOAuth2ConfigRequest,
InsertOAuth2ConfigResponse,
OAuth2ConfigResponse, OAuth2ConfigResponse,
UpsertOAuth2ConfigRequest, UpdateOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse, UpdateOAuth2ConfigResponse,
ValidateConfigRequest, ValidateConfigRequest,
ValidateConfigResponse, ValidateConfigResponse,
} from "@budibase/types" } from "@budibase/types"
@ -11,13 +13,12 @@ import { BaseAPIClient } from "./types"
export interface OAuth2Endpoints { export interface OAuth2Endpoints {
fetch: () => Promise<OAuth2ConfigResponse[]> fetch: () => Promise<OAuth2ConfigResponse[]>
create: ( create: (
config: UpsertOAuth2ConfigRequest config: InsertOAuth2ConfigRequest
) => Promise<UpsertOAuth2ConfigResponse> ) => Promise<InsertOAuth2ConfigResponse>
update: ( update: (
id: string, config: UpdateOAuth2ConfigRequest
config: UpsertOAuth2ConfigRequest ) => Promise<UpdateOAuth2ConfigResponse>
) => Promise<UpsertOAuth2ConfigResponse> delete: (id: string, rev: string) => Promise<void>
delete: (id: string) => Promise<void>
validate: (config: ValidateConfigRequest) => Promise<ValidateConfigResponse> validate: (config: ValidateConfigRequest) => Promise<ValidateConfigResponse>
} }
@ -38,8 +39,8 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
*/ */
create: async config => { create: async config => {
return await API.post< return await API.post<
UpsertOAuth2ConfigRequest, InsertOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse InsertOAuth2ConfigResponse
>({ >({
url: `/api/oauth2`, url: `/api/oauth2`,
body: { body: {
@ -51,10 +52,10 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
/** /**
* Updates an existing OAuth2 configuration. * Updates an existing OAuth2 configuration.
*/ */
update: async (id, config) => { update: async config => {
return await API.put<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>( return await API.put<UpdateOAuth2ConfigRequest, UpdateOAuth2ConfigResponse>(
{ {
url: `/api/oauth2/${id}`, url: `/api/oauth2/${config._id}`,
body: { body: {
...config, ...config,
}, },
@ -65,10 +66,11 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
/** /**
* Deletes an OAuth2 configuration by its id. * Deletes an OAuth2 configuration by its id.
* @param id the ID of the OAuth2 config * @param id the ID of the OAuth2 config
* @param rev the rev of the OAuth2 config
*/ */
delete: async id => { delete: async (id, rev) => {
return await API.delete<void, void>({ return await API.delete<void, void>({
url: `/api/oauth2/${id}`, url: `/api/oauth2/${id}/${rev}`,
}) })
}, },
validate: async function ( validate: async function (

View File

@ -1,14 +1,15 @@
import { import {
UpsertOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse,
Ctx, Ctx,
FetchOAuth2ConfigsResponse, FetchOAuth2ConfigsResponse,
OAuth2Config, OAuth2Config,
RequiredKeys,
OAuth2ConfigResponse, OAuth2ConfigResponse,
PASSWORD_REPLACEMENT, PASSWORD_REPLACEMENT,
ValidateConfigResponse, ValidateConfigResponse,
ValidateConfigRequest, ValidateConfigRequest,
InsertOAuth2ConfigRequest,
InsertOAuth2ConfigResponse,
UpdateOAuth2ConfigRequest,
UpdateOAuth2ConfigResponse,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../sdk" import sdk from "../../sdk"
@ -16,7 +17,8 @@ function toFetchOAuth2ConfigsResponse(
config: OAuth2Config config: OAuth2Config
): OAuth2ConfigResponse { ): OAuth2ConfigResponse {
return { return {
id: config.id, _id: config._id!,
_rev: config._rev!,
name: config.name, name: config.name,
url: config.url, url: config.url,
clientId: config.clientId, clientId: config.clientId,
@ -35,10 +37,10 @@ export async function fetch(ctx: Ctx<void, FetchOAuth2ConfigsResponse>) {
} }
export async function create( export async function create(
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse> ctx: Ctx<InsertOAuth2ConfigRequest, InsertOAuth2ConfigResponse>
) { ) {
const { body } = ctx.request const { body } = ctx.request
const newConfig: RequiredKeys<Omit<OAuth2Config, "id">> = { const newConfig = {
name: body.name, name: body.name,
url: body.url, url: body.url,
clientId: body.clientId, clientId: body.clientId,
@ -54,11 +56,17 @@ export async function create(
} }
export async function edit( export async function edit(
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse> ctx: Ctx<UpdateOAuth2ConfigRequest, UpdateOAuth2ConfigResponse>
) { ) {
const { body } = ctx.request const { body } = ctx.request
const toUpdate: RequiredKeys<OAuth2Config> = {
id: ctx.params.id, if (ctx.params.id !== body._id) {
ctx.throw("Path and body ids do not match", 400)
}
const toUpdate = {
_id: body._id,
_rev: body._rev,
name: body.name, name: body.name,
url: body.url, url: body.url,
clientId: body.clientId, clientId: body.clientId,
@ -72,12 +80,10 @@ export async function edit(
} }
} }
export async function remove( export async function remove(ctx: Ctx<void, void>) {
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse> const { id, rev } = ctx.params
) {
const configToRemove = ctx.params.id
await sdk.oauth2.remove(configToRemove) await sdk.oauth2.remove(id, rev)
ctx.status = 204 ctx.status = 204
} }
@ -92,10 +98,10 @@ export async function validate(
method: body.method, method: body.method,
} }
if (config.clientSecret === PASSWORD_REPLACEMENT && body.id) { if (config.clientSecret === PASSWORD_REPLACEMENT && body._id) {
const existingConfig = await sdk.oauth2.get(body.id) const existingConfig = await sdk.oauth2.get(body._id)
if (!existingConfig) { if (!existingConfig) {
ctx.throw(`OAuth2 config with id '${body.id}' not found.`, 404) ctx.throw(`OAuth2 config with id '${body._id}' not found.`, 404)
} }
config.clientSecret = existingConfig.clientSecret config.clientSecret = existingConfig.clientSecret

View File

@ -6,7 +6,7 @@ import authorized from "../../middleware/authorized"
import * as controller from "../controllers/oauth2" import * as controller from "../controllers/oauth2"
import Joi from "joi" import Joi from "joi"
const baseValidation = { const baseSchema = {
url: Joi.string().required(), url: Joi.string().required(),
clientId: Joi.string().required(), clientId: Joi.string().required(),
clientSecret: Joi.string().required(), clientSecret: Joi.string().required(),
@ -15,24 +15,27 @@ const baseValidation = {
.valid(...Object.values(OAuth2CredentialsMethod)), .valid(...Object.values(OAuth2CredentialsMethod)),
} }
function oAuth2ConfigValidator() { const insertSchema = Joi.object({
return middleware.joiValidator.body( name: Joi.string().required(),
Joi.object({ ...baseSchema,
name: Joi.string().required(), })
...baseValidation,
}),
{ allowUnknown: false }
)
}
function oAuth2ConfigValidationValidator() { const updateSchema = Joi.object({
return middleware.joiValidator.body( _id: Joi.string().required(),
Joi.object({ _rev: Joi.string().required(),
id: Joi.string().required(), name: Joi.string().required(),
...baseValidation, ...baseSchema,
}), })
{ allowUnknown: false }
) const validationSchema = Joi.object({
_id: Joi.string(),
...baseSchema,
})
function oAuth2ConfigValidator(
schema: typeof validationSchema | typeof insertSchema | typeof updateSchema
) {
return middleware.joiValidator.body(schema, { allowUnknown: false })
} }
const router: Router = new Router() const router: Router = new Router()
@ -41,24 +44,24 @@ router.get("/api/oauth2", authorized(PermissionType.BUILDER), controller.fetch)
router.post( router.post(
"/api/oauth2", "/api/oauth2",
authorized(PermissionType.BUILDER), authorized(PermissionType.BUILDER),
oAuth2ConfigValidator(), oAuth2ConfigValidator(insertSchema),
controller.create controller.create
) )
router.put( router.put(
"/api/oauth2/:id", "/api/oauth2/:id",
authorized(PermissionType.BUILDER), authorized(PermissionType.BUILDER),
oAuth2ConfigValidator(), oAuth2ConfigValidator(updateSchema),
controller.edit controller.edit
) )
router.delete( router.delete(
"/api/oauth2/:id", "/api/oauth2/:id/:rev",
authorized(PermissionType.BUILDER), authorized(PermissionType.BUILDER),
controller.remove controller.remove
) )
router.post( router.post(
"/api/oauth2/validate", "/api/oauth2/validate",
authorized(PermissionType.BUILDER), authorized(PermissionType.BUILDER),
oAuth2ConfigValidationValidator(), oAuth2ConfigValidator(validationSchema),
controller.validate controller.validate
) )

View File

@ -1,9 +1,9 @@
import { import {
OAuth2Config, DocumentType,
InsertOAuth2ConfigRequest,
OAuth2ConfigResponse,
OAuth2CredentialsMethod, OAuth2CredentialsMethod,
PASSWORD_REPLACEMENT, PASSWORD_REPLACEMENT,
UpsertOAuth2ConfigRequest,
VirtualDocumentType,
} from "@budibase/types" } 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"
@ -12,7 +12,7 @@ import _ from "lodash/fp"
describe("/oauth2", () => { describe("/oauth2", () => {
let config = setup.getConfig() let config = setup.getConfig()
function makeOAuth2Config(): UpsertOAuth2ConfigRequest { function makeOAuth2Config(): InsertOAuth2ConfigRequest {
return { return {
name: generator.guid(), name: generator.guid(),
url: generator.url(), url: generator.url(),
@ -27,7 +27,7 @@ describe("/oauth2", () => {
beforeEach(async () => await config.newTenant()) beforeEach(async () => await config.newTenant())
const expectOAuth2ConfigId = expect.stringMatching( const expectOAuth2ConfigId = expect.stringMatching(
`^${VirtualDocumentType.OAUTH2_CONFIG}_.+$` `^${DocumentType.OAUTH2_CONFIG}_.+$`
) )
describe("fetch", () => { describe("fetch", () => {
@ -43,7 +43,7 @@ describe("/oauth2", () => {
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const oauth2Config = makeOAuth2Config() const oauth2Config = makeOAuth2Config()
const result = await config.api.oauth2.create(oauth2Config) const result = await config.api.oauth2.create(oauth2Config)
existingConfigs.push({ ...oauth2Config, id: result.config.id }) existingConfigs.push(result.config)
} }
const response = await config.api.oauth2.fetch() const response = await config.api.oauth2.fetch()
@ -51,7 +51,8 @@ describe("/oauth2", () => {
expect(response).toEqual({ expect(response).toEqual({
configs: expect.arrayContaining( configs: expect.arrayContaining(
existingConfigs.map(c => ({ existingConfigs.map(c => ({
id: c.id, _id: c._id,
_rev: c._rev,
name: c.name, name: c.name,
url: c.url, url: c.url,
clientId: c.clientId, clientId: c.clientId,
@ -72,7 +73,8 @@ describe("/oauth2", () => {
expect(response).toEqual({ expect(response).toEqual({
configs: [ configs: [
{ {
id: expectOAuth2ConfigId, _id: expectOAuth2ConfigId,
_rev: expect.stringMatching(/^1-\w+/),
name: oauth2Config.name, name: oauth2Config.name,
url: oauth2Config.url, url: oauth2Config.url,
clientId: oauth2Config.clientId, clientId: oauth2Config.clientId,
@ -90,25 +92,29 @@ describe("/oauth2", () => {
await config.api.oauth2.create(oauth2Config2, { status: 201 }) await config.api.oauth2.create(oauth2Config2, { status: 201 })
const response = await config.api.oauth2.fetch() const response = await config.api.oauth2.fetch()
expect(response.configs).toEqual([ expect(response.configs).toEqual(
{ expect.arrayContaining([
id: expectOAuth2ConfigId, {
name: oauth2Config.name, _id: expectOAuth2ConfigId,
url: oauth2Config.url, _rev: expect.stringMatching(/^1-\w+/),
clientId: oauth2Config.clientId, name: oauth2Config.name,
clientSecret: PASSWORD_REPLACEMENT, url: oauth2Config.url,
method: oauth2Config.method, clientId: oauth2Config.clientId,
}, clientSecret: PASSWORD_REPLACEMENT,
{ method: oauth2Config.method,
id: expectOAuth2ConfigId, },
name: oauth2Config2.name, {
url: oauth2Config2.url, _id: expectOAuth2ConfigId,
clientId: oauth2Config2.clientId, _rev: expect.stringMatching(/^1-\w+/),
clientSecret: PASSWORD_REPLACEMENT, name: oauth2Config2.name,
method: oauth2Config2.method, url: oauth2Config2.url,
}, clientId: oauth2Config2.clientId,
]) clientSecret: PASSWORD_REPLACEMENT,
expect(response.configs[0].id).not.toEqual(response.configs[1].id) method: oauth2Config2.method,
},
])
)
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 () => {
@ -118,7 +124,7 @@ describe("/oauth2", () => {
await config.api.oauth2.create(oauth2Config2, { await config.api.oauth2.create(oauth2Config2, {
status: 400, status: 400,
body: { body: {
message: "Name already used", message: `OAuth2 config with name '${oauth2Config.name}' is already taken.`,
status: 400, status: 400,
}, },
}) })
@ -126,7 +132,8 @@ 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, _id: expectOAuth2ConfigId,
_rev: expect.stringMatching(/^1-\w+/),
name: oauth2Config.name, name: oauth2Config.name,
url: oauth2Config.url, url: oauth2Config.url,
clientId: oauth2Config.clientId, clientId: oauth2Config.clientId,
@ -138,7 +145,7 @@ describe("/oauth2", () => {
}) })
describe("update", () => { describe("update", () => {
let existingConfigs: OAuth2Config[] = [] let existingConfigs: OAuth2ConfigResponse[] = []
beforeEach(async () => { beforeEach(async () => {
existingConfigs = [] existingConfigs = []
@ -146,14 +153,14 @@ describe("/oauth2", () => {
const oauth2Config = makeOAuth2Config() const oauth2Config = makeOAuth2Config()
const result = await config.api.oauth2.create(oauth2Config) const result = await config.api.oauth2.create(oauth2Config)
existingConfigs.push({ ...oauth2Config, id: result.config.id }) existingConfigs.push(result.config)
} }
}) })
it("can update an existing configuration", async () => { it("can update an existing configuration", async () => {
const { id: configId, ...configData } = _.sample(existingConfigs)! const configData = _.sample(existingConfigs)!
await config.api.oauth2.update(configId, { await config.api.oauth2.update({
...configData, ...configData,
name: "updated name", name: "updated name",
}) })
@ -163,7 +170,8 @@ describe("/oauth2", () => {
expect(response.configs).toEqual( expect(response.configs).toEqual(
expect.arrayContaining([ expect.arrayContaining([
{ {
id: configId, _id: configData._id,
_rev: expect.not.stringMatching(configData._rev),
name: "updated name", name: "updated name",
url: configData.url, url: configData.url,
clientId: configData.clientId, clientId: configData.clientId,
@ -175,7 +183,12 @@ describe("/oauth2", () => {
}) })
it("throw if config not found", async () => { it("throw if config not found", async () => {
await config.api.oauth2.update("unexisting", makeOAuth2Config(), { const toUpdate = {
...makeOAuth2Config(),
_id: "unexisting",
_rev: "unexisting",
}
await config.api.oauth2.update(toUpdate, {
status: 404, status: 404,
body: { message: "OAuth2 config with id 'unexisting' not found." }, body: { message: "OAuth2 config with id 'unexisting' not found." },
}) })
@ -183,12 +196,10 @@ describe("/oauth2", () => {
it("throws if trying to use an existing name", async () => { it("throws if trying to use an existing name", async () => {
const [config1, config2] = _.sampleSize(2, existingConfigs) const [config1, config2] = _.sampleSize(2, existingConfigs)
const { id: configId, ...configData } = config1
await config.api.oauth2.update( await config.api.oauth2.update(
configId,
{ {
...configData, ...config1,
name: config2.name, name: config2.name,
}, },
{ {
@ -202,7 +213,7 @@ describe("/oauth2", () => {
}) })
describe("delete", () => { describe("delete", () => {
let existingConfigs: OAuth2Config[] = [] let existingConfigs: OAuth2ConfigResponse[] = []
beforeEach(async () => { beforeEach(async () => {
existingConfigs = [] existingConfigs = []
@ -210,22 +221,26 @@ describe("/oauth2", () => {
const oauth2Config = makeOAuth2Config() const oauth2Config = makeOAuth2Config()
const result = await config.api.oauth2.create(oauth2Config) const result = await config.api.oauth2.create(oauth2Config)
existingConfigs.push({ ...oauth2Config, id: result.config.id }) existingConfigs.push(result.config)
} }
}) })
it("can delete an existing configuration", async () => { it("can delete an existing configuration", async () => {
const { id: configId } = _.sample(existingConfigs)! const configToDelete = _.sample(existingConfigs)!
await config.api.oauth2.delete(configId, { status: 204 }) await config.api.oauth2.delete(configToDelete._id, configToDelete._rev, {
status: 204,
})
const response = await config.api.oauth2.fetch() const response = await config.api.oauth2.fetch()
expect(response.configs).toHaveLength(existingConfigs.length - 1) expect(response.configs).toHaveLength(existingConfigs.length - 1)
expect(response.configs.find(c => c.id === configId)).toBeUndefined() expect(
response.configs.find(c => c._id === configToDelete._id)
).toBeUndefined()
}) })
it("throw if config not found", async () => { it("throw if config not found", async () => {
await config.api.oauth2.delete("unexisting", { await config.api.oauth2.delete("unexisting", "rev", {
status: 404, status: 404,
body: { message: "OAuth2 config with id 'unexisting' not found." }, body: { message: "OAuth2 config with id 'unexisting' not found." },
}) })

View File

@ -307,7 +307,7 @@ describe("REST Integration", () => {
config.appId, config.appId,
async () => async () =>
await integration.read({ await integration.read({
authConfigId: oauthConfig.id, authConfigId: oauthConfig._id,
authConfigType: RestAuthType.OAUTH2, authConfigType: RestAuthType.OAUTH2,
}) })
) )
@ -349,7 +349,7 @@ describe("REST Integration", () => {
config.appId, config.appId,
async () => async () =>
await integration.read({ await integration.read({
authConfigId: oauthConfig.id, authConfigId: oauthConfig._id,
authConfigType: RestAuthType.OAUTH2, authConfigType: RestAuthType.OAUTH2,
}) })
) )

View File

@ -1,101 +1,92 @@
import { context, HTTPError, utils } from "@budibase/backend-core" import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
import { import {
Database,
DocumentType, DocumentType,
OAuth2Config, OAuth2Config,
OAuth2Configs,
PASSWORD_REPLACEMENT, PASSWORD_REPLACEMENT,
SEPARATOR, SEPARATOR,
VirtualDocumentType, WithRequired,
} from "@budibase/types" } from "@budibase/types"
async function getDocument(db: Database = context.getAppDB()) { type CreatedOAuthConfig = WithRequired<OAuth2Config, "_id" | "_rev">
const result = await db.tryGet<OAuth2Configs>(DocumentType.OAUTH2_CONFIG)
return result
}
export async function fetch(): Promise<OAuth2Config[]> { async function guardName(name: string, id?: string) {
const result = await getDocument() const existingConfigs = await fetch()
if (!result) {
return []
}
return Object.values(result.configs)
}
export async function create( if (existingConfigs.find(c => c.name === name && c._id !== id)) {
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]
}
export async function update(config: OAuth2Config): Promise<OAuth2Config> {
const db = context.getAppDB()
const doc: OAuth2Configs = (await getDocument(db)) ?? {
_id: DocumentType.OAUTH2_CONFIG,
configs: {},
}
if (!doc.configs[config.id]) {
throw new HTTPError(`OAuth2 config with id '${config.id}' not found.`, 404)
}
if (
Object.values(doc.configs).find(
c => c.name === config.name && c.id !== config.id
)
) {
throw new HTTPError( throw new HTTPError(
`OAuth2 config with name '${config.name}' is already taken.`, `OAuth2 config with name '${name}' is already taken.`,
400 400
) )
} }
}
doc.configs[config.id] = { export async function fetch(): Promise<CreatedOAuthConfig[]> {
const db = context.getAppDB()
const docs = await db.allDocs<OAuth2Config>(
docIds.getOAuth2ConfigParams(null, { include_docs: true })
)
const result = docs.rows.map(r => ({
...r.doc!,
_id: r.doc!._id!,
_rev: r.doc!._rev!,
}))
return result
}
export async function create(
config: Omit<OAuth2Config, "_id" | "_rev" | "createdAt" | "updatedAt">
): Promise<CreatedOAuthConfig> {
const db = context.getAppDB()
await guardName(config.name)
const response = await db.put({
_id: `${DocumentType.OAUTH2_CONFIG}${SEPARATOR}${utils.newid()}`,
...config,
})
return {
_id: response.id!,
_rev: response.rev!,
...config,
}
}
export async function get(id: string): Promise<OAuth2Config | undefined> {
const db = context.getAppDB()
return await db.tryGet(id)
}
export async function update(
config: CreatedOAuthConfig
): Promise<CreatedOAuthConfig> {
const db = context.getAppDB()
await guardName(config.name, config._id)
const existing = await get(config._id)
if (!existing) {
throw new HTTPError(`OAuth2 config with id '${config._id}' not found.`, 404)
}
const toUpdate = {
...config, ...config,
clientSecret: clientSecret:
config.clientSecret === PASSWORD_REPLACEMENT config.clientSecret === PASSWORD_REPLACEMENT
? doc.configs[config.id].clientSecret ? existing.clientSecret
: config.clientSecret, : config.clientSecret,
} }
await db.put(doc) const result = await db.put(toUpdate)
return doc.configs[config.id] return { ...toUpdate, _rev: result.rev }
} }
export async function remove(configId: string): Promise<void> { export async function remove(configId: string, _rev: string): Promise<void> {
const db = context.getAppDB() const db = context.getAppDB()
const doc: OAuth2Configs = (await getDocument(db)) ?? { try {
_id: DocumentType.OAUTH2_CONFIG, await db.remove(configId, _rev)
configs: {}, } catch (e: any) {
if (e.status === 404) {
throw new HTTPError(`OAuth2 config with id '${configId}' not found.`, 404)
}
throw e
} }
if (!doc.configs[configId]) {
throw new HTTPError(`OAuth2 config with id '${configId}' not found.`, 404)
}
delete doc.configs[configId]
await db.put(doc)
} }

View File

@ -55,7 +55,7 @@ describe("oauth2 utils", () => {
method, method,
}) })
const response = await generateToken(oauthConfig.id) const response = await generateToken(oauthConfig._id)
return response return response
}) })
@ -73,7 +73,7 @@ describe("oauth2 utils", () => {
method, method,
}) })
await generateToken(oauthConfig.id) await generateToken(oauthConfig._id)
}) })
).rejects.toThrow("Error fetching oauth2 token: Not Found") ).rejects.toThrow("Error fetching oauth2 token: Not Found")
}) })
@ -89,7 +89,7 @@ describe("oauth2 utils", () => {
method, method,
}) })
await generateToken(oauthConfig.id) await generateToken(oauthConfig._id)
}) })
).rejects.toThrow( ).rejects.toThrow(
"Error fetching oauth2 token: Invalid client or Invalid client credentials" "Error fetching oauth2 token: Invalid client or Invalid client credentials"
@ -107,7 +107,7 @@ describe("oauth2 utils", () => {
method, method,
}) })
await generateToken(oauthConfig.id) await generateToken(oauthConfig._id)
}) })
).rejects.toThrow( ).rejects.toThrow(
"Error fetching oauth2 token: Invalid client or Invalid client credentials" "Error fetching oauth2 token: Invalid client or Invalid client credentials"

View File

@ -1,7 +1,9 @@
import { import {
UpsertOAuth2ConfigRequest, InsertOAuth2ConfigRequest,
UpsertOAuth2ConfigResponse, InsertOAuth2ConfigResponse,
FetchOAuth2ConfigsResponse, FetchOAuth2ConfigsResponse,
UpdateOAuth2ConfigRequest,
UpdateOAuth2ConfigResponse,
} from "@budibase/types" } from "@budibase/types"
import { Expectations, TestAPI } from "./base" import { Expectations, TestAPI } from "./base"
@ -13,10 +15,10 @@ export class OAuth2API extends TestAPI {
} }
create = async ( create = async (
body: UpsertOAuth2ConfigRequest, body: InsertOAuth2ConfigRequest,
expectations?: Expectations expectations?: Expectations
) => { ) => {
return await this._post<UpsertOAuth2ConfigResponse>("/api/oauth2", { return await this._post<InsertOAuth2ConfigResponse>("/api/oauth2", {
body, body,
expectations: { expectations: {
status: expectations?.status ?? 201, status: expectations?.status ?? 201,
@ -26,18 +28,20 @@ export class OAuth2API extends TestAPI {
} }
update = async ( update = async (
id: string, body: UpdateOAuth2ConfigRequest,
body: UpsertOAuth2ConfigRequest,
expectations?: Expectations expectations?: Expectations
) => { ) => {
return await this._put<UpsertOAuth2ConfigResponse>(`/api/oauth2/${id}`, { return await this._put<UpdateOAuth2ConfigResponse>(
body, `/api/oauth2/${body._id}`,
expectations, {
}) body,
expectations,
}
)
} }
delete = async (id: string, expectations?: Expectations) => { delete = async (id: string, rev: string, expectations?: Expectations) => {
return await this._delete<void>(`/api/oauth2/${id}`, { return await this._delete<void>(`/api/oauth2/${id}/${rev}`, {
expectations, expectations,
}) })
} }

View File

@ -1,7 +1,8 @@
import { OAuth2CredentialsMethod } from "@budibase/types" import { OAuth2CredentialsMethod } from "@budibase/types"
export interface OAuth2ConfigResponse { export interface OAuth2ConfigResponse {
id: string _id: string
_rev: string
name: string name: string
url: string url: string
clientId: string clientId: string
@ -13,7 +14,7 @@ export interface FetchOAuth2ConfigsResponse {
configs: OAuth2ConfigResponse[] configs: OAuth2ConfigResponse[]
} }
export interface UpsertOAuth2ConfigRequest { export interface InsertOAuth2ConfigRequest {
name: string name: string
url: string url: string
clientId: string clientId: string
@ -21,12 +22,26 @@ export interface UpsertOAuth2ConfigRequest {
method: OAuth2CredentialsMethod method: OAuth2CredentialsMethod
} }
export interface UpsertOAuth2ConfigResponse { export interface InsertOAuth2ConfigResponse {
config: OAuth2ConfigResponse
}
export interface UpdateOAuth2ConfigRequest {
_id: string
_rev: string
name: string
url: string
clientId: string
clientSecret: string
method: OAuth2CredentialsMethod
}
export interface UpdateOAuth2ConfigResponse {
config: OAuth2ConfigResponse config: OAuth2ConfigResponse
} }
export interface ValidateConfigRequest { export interface ValidateConfigRequest {
id?: string _id?: string
url: string url: string
clientId: string clientId: string
clientSecret: string clientSecret: string

View File

@ -5,15 +5,10 @@ export enum OAuth2CredentialsMethod {
BODY = "BODY", BODY = "BODY",
} }
export interface OAuth2Config { export interface OAuth2Config extends Document {
id: string
name: string name: string
url: string url: string
clientId: string clientId: string
clientSecret: string clientSecret: string
method: OAuth2CredentialsMethod method: OAuth2CredentialsMethod
} }
export interface OAuth2Configs extends Document {
configs: Record<string, OAuth2Config>
}

View File

@ -82,7 +82,6 @@ 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