Merge branch 'master' into feature/automation-sidebar
This commit is contained in:
commit
f7e2eeac6e
|
@ -200,3 +200,13 @@ export function getStartEndKeyURL(baseKey: any, tenantId?: string) {
|
|||
export const getPluginParams = (pluginId?: string | null, 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)
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
...authConfigs,
|
||||
...$oauth2.configs.map(c => ({
|
||||
label: c.name,
|
||||
value: c.id,
|
||||
value: c._id,
|
||||
})),
|
||||
]
|
||||
$: authConfig = allConfigs.find(c => c.value === authConfigId)
|
||||
|
@ -108,8 +108,9 @@
|
|||
{#each $oauth2.configs as config}
|
||||
<ListItem
|
||||
title={config.name}
|
||||
on:click={() => selectConfiguration(config.id, RestAuthType.OAUTH2)}
|
||||
selected={config.id === authConfigId}
|
||||
on:click={() =>
|
||||
selectConfiguration(config._id, RestAuthType.OAUTH2)}
|
||||
selected={config._id === authConfigId}
|
||||
/>
|
||||
{/each}
|
||||
</List>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
Modal,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import type { OAuth2Config } from "@budibase/types"
|
||||
import type { OAuth2Config } from "@/types"
|
||||
import OAuth2ConfigModalContent from "./OAuth2ConfigModalContent.svelte"
|
||||
import { confirm } from "@/helpers"
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
|||
warning: true,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await oauth2.delete(row.id)
|
||||
await oauth2.delete(row._id, row._rev)
|
||||
notifications.success(`Config '${row.name}' deleted successfully`)
|
||||
} catch (e: any) {
|
||||
let message = "Error deleting config"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { oauth2 } from "@/stores/builder"
|
||||
import type { OAuth2Config, UpsertOAuth2Config } from "@/types"
|
||||
import type { OAuth2Config } from "@/types"
|
||||
import {
|
||||
Body,
|
||||
Divider,
|
||||
|
@ -12,6 +12,7 @@
|
|||
notifications,
|
||||
Select,
|
||||
} from "@budibase/bbui"
|
||||
import type { InsertOAuth2ConfigRequest } from "@budibase/types"
|
||||
import {
|
||||
OAuth2CredentialsMethod,
|
||||
PASSWORD_REPLACEMENT,
|
||||
|
@ -50,7 +51,7 @@
|
|||
name: requiredString("Name is required.").refine(
|
||||
val =>
|
||||
!$oauth2.configs
|
||||
.filter(c => c.id !== config.id)
|
||||
.filter(c => c._id !== config._id)
|
||||
.map(c => c.name.toLowerCase())
|
||||
.includes(val.toLowerCase()),
|
||||
{
|
||||
|
@ -63,7 +64,7 @@
|
|||
method: z.nativeEnum(OAuth2CredentialsMethod, {
|
||||
message: "Authentication method is required.",
|
||||
}),
|
||||
}) satisfies ZodType<UpsertOAuth2Config>
|
||||
}) satisfies ZodType<InsertOAuth2ConfigRequest>
|
||||
|
||||
const validationResult = validator.safeParse(config)
|
||||
errors = {}
|
||||
|
@ -91,7 +92,7 @@
|
|||
const { data: configData } = validationResult
|
||||
try {
|
||||
const connectionValidation = await oauth2.validate({
|
||||
id: config?.id,
|
||||
_id: config?._id,
|
||||
url: configData.url,
|
||||
clientId: configData.clientId,
|
||||
clientSecret: configData.clientSecret,
|
||||
|
@ -110,7 +111,11 @@
|
|||
await oauth2.create(configData)
|
||||
notifications.success("Settings created.")
|
||||
} else {
|
||||
await oauth2.edit(config!.id, configData)
|
||||
await oauth2.edit({
|
||||
...configData,
|
||||
_id: config!._id,
|
||||
_rev: config!._rev,
|
||||
})
|
||||
notifications.success("Settings saved.")
|
||||
}
|
||||
} catch (e: any) {
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { API } from "@/api"
|
||||
import { BudiStore } from "@/stores/BudiStore"
|
||||
import { OAuth2Config, UpsertOAuth2Config } from "@/types"
|
||||
import { ValidateConfigRequest } from "@budibase/types"
|
||||
import { OAuth2Config } from "@/types"
|
||||
import {
|
||||
InsertOAuth2ConfigRequest,
|
||||
UpdateOAuth2ConfigRequest,
|
||||
ValidateConfigRequest,
|
||||
} from "@budibase/types"
|
||||
|
||||
interface OAuth2StoreState {
|
||||
configs: OAuth2Config[]
|
||||
|
@ -27,7 +31,8 @@ export class OAuth2Store extends BudiStore<OAuth2StoreState> {
|
|||
this.store.update(store => ({
|
||||
...store,
|
||||
configs: configs.map(c => ({
|
||||
id: c.id,
|
||||
_id: c._id,
|
||||
_rev: c._rev,
|
||||
name: c.name,
|
||||
url: c.url,
|
||||
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 this.fetch()
|
||||
}
|
||||
|
||||
async edit(id: string, config: UpsertOAuth2Config) {
|
||||
await API.oauth2.update(id, config)
|
||||
async edit(config: UpdateOAuth2ConfigRequest) {
|
||||
await API.oauth2.update(config)
|
||||
await this.fetch()
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
await API.oauth2.delete(id)
|
||||
async delete(id: string, rev: string) {
|
||||
await API.oauth2.delete(id, rev)
|
||||
await this.fetch()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,3 @@
|
|||
import {
|
||||
UpsertOAuth2ConfigRequest,
|
||||
OAuth2ConfigResponse,
|
||||
} from "@budibase/types"
|
||||
import { OAuth2ConfigResponse } from "@budibase/types"
|
||||
|
||||
export interface OAuth2Config extends OAuth2ConfigResponse {}
|
||||
|
||||
export interface UpsertOAuth2Config extends UpsertOAuth2ConfigRequest {}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import {
|
||||
FetchOAuth2ConfigsResponse,
|
||||
InsertOAuth2ConfigRequest,
|
||||
InsertOAuth2ConfigResponse,
|
||||
OAuth2ConfigResponse,
|
||||
UpsertOAuth2ConfigRequest,
|
||||
UpsertOAuth2ConfigResponse,
|
||||
UpdateOAuth2ConfigRequest,
|
||||
UpdateOAuth2ConfigResponse,
|
||||
ValidateConfigRequest,
|
||||
ValidateConfigResponse,
|
||||
} from "@budibase/types"
|
||||
|
@ -11,13 +13,12 @@ import { BaseAPIClient } from "./types"
|
|||
export interface OAuth2Endpoints {
|
||||
fetch: () => Promise<OAuth2ConfigResponse[]>
|
||||
create: (
|
||||
config: UpsertOAuth2ConfigRequest
|
||||
) => Promise<UpsertOAuth2ConfigResponse>
|
||||
config: InsertOAuth2ConfigRequest
|
||||
) => Promise<InsertOAuth2ConfigResponse>
|
||||
update: (
|
||||
id: string,
|
||||
config: UpsertOAuth2ConfigRequest
|
||||
) => Promise<UpsertOAuth2ConfigResponse>
|
||||
delete: (id: string) => Promise<void>
|
||||
config: UpdateOAuth2ConfigRequest
|
||||
) => Promise<UpdateOAuth2ConfigResponse>
|
||||
delete: (id: string, rev: string) => Promise<void>
|
||||
validate: (config: ValidateConfigRequest) => Promise<ValidateConfigResponse>
|
||||
}
|
||||
|
||||
|
@ -38,8 +39,8 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
|
|||
*/
|
||||
create: async config => {
|
||||
return await API.post<
|
||||
UpsertOAuth2ConfigRequest,
|
||||
UpsertOAuth2ConfigResponse
|
||||
InsertOAuth2ConfigRequest,
|
||||
InsertOAuth2ConfigResponse
|
||||
>({
|
||||
url: `/api/oauth2`,
|
||||
body: {
|
||||
|
@ -51,10 +52,10 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
|
|||
/**
|
||||
* Updates an existing OAuth2 configuration.
|
||||
*/
|
||||
update: async (id, config) => {
|
||||
return await API.put<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>(
|
||||
update: async config => {
|
||||
return await API.put<UpdateOAuth2ConfigRequest, UpdateOAuth2ConfigResponse>(
|
||||
{
|
||||
url: `/api/oauth2/${id}`,
|
||||
url: `/api/oauth2/${config._id}`,
|
||||
body: {
|
||||
...config,
|
||||
},
|
||||
|
@ -65,10 +66,11 @@ export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
|
|||
/**
|
||||
* Deletes an OAuth2 configuration by its id.
|
||||
* @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>({
|
||||
url: `/api/oauth2/${id}`,
|
||||
url: `/api/oauth2/${id}/${rev}`,
|
||||
})
|
||||
},
|
||||
validate: async function (
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import {
|
||||
UpsertOAuth2ConfigRequest,
|
||||
UpsertOAuth2ConfigResponse,
|
||||
Ctx,
|
||||
FetchOAuth2ConfigsResponse,
|
||||
OAuth2Config,
|
||||
RequiredKeys,
|
||||
OAuth2ConfigResponse,
|
||||
PASSWORD_REPLACEMENT,
|
||||
ValidateConfigResponse,
|
||||
ValidateConfigRequest,
|
||||
InsertOAuth2ConfigRequest,
|
||||
InsertOAuth2ConfigResponse,
|
||||
UpdateOAuth2ConfigRequest,
|
||||
UpdateOAuth2ConfigResponse,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../sdk"
|
||||
|
||||
|
@ -16,7 +17,8 @@ function toFetchOAuth2ConfigsResponse(
|
|||
config: OAuth2Config
|
||||
): OAuth2ConfigResponse {
|
||||
return {
|
||||
id: config.id,
|
||||
_id: config._id!,
|
||||
_rev: config._rev!,
|
||||
name: config.name,
|
||||
url: config.url,
|
||||
clientId: config.clientId,
|
||||
|
@ -35,10 +37,10 @@ export async function fetch(ctx: Ctx<void, FetchOAuth2ConfigsResponse>) {
|
|||
}
|
||||
|
||||
export async function create(
|
||||
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
|
||||
ctx: Ctx<InsertOAuth2ConfigRequest, InsertOAuth2ConfigResponse>
|
||||
) {
|
||||
const { body } = ctx.request
|
||||
const newConfig: RequiredKeys<Omit<OAuth2Config, "id">> = {
|
||||
const newConfig = {
|
||||
name: body.name,
|
||||
url: body.url,
|
||||
clientId: body.clientId,
|
||||
|
@ -54,11 +56,17 @@ export async function create(
|
|||
}
|
||||
|
||||
export async function edit(
|
||||
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
|
||||
ctx: Ctx<UpdateOAuth2ConfigRequest, UpdateOAuth2ConfigResponse>
|
||||
) {
|
||||
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,
|
||||
url: body.url,
|
||||
clientId: body.clientId,
|
||||
|
@ -72,12 +80,10 @@ export async function edit(
|
|||
}
|
||||
}
|
||||
|
||||
export async function remove(
|
||||
ctx: Ctx<UpsertOAuth2ConfigRequest, UpsertOAuth2ConfigResponse>
|
||||
) {
|
||||
const configToRemove = ctx.params.id
|
||||
export async function remove(ctx: Ctx<void, void>) {
|
||||
const { id, rev } = ctx.params
|
||||
|
||||
await sdk.oauth2.remove(configToRemove)
|
||||
await sdk.oauth2.remove(id, rev)
|
||||
ctx.status = 204
|
||||
}
|
||||
|
||||
|
@ -92,10 +98,10 @@ export async function validate(
|
|||
method: body.method,
|
||||
}
|
||||
|
||||
if (config.clientSecret === PASSWORD_REPLACEMENT && body.id) {
|
||||
const existingConfig = await sdk.oauth2.get(body.id)
|
||||
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)
|
||||
ctx.throw(`OAuth2 config with id '${body._id}' not found.`, 404)
|
||||
}
|
||||
|
||||
config.clientSecret = existingConfig.clientSecret
|
||||
|
|
|
@ -6,7 +6,7 @@ import authorized from "../../middleware/authorized"
|
|||
import * as controller from "../controllers/oauth2"
|
||||
import Joi from "joi"
|
||||
|
||||
const baseValidation = {
|
||||
const baseSchema = {
|
||||
url: Joi.string().required(),
|
||||
clientId: Joi.string().required(),
|
||||
clientSecret: Joi.string().required(),
|
||||
|
@ -15,24 +15,27 @@ const baseValidation = {
|
|||
.valid(...Object.values(OAuth2CredentialsMethod)),
|
||||
}
|
||||
|
||||
function oAuth2ConfigValidator() {
|
||||
return middleware.joiValidator.body(
|
||||
Joi.object({
|
||||
name: Joi.string().required(),
|
||||
...baseValidation,
|
||||
}),
|
||||
{ allowUnknown: false }
|
||||
)
|
||||
}
|
||||
const insertSchema = Joi.object({
|
||||
name: Joi.string().required(),
|
||||
...baseSchema,
|
||||
})
|
||||
|
||||
function oAuth2ConfigValidationValidator() {
|
||||
return middleware.joiValidator.body(
|
||||
Joi.object({
|
||||
id: Joi.string().required(),
|
||||
...baseValidation,
|
||||
}),
|
||||
{ allowUnknown: false }
|
||||
)
|
||||
const updateSchema = Joi.object({
|
||||
_id: Joi.string().required(),
|
||||
_rev: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
...baseSchema,
|
||||
})
|
||||
|
||||
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()
|
||||
|
@ -41,24 +44,24 @@ router.get("/api/oauth2", authorized(PermissionType.BUILDER), controller.fetch)
|
|||
router.post(
|
||||
"/api/oauth2",
|
||||
authorized(PermissionType.BUILDER),
|
||||
oAuth2ConfigValidator(),
|
||||
oAuth2ConfigValidator(insertSchema),
|
||||
controller.create
|
||||
)
|
||||
router.put(
|
||||
"/api/oauth2/:id",
|
||||
authorized(PermissionType.BUILDER),
|
||||
oAuth2ConfigValidator(),
|
||||
oAuth2ConfigValidator(updateSchema),
|
||||
controller.edit
|
||||
)
|
||||
router.delete(
|
||||
"/api/oauth2/:id",
|
||||
"/api/oauth2/:id/:rev",
|
||||
authorized(PermissionType.BUILDER),
|
||||
controller.remove
|
||||
)
|
||||
router.post(
|
||||
"/api/oauth2/validate",
|
||||
authorized(PermissionType.BUILDER),
|
||||
oAuth2ConfigValidationValidator(),
|
||||
oAuth2ConfigValidator(validationSchema),
|
||||
controller.validate
|
||||
)
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {
|
||||
OAuth2Config,
|
||||
DocumentType,
|
||||
InsertOAuth2ConfigRequest,
|
||||
OAuth2ConfigResponse,
|
||||
OAuth2CredentialsMethod,
|
||||
PASSWORD_REPLACEMENT,
|
||||
UpsertOAuth2ConfigRequest,
|
||||
VirtualDocumentType,
|
||||
} from "@budibase/types"
|
||||
import * as setup from "./utilities"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
|
@ -12,7 +12,7 @@ import _ from "lodash/fp"
|
|||
describe("/oauth2", () => {
|
||||
let config = setup.getConfig()
|
||||
|
||||
function makeOAuth2Config(): UpsertOAuth2ConfigRequest {
|
||||
function makeOAuth2Config(): InsertOAuth2ConfigRequest {
|
||||
return {
|
||||
name: generator.guid(),
|
||||
url: generator.url(),
|
||||
|
@ -27,7 +27,7 @@ describe("/oauth2", () => {
|
|||
beforeEach(async () => await config.newTenant())
|
||||
|
||||
const expectOAuth2ConfigId = expect.stringMatching(
|
||||
`^${VirtualDocumentType.OAUTH2_CONFIG}_.+$`
|
||||
`^${DocumentType.OAUTH2_CONFIG}_.+$`
|
||||
)
|
||||
|
||||
describe("fetch", () => {
|
||||
|
@ -43,7 +43,7 @@ describe("/oauth2", () => {
|
|||
for (let i = 0; i < 10; i++) {
|
||||
const oauth2Config = makeOAuth2Config()
|
||||
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()
|
||||
|
@ -51,7 +51,8 @@ describe("/oauth2", () => {
|
|||
expect(response).toEqual({
|
||||
configs: expect.arrayContaining(
|
||||
existingConfigs.map(c => ({
|
||||
id: c.id,
|
||||
_id: c._id,
|
||||
_rev: c._rev,
|
||||
name: c.name,
|
||||
url: c.url,
|
||||
clientId: c.clientId,
|
||||
|
@ -72,7 +73,8 @@ describe("/oauth2", () => {
|
|||
expect(response).toEqual({
|
||||
configs: [
|
||||
{
|
||||
id: expectOAuth2ConfigId,
|
||||
_id: expectOAuth2ConfigId,
|
||||
_rev: expect.stringMatching(/^1-\w+/),
|
||||
name: oauth2Config.name,
|
||||
url: oauth2Config.url,
|
||||
clientId: oauth2Config.clientId,
|
||||
|
@ -90,25 +92,29 @@ describe("/oauth2", () => {
|
|||
await config.api.oauth2.create(oauth2Config2, { status: 201 })
|
||||
|
||||
const response = await config.api.oauth2.fetch()
|
||||
expect(response.configs).toEqual([
|
||||
{
|
||||
id: expectOAuth2ConfigId,
|
||||
name: oauth2Config.name,
|
||||
url: oauth2Config.url,
|
||||
clientId: oauth2Config.clientId,
|
||||
clientSecret: PASSWORD_REPLACEMENT,
|
||||
method: oauth2Config.method,
|
||||
},
|
||||
{
|
||||
id: expectOAuth2ConfigId,
|
||||
name: oauth2Config2.name,
|
||||
url: oauth2Config2.url,
|
||||
clientId: oauth2Config2.clientId,
|
||||
clientSecret: PASSWORD_REPLACEMENT,
|
||||
method: oauth2Config2.method,
|
||||
},
|
||||
])
|
||||
expect(response.configs[0].id).not.toEqual(response.configs[1].id)
|
||||
expect(response.configs).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
_id: expectOAuth2ConfigId,
|
||||
_rev: expect.stringMatching(/^1-\w+/),
|
||||
name: oauth2Config.name,
|
||||
url: oauth2Config.url,
|
||||
clientId: oauth2Config.clientId,
|
||||
clientSecret: PASSWORD_REPLACEMENT,
|
||||
method: oauth2Config.method,
|
||||
},
|
||||
{
|
||||
_id: expectOAuth2ConfigId,
|
||||
_rev: expect.stringMatching(/^1-\w+/),
|
||||
name: oauth2Config2.name,
|
||||
url: oauth2Config2.url,
|
||||
clientId: oauth2Config2.clientId,
|
||||
clientSecret: PASSWORD_REPLACEMENT,
|
||||
method: oauth2Config2.method,
|
||||
},
|
||||
])
|
||||
)
|
||||
expect(response.configs[0]._id).not.toEqual(response.configs[1]._id)
|
||||
})
|
||||
|
||||
it("cannot create configurations with already existing names", async () => {
|
||||
|
@ -118,7 +124,7 @@ describe("/oauth2", () => {
|
|||
await config.api.oauth2.create(oauth2Config2, {
|
||||
status: 400,
|
||||
body: {
|
||||
message: "Name already used",
|
||||
message: `OAuth2 config with name '${oauth2Config.name}' is already taken.`,
|
||||
status: 400,
|
||||
},
|
||||
})
|
||||
|
@ -126,7 +132,8 @@ describe("/oauth2", () => {
|
|||
const response = await config.api.oauth2.fetch()
|
||||
expect(response.configs).toEqual([
|
||||
{
|
||||
id: expectOAuth2ConfigId,
|
||||
_id: expectOAuth2ConfigId,
|
||||
_rev: expect.stringMatching(/^1-\w+/),
|
||||
name: oauth2Config.name,
|
||||
url: oauth2Config.url,
|
||||
clientId: oauth2Config.clientId,
|
||||
|
@ -138,7 +145,7 @@ describe("/oauth2", () => {
|
|||
})
|
||||
|
||||
describe("update", () => {
|
||||
let existingConfigs: OAuth2Config[] = []
|
||||
let existingConfigs: OAuth2ConfigResponse[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
existingConfigs = []
|
||||
|
@ -146,14 +153,14 @@ describe("/oauth2", () => {
|
|||
const oauth2Config = makeOAuth2Config()
|
||||
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 () => {
|
||||
const { id: configId, ...configData } = _.sample(existingConfigs)!
|
||||
const configData = _.sample(existingConfigs)!
|
||||
|
||||
await config.api.oauth2.update(configId, {
|
||||
await config.api.oauth2.update({
|
||||
...configData,
|
||||
name: "updated name",
|
||||
})
|
||||
|
@ -163,7 +170,8 @@ describe("/oauth2", () => {
|
|||
expect(response.configs).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
id: configId,
|
||||
_id: configData._id,
|
||||
_rev: expect.not.stringMatching(configData._rev),
|
||||
name: "updated name",
|
||||
url: configData.url,
|
||||
clientId: configData.clientId,
|
||||
|
@ -175,7 +183,12 @@ describe("/oauth2", () => {
|
|||
})
|
||||
|
||||
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,
|
||||
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 () => {
|
||||
const [config1, config2] = _.sampleSize(2, existingConfigs)
|
||||
const { id: configId, ...configData } = config1
|
||||
|
||||
await config.api.oauth2.update(
|
||||
configId,
|
||||
{
|
||||
...configData,
|
||||
...config1,
|
||||
name: config2.name,
|
||||
},
|
||||
{
|
||||
|
@ -202,7 +213,7 @@ describe("/oauth2", () => {
|
|||
})
|
||||
|
||||
describe("delete", () => {
|
||||
let existingConfigs: OAuth2Config[] = []
|
||||
let existingConfigs: OAuth2ConfigResponse[] = []
|
||||
|
||||
beforeEach(async () => {
|
||||
existingConfigs = []
|
||||
|
@ -210,22 +221,26 @@ describe("/oauth2", () => {
|
|||
const oauth2Config = makeOAuth2Config()
|
||||
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 () => {
|
||||
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()
|
||||
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 () => {
|
||||
await config.api.oauth2.delete("unexisting", {
|
||||
await config.api.oauth2.delete("unexisting", "rev", {
|
||||
status: 404,
|
||||
body: { message: "OAuth2 config with id 'unexisting' not found." },
|
||||
})
|
||||
|
|
|
@ -307,7 +307,7 @@ describe("REST Integration", () => {
|
|||
config.appId,
|
||||
async () =>
|
||||
await integration.read({
|
||||
authConfigId: oauthConfig.id,
|
||||
authConfigId: oauthConfig._id,
|
||||
authConfigType: RestAuthType.OAUTH2,
|
||||
})
|
||||
)
|
||||
|
@ -349,7 +349,7 @@ describe("REST Integration", () => {
|
|||
config.appId,
|
||||
async () =>
|
||||
await integration.read({
|
||||
authConfigId: oauthConfig.id,
|
||||
authConfigId: oauthConfig._id,
|
||||
authConfigType: RestAuthType.OAUTH2,
|
||||
})
|
||||
)
|
||||
|
|
|
@ -1,101 +1,92 @@
|
|||
import { context, HTTPError, utils } from "@budibase/backend-core"
|
||||
import { context, docIds, HTTPError, utils } from "@budibase/backend-core"
|
||||
import {
|
||||
Database,
|
||||
DocumentType,
|
||||
OAuth2Config,
|
||||
OAuth2Configs,
|
||||
PASSWORD_REPLACEMENT,
|
||||
SEPARATOR,
|
||||
VirtualDocumentType,
|
||||
WithRequired,
|
||||
} from "@budibase/types"
|
||||
|
||||
async function getDocument(db: Database = context.getAppDB()) {
|
||||
const result = await db.tryGet<OAuth2Configs>(DocumentType.OAUTH2_CONFIG)
|
||||
return result
|
||||
}
|
||||
type CreatedOAuthConfig = WithRequired<OAuth2Config, "_id" | "_rev">
|
||||
|
||||
export async function fetch(): Promise<OAuth2Config[]> {
|
||||
const result = await getDocument()
|
||||
if (!result) {
|
||||
return []
|
||||
}
|
||||
return Object.values(result.configs)
|
||||
}
|
||||
async function guardName(name: string, id?: string) {
|
||||
const existingConfigs = await fetch()
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
) {
|
||||
if (existingConfigs.find(c => c.name === name && c._id !== id)) {
|
||||
throw new HTTPError(
|
||||
`OAuth2 config with name '${config.name}' is already taken.`,
|
||||
`OAuth2 config with name '${name}' is already taken.`,
|
||||
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,
|
||||
clientSecret:
|
||||
config.clientSecret === PASSWORD_REPLACEMENT
|
||||
? doc.configs[config.id].clientSecret
|
||||
? existing.clientSecret
|
||||
: config.clientSecret,
|
||||
}
|
||||
|
||||
await db.put(doc)
|
||||
return doc.configs[config.id]
|
||||
const result = await db.put(toUpdate)
|
||||
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 doc: OAuth2Configs = (await getDocument(db)) ?? {
|
||||
_id: DocumentType.OAUTH2_CONFIG,
|
||||
configs: {},
|
||||
try {
|
||||
await db.remove(configId, _rev)
|
||||
} 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)
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ describe("oauth2 utils", () => {
|
|||
method,
|
||||
})
|
||||
|
||||
const response = await generateToken(oauthConfig.id)
|
||||
const response = await generateToken(oauthConfig._id)
|
||||
return response
|
||||
})
|
||||
|
||||
|
@ -73,7 +73,7 @@ describe("oauth2 utils", () => {
|
|||
method,
|
||||
})
|
||||
|
||||
await generateToken(oauthConfig.id)
|
||||
await generateToken(oauthConfig._id)
|
||||
})
|
||||
).rejects.toThrow("Error fetching oauth2 token: Not Found")
|
||||
})
|
||||
|
@ -89,7 +89,7 @@ describe("oauth2 utils", () => {
|
|||
method,
|
||||
})
|
||||
|
||||
await generateToken(oauthConfig.id)
|
||||
await generateToken(oauthConfig._id)
|
||||
})
|
||||
).rejects.toThrow(
|
||||
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
|
||||
|
@ -107,7 +107,7 @@ describe("oauth2 utils", () => {
|
|||
method,
|
||||
})
|
||||
|
||||
await generateToken(oauthConfig.id)
|
||||
await generateToken(oauthConfig._id)
|
||||
})
|
||||
).rejects.toThrow(
|
||||
"Error fetching oauth2 token: Invalid client or Invalid client credentials"
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import {
|
||||
UpsertOAuth2ConfigRequest,
|
||||
UpsertOAuth2ConfigResponse,
|
||||
InsertOAuth2ConfigRequest,
|
||||
InsertOAuth2ConfigResponse,
|
||||
FetchOAuth2ConfigsResponse,
|
||||
UpdateOAuth2ConfigRequest,
|
||||
UpdateOAuth2ConfigResponse,
|
||||
} from "@budibase/types"
|
||||
import { Expectations, TestAPI } from "./base"
|
||||
|
||||
|
@ -13,10 +15,10 @@ export class OAuth2API extends TestAPI {
|
|||
}
|
||||
|
||||
create = async (
|
||||
body: UpsertOAuth2ConfigRequest,
|
||||
body: InsertOAuth2ConfigRequest,
|
||||
expectations?: Expectations
|
||||
) => {
|
||||
return await this._post<UpsertOAuth2ConfigResponse>("/api/oauth2", {
|
||||
return await this._post<InsertOAuth2ConfigResponse>("/api/oauth2", {
|
||||
body,
|
||||
expectations: {
|
||||
status: expectations?.status ?? 201,
|
||||
|
@ -26,18 +28,20 @@ export class OAuth2API extends TestAPI {
|
|||
}
|
||||
|
||||
update = async (
|
||||
id: string,
|
||||
body: UpsertOAuth2ConfigRequest,
|
||||
body: UpdateOAuth2ConfigRequest,
|
||||
expectations?: Expectations
|
||||
) => {
|
||||
return await this._put<UpsertOAuth2ConfigResponse>(`/api/oauth2/${id}`, {
|
||||
body,
|
||||
expectations,
|
||||
})
|
||||
return await this._put<UpdateOAuth2ConfigResponse>(
|
||||
`/api/oauth2/${body._id}`,
|
||||
{
|
||||
body,
|
||||
expectations,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
delete = async (id: string, expectations?: Expectations) => {
|
||||
return await this._delete<void>(`/api/oauth2/${id}`, {
|
||||
delete = async (id: string, rev: string, expectations?: Expectations) => {
|
||||
return await this._delete<void>(`/api/oauth2/${id}/${rev}`, {
|
||||
expectations,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { OAuth2CredentialsMethod } from "@budibase/types"
|
||||
|
||||
export interface OAuth2ConfigResponse {
|
||||
id: string
|
||||
_id: string
|
||||
_rev: string
|
||||
name: string
|
||||
url: string
|
||||
clientId: string
|
||||
|
@ -13,7 +14,7 @@ export interface FetchOAuth2ConfigsResponse {
|
|||
configs: OAuth2ConfigResponse[]
|
||||
}
|
||||
|
||||
export interface UpsertOAuth2ConfigRequest {
|
||||
export interface InsertOAuth2ConfigRequest {
|
||||
name: string
|
||||
url: string
|
||||
clientId: string
|
||||
|
@ -21,12 +22,26 @@ export interface UpsertOAuth2ConfigRequest {
|
|||
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
|
||||
}
|
||||
|
||||
export interface ValidateConfigRequest {
|
||||
id?: string
|
||||
_id?: string
|
||||
url: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
|
|
|
@ -5,15 +5,10 @@ export enum OAuth2CredentialsMethod {
|
|||
BODY = "BODY",
|
||||
}
|
||||
|
||||
export interface OAuth2Config {
|
||||
id: string
|
||||
export interface OAuth2Config extends Document {
|
||||
name: string
|
||||
url: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
method: OAuth2CredentialsMethod
|
||||
}
|
||||
|
||||
export interface OAuth2Configs extends Document {
|
||||
configs: Record<string, OAuth2Config>
|
||||
}
|
||||
|
|
|
@ -82,7 +82,6 @@ export enum InternalTable {
|
|||
export enum VirtualDocumentType {
|
||||
VIEW = "view",
|
||||
ROW_ACTION = "row_action",
|
||||
OAUTH2_CONFIG = "oauth2",
|
||||
}
|
||||
|
||||
// Because VirtualDocumentTypes can overlap, we need to make sure that we search
|
||||
|
|
Loading…
Reference in New Issue