Merge pull request #15740 from Budibase/BUDI-9127/oauth2-settings

OAuth2 configuration frontend
This commit is contained in:
Adria Navarro 2025-03-18 18:42:39 +01:00 committed by GitHub
commit 26f7677e51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 290 additions and 14 deletions

View File

@ -1,5 +1,5 @@
<script>
export let title
<script lang="ts">
export let title = ""
</script>
<div class="side-nav">

View File

@ -1,6 +1,6 @@
<script>
export let text
export let url
<script lang="ts">
export let text = ""
export let url = ""
export let active = false
export let disabled = false
</script>

View File

@ -1,11 +1,15 @@
<script>
<script lang="ts">
import { Content, SideNav, SideNavItem } from "@/components/portal/page"
import { Page, Layout, AbsTooltip, TooltipPosition } from "@budibase/bbui"
import { url, isActive } from "@roxi/routify"
import DeleteModal from "@/components/deploy/DeleteModal.svelte"
import { isOnlyUser, appStore } from "@/stores/builder"
import { featureFlag } from "@/helpers"
import { FeatureFlag } from "@budibase/types"
let deleteModal
let deleteModal: DeleteModal
$: oauth2Enabled = featureFlag.isEnabled(FeatureFlag.OAUTH2_CONFIG)
</script>
<!-- routify:options index=4 -->
@ -44,11 +48,18 @@
url={$url("./version")}
active={$isActive("./version")}
/>
{#if oauth2Enabled}
<SideNavItem
text="OAuth2"
url={$url("./oauth2")}
active={$isActive("./oauth2")}
/>
{/if}
<div class="delete-action">
<AbsTooltip
position={TooltipPosition.Bottom}
text={$isOnlyUser
? null
? undefined
: "Unavailable - another user is editing this app"}
>
<SideNavItem

View File

@ -0,0 +1,74 @@
<script lang="ts">
import { oauth2 } from "@/stores/builder"
import type { CreateOAuth2Config } from "@/types"
import {
Body,
Button,
Divider,
Heading,
Input,
Link,
Modal,
ModalContent,
} from "@budibase/bbui"
let modal: Modal
function openModal() {
modal.show()
}
let config: Partial<CreateOAuth2Config> = {}
$: saveOAuth2Config = async () => {
await oauth2.create(config as any) // TODO
}
</script>
<Button cta size="M" on:click={openModal}>Add OAuth2</Button>
<Modal bind:this={modal}>
<ModalContent onConfirm={saveOAuth2Config} size="M">
<Heading size="S">Create new OAuth2 connection</Heading>
<Body size="S">
The OAuth 2 authentication below uses the Client Credentials (machine to
machine) grant type.
</Body>
<Divider noGrid noMargin />
<Input label="Name*" placeholder="Type here..." bind:value={config.name} />
<Input
label="Service URL*"
placeholder="E.g. www.google.com"
bind:value={config.url}
/>
<div class="field-info">
<Body size="XS" color="var(--spectrum-global-color-gray-700)">
The location where the flow sends the credentials. This field should be
a full URL.
</Body>
</div>
<Input
label="Client ID*"
placeholder="Type here..."
bind:value={config.clientId}
/>
<Input
type="password"
label="Client secret*"
placeholder="Type here..."
bind:value={config.clientSecret}
/>
<Body size="S"
>To learn how to configure OAuth2, our documentation <Link
href="TODO"
target="_blank"
size="M">our documentation.</Link
></Body
>
</ModalContent>
</Modal>
<style>
.field-info {
margin-top: calc(var(--spacing-xl) * -1 + var(--spacing-s));
}
</style>

View File

@ -0,0 +1,18 @@
<script lang="ts">
import { ActionMenu, Icon, MenuItem } from "@budibase/bbui"
function onEdit() {
// TODO
}
function onDelete() {
// TODO
}
</script>
<ActionMenu align="right">
<div slot="control" class="control icon">
<Icon size="S" hoverable name="MoreSmallList" />
</div>
<MenuItem on:click={onEdit} icon="Edit">Edit</MenuItem>
<MenuItem on:click={onDelete} icon="Delete">Delete</MenuItem>
</ActionMenu>

View File

@ -0,0 +1,61 @@
<script lang="ts">
import { Layout, Heading, Body, Divider, Table } from "@budibase/bbui"
import { oauth2 } from "@/stores/builder"
import AddButton from "./AddButton.svelte"
import { onMount } from "svelte"
import MoreMenuRenderer from "./MoreMenuRenderer.svelte"
const schema = {
name: {
sortable: false,
},
lastUsed: {
displayName: "Last used",
sortable: false,
},
more: {
width: "auto",
displayName: "",
},
}
const customRenderers = [{ column: "more", component: MoreMenuRenderer }]
onMount(() => {
oauth2.fetch()
})
$: configs = $oauth2.configs.map(c => ({
lastUsed: "Never used",
...c,
}))
</script>
<Layout noPadding>
<Layout gap="XS" noPadding>
<div class="header">
<Heading>OAuth2</Heading>
<AddButton />
</div>
<Body
>Manage and configure OAuth 2.0 Client Credentials for secure API access.</Body
>
</Layout>
<Divider />
<Table
data={configs}
loading={$oauth2.loading}
{schema}
{customRenderers}
allowEditRows={false}
allowEditColumns={false}
allowClickRows={false}
/>
</Layout>
<style>
.header {
display: flex;
justify-content: space-between;
}
</style>

View File

@ -37,6 +37,9 @@ import { flags } from "./flags"
import { rowActions } from "./rowActions"
import componentTreeNodesStore from "./componentTreeNodes"
import { appPublished } from "./published"
import { oauth2 } from "./oauth2"
import { FetchAppPackageResponse } from "@budibase/types"
export {
componentTreeNodesStore,
@ -77,6 +80,7 @@ export {
screenComponentsList,
screenComponentErrors,
screenComponentErrorList,
oauth2,
}
export const reset = () => {
@ -106,7 +110,7 @@ const resetBuilderHistory = () => {
automationHistoryStore.reset()
}
export const initialise = async pkg => {
export const initialise = async (pkg: FetchAppPackageResponse) => {
const { application } = pkg
// must be first operation to make sure subsequent requests have correct app ID
appStore.syncAppPackage(pkg)

View File

@ -15,7 +15,7 @@ export class NavigationStore extends BudiStore<AppNavigation> {
super(INITIAL_NAVIGATION_STATE)
}
syncAppNavigation(nav: AppNavigation) {
syncAppNavigation(nav?: AppNavigation) {
this.update(state => ({
...state,
...nav,

View File

@ -0,0 +1,51 @@
import { API } from "@/api"
import { BudiStore } from "@/stores/BudiStore"
import { CreateOAuth2Config } from "@/types"
interface Config {
id: string
name: string
}
interface OAuth2StoreState {
configs: Config[]
loading: boolean
error?: string
}
export class OAuth2Store extends BudiStore<OAuth2StoreState> {
constructor() {
super({
configs: [],
loading: false,
})
}
async fetch() {
this.store.update(store => ({
...store,
loading: true,
}))
try {
const configs = await API.oauth2.fetch()
this.store.update(store => ({
...store,
configs: configs.map(c => ({ id: c.id, name: c.name })),
loading: false,
}))
} catch (e: any) {
this.store.update(store => ({
...store,
loading: false,
error: e.message,
}))
}
}
async create(config: CreateOAuth2Config) {
await API.oauth2.create(config)
await this.fetch()
}
}
export const oauth2 = new OAuth2Store()

View File

@ -1 +1,2 @@
export * from "./bindings"
export * from "./oauth2"

View File

@ -0,0 +1,3 @@
import { CreateOAuth2ConfigRequest } from "@budibase/types"
export interface CreateOAuth2Config extends CreateOAuth2ConfigRequest {}

View File

@ -45,6 +45,7 @@ import { buildAuditLogEndpoints } from "./auditLogs"
import { buildLogsEndpoints } from "./logs"
import { buildMigrationEndpoints } from "./migrations"
import { buildRowActionEndpoints } from "./rowActions"
import { buildOAuth2Endpoints } from "./oauth2"
export type { APIClient } from "./types"
@ -290,5 +291,6 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
...buildMigrationEndpoints(API),
viewV2: buildViewV2Endpoints(API),
rowActions: buildRowActionEndpoints(API),
oauth2: buildOAuth2Endpoints(API),
}
}

View File

@ -0,0 +1,45 @@
import {
FetchOAuth2ConfigsResponse,
CreateOAuth2ConfigResponse,
OAuth2ConfigResponse,
CreateOAuth2ConfigRequest,
} from "@budibase/types"
import { BaseAPIClient } from "./types"
export interface OAuth2Endpoints {
fetch: () => Promise<OAuth2ConfigResponse[]>
create: (
config: CreateOAuth2ConfigRequest
) => Promise<CreateOAuth2ConfigResponse>
}
export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
/**
* Gets all OAuth2 configurations for the app.
* @param tableId the ID of the table
*/
fetch: async () => {
return (
await API.get<FetchOAuth2ConfigsResponse>({
url: `/api/oauth2`,
})
).configs
},
/**
* 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<
CreateOAuth2ConfigRequest,
CreateOAuth2ConfigResponse
>({
url: `/api/oauth2`,
body: {
...config,
},
})
},
})

View File

@ -16,6 +16,7 @@ import { LayoutEndpoints } from "./layouts"
import { LicensingEndpoints } from "./licensing"
import { LogEndpoints } from "./logs"
import { MigrationEndpoints } from "./migrations"
import { OAuth2Endpoints } from "./oauth2"
import { OtherEndpoints } from "./other"
import { PermissionEndpoints } from "./permissions"
import { PluginEndpoins } from "./plugins"
@ -132,4 +133,8 @@ export type APIClient = BaseAPIClient &
TableEndpoints &
TemplateEndpoints &
UserEndpoints &
ViewEndpoints & { rowActions: RowActionEndpoints; viewV2: ViewV2Endpoints }
ViewEndpoints & {
rowActions: RowActionEndpoints
viewV2: ViewV2Endpoints
oauth2: OAuth2Endpoints
}

View File

@ -1,4 +1,4 @@
interface OAuth2ConfigResponse {
export interface OAuth2ConfigResponse {
id: string
name: string
}

View File

@ -1,15 +1,16 @@
export enum FeatureFlag {
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
AI_JS_GENERATION = "AI_JS_GENERATION",
OAUTH2_CONFIG = "OAUTH2_CONFIG",
// Account-portal
DIRECT_LOGIN_TO_ACCOUNT_PORTAL = "DIRECT_LOGIN_TO_ACCOUNT_PORTAL",
AI_JS_GENERATION = "AI_JS_GENERATION",
}
export const FeatureFlagDefaults: Record<FeatureFlag, boolean> = {
[FeatureFlag.USE_ZOD_VALIDATOR]: false,
[FeatureFlag.AI_JS_GENERATION]: false,
[FeatureFlag.OAUTH2_CONFIG]: false,
// Account-portal
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,