datasource oauth

This commit is contained in:
zxhlyh
2025-07-21 17:40:19 +08:00
parent 039a053027
commit caa2de3344
23 changed files with 701 additions and 272 deletions

View File

@ -6,6 +6,7 @@ import Button from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/button'
import ApiKeyModal from './api-key-modal'
import type { PluginPayload } from '../types'
import type { FormSchema } from '@/app/components/base/form/types'
export type AddApiKeyButtonProps = {
pluginPayload: PluginPayload
@ -13,13 +14,15 @@ export type AddApiKeyButtonProps = {
buttonText?: string
disabled?: boolean
onUpdate?: () => void
formSchemas?: FormSchema[]
}
const AddApiKeyButton = ({
pluginPayload,
buttonVariant = 'secondary-accent',
buttonText = 'use api key',
buttonText = 'Use Api Key',
disabled,
onUpdate,
formSchemas = [],
}: AddApiKeyButtonProps) => {
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false)
@ -39,6 +42,7 @@ const AddApiKeyButton = ({
pluginPayload={pluginPayload}
onClose={() => setIsApiKeyModalOpen(false)}
onUpdate={onUpdate}
formSchemas={formSchemas}
/>
)
}

View File

@ -36,6 +36,13 @@ export type AddOAuthButtonProps = {
dividerClassName?: string
disabled?: boolean
onUpdate?: () => void
oAuthData?: {
schema?: FormSchema[]
is_oauth_custom_client_enabled?: boolean
is_system_oauth_params_exists?: boolean
client_params?: Record<string, any>
redirect_uri?: string
}
}
const AddOAuthButton = ({
pluginPayload,
@ -47,19 +54,26 @@ const AddOAuthButton = ({
dividerClassName,
disabled,
onUpdate,
oAuthData,
}: AddOAuthButtonProps) => {
const { t } = useTranslation()
const renderI18nObject = useRenderI18nObject()
const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false)
const { mutateAsync: getPluginOAuthUrl } = useGetPluginOAuthUrlHook(pluginPayload)
const { data, isLoading } = useGetPluginOAuthClientSchemaHook(pluginPayload)
const mergedOAuthData = useMemo(() => {
if (oAuthData)
return oAuthData
return data
}, [oAuthData, data])
const {
schema = [],
is_oauth_custom_client_enabled,
is_system_oauth_params_exists,
client_params,
redirect_uri,
} = data || {}
} = mergedOAuthData as any
const isConfigured = is_system_oauth_params_exists || is_oauth_custom_client_enabled
const handleOAuth = useCallback(async () => {
const { authorization_url } = await getPluginOAuthUrl()
@ -112,7 +126,7 @@ const AddOAuthButton = ({
)
}, [t, redirect_uri, renderI18nObject])
const memorizedSchemas = useMemo(() => {
const result: FormSchema[] = schema.map((item, index) => {
const result: FormSchema[] = (schema as FormSchema[]).map((item, index) => {
return {
...item,
label: index === 0 ? renderCustomLabel(item) : item.label,

View File

@ -11,7 +11,10 @@ import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import Modal from '@/app/components/base/modal/modal'
import { CredentialTypeEnum } from '../types'
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
import type { FormRefObject } from '@/app/components/base/form/types'
import type {
FormRefObject,
FormSchema,
} from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types'
import { useToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
@ -30,6 +33,7 @@ export type ApiKeyModalProps = {
onRemove?: () => void
disabled?: boolean
onUpdate?: () => void
formSchemas?: FormSchema[]
}
const ApiKeyModal = ({
pluginPayload,
@ -38,6 +42,7 @@ const ApiKeyModal = ({
onRemove,
disabled,
onUpdate,
formSchemas: formSchemasFromProps = [],
}: ApiKeyModalProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
@ -48,6 +53,12 @@ const ApiKeyModal = ({
setDoingAction(value)
}, [])
const { data = [], isLoading } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY)
const mergedData = useMemo(() => {
if (formSchemasFromProps?.length)
return formSchemasFromProps
return data
}, [formSchemasFromProps, data])
const formSchemas = useMemo(() => {
return [
{
@ -56,9 +67,9 @@ const ApiKeyModal = ({
label: t('plugin.auth.authorizationName'),
required: false,
},
...data,
...mergedData,
]
}, [data, t])
}, [mergedData, t])
const defaultValues = formSchemas.reduce((acc, schema) => {
if (schema.default)
acc[schema.name] = schema.default
@ -165,7 +176,7 @@ const ApiKeyModal = ({
)
}
{
!isLoading && !!data.length && (
!isLoading && !!mergedData.length && (
<AuthForm
ref={formRef}
formSchemas={formSchemas}

View File

@ -0,0 +1,43 @@
import {
memo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiEqualizer2Line } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
type AuthorizedInDataSourceNodeProps = {
authorizationsNum: number
onJumpToDataSourcePage: () => void
}
const AuthorizedInDataSourceNode = ({
authorizationsNum,
onJumpToDataSourcePage,
}: AuthorizedInDataSourceNodeProps) => {
const { t } = useTranslation()
return (
<Button
size='small'
onClick={onJumpToDataSourcePage}
>
<Indicator
className='mr-1.5'
color='green'
/>
{
authorizationsNum > 1
? t('plugin.auth.authorizations')
: t('plugin.auth.authorization')
}
<RiEqualizer2Line
className={cn(
'h-3.5 w-3.5 text-components-button-ghost-text',
)}
/>
</Button>
)
}
export default memo(AuthorizedInDataSourceNode)

View File

@ -24,6 +24,22 @@ export const useGetApi = ({ category = AuthCategory.tool, provider }: PluginPayl
}
}
if (category === AuthCategory.datasource) {
return {
getCredentialInfo: '',
setDefaultCredential: `/auth/plugin/datasource/${provider}/default`,
getCredentials: `/auth/plugin/datasource/${provider}`,
addCredential: `/auth/plugin/datasource/${provider}`,
updateCredential: `/auth/plugin/datasource/${provider}/update`,
deleteCredential: `/auth/plugin/datasource/${provider}/delete`,
getCredentialSchema: () => '',
getOauthUrl: `/oauth/plugin/${provider}/datasource/get-authorization-url`,
getOauthClientSchema: '',
setCustomOauthClient: `/auth/plugin/datasource/${provider}/custom-client`,
deleteCustomOAuthClient: `/auth/plugin/datasource/${provider}/custom-client`,
}
}
return {
getCredentialInfo: '',
setDefaultCredential: '',

View File

@ -0,0 +1,124 @@
import {
useCallback,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useToastContext } from '@/app/components/base/toast'
import type { PluginPayload } from '../types'
import {
useDeletePluginCredentialHook,
useSetPluginDefaultCredentialHook,
useUpdatePluginCredentialHook,
} from '../hooks/use-credential'
export const usePluginAuthAction = (
pluginPayload: PluginPayload,
onUpdate?: () => void,
) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const pendingOperationCredentialId = useRef<string | null>(null)
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
const { mutateAsync: deletePluginCredential } = useDeletePluginCredentialHook(pluginPayload)
const openConfirm = useCallback((credentialId?: string) => {
if (credentialId)
pendingOperationCredentialId.current = credentialId
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const closeConfirm = useCallback(() => {
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}, [])
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((doing: boolean) => {
doingActionRef.current = doing
setDoingAction(doing)
}, [])
const handleConfirm = useCallback(async () => {
if (doingActionRef.current)
return
if (!pendingOperationCredentialId.current) {
setDeleteCredentialId(null)
return
}
try {
handleSetDoingAction(true)
await deletePluginCredential({ credential_id: pendingOperationCredentialId.current })
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}
finally {
handleSetDoingAction(false)
}
}, [deletePluginCredential, onUpdate, notify, t, handleSetDoingAction])
const [editValues, setEditValues] = useState<Record<string, any> | null>(null)
const handleEdit = useCallback((id: string, values: Record<string, any>) => {
pendingOperationCredentialId.current = id
setEditValues(values)
}, [])
const handleRemove = useCallback(() => {
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const { mutateAsync: setPluginDefaultCredential } = useSetPluginDefaultCredentialHook(pluginPayload)
const handleSetDefault = useCallback(async (id: string) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await setPluginDefaultCredential(id)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [setPluginDefaultCredential, onUpdate, notify, t, handleSetDoingAction])
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
const handleRename = useCallback(async (payload: {
credential_id: string
name: string
}) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await updatePluginCredential(payload)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
return {
doingAction,
handleSetDoingAction,
openConfirm,
closeConfirm,
deleteCredentialId,
setDeleteCredentialId,
handleConfirm,
editValues,
setEditValues,
handleEdit,
handleRemove,
handleSetDefault,
handleRename,
pendingOperationCredentialId,
}
}

View File

@ -3,4 +3,10 @@ export { default as Authorized } from './authorized'
export { default as AuthorizedInNode } from './authorized-in-node'
export { default as PluginAuthInAgent } from './plugin-auth-in-agent'
export { usePluginAuth } from './hooks/use-plugin-auth'
export { default as PluginAuthInDataSourceNode } from './plugin-auth-in-datasource-node'
export { default as AuthorizedInDataSourceNode } from './authorized-in-data-source-node'
export { default as AddOAuthButton } from './authorize/add-oauth-button'
export { default as AddApiKeyButton } from './authorize/add-api-key-button'
export { default as ApiKeyModal } from './authorize/api-key-modal'
export * from './hooks/use-plugin-auth-action'
export * from './types'

View File

@ -0,0 +1,39 @@
import { memo } from 'react'
import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
type PluginAuthInDataSourceNodeProps = {
children?: ReactNode
isAuthorized?: boolean
onJumpToDataSourcePage: () => void
}
const PluginAuthInDataSourceNode = ({
children,
isAuthorized,
onJumpToDataSourcePage,
}: PluginAuthInDataSourceNodeProps) => {
const { t } = useTranslation()
return (
<>
{
!isAuthorized && (
<div className='px-4 pb-2'>
<Button
className='w-full'
variant='primary'
onClick={onJumpToDataSourcePage}
>
<RiAddLine className='mr-1 h-4 w-4' />
{t('common.integrations.connect')}
</Button>
</div>
)
}
{isAuthorized && children}
</>
)
}
export default memo(PluginAuthInDataSourceNode)

View File

@ -1,3 +1,6 @@
export type { AddApiKeyButtonProps } from './authorize/add-api-key-button'
export type { AddOAuthButtonProps } from './authorize/add-oauth-button'
export enum AuthCategory {
tool = 'tool',
datasource = 'datasource',