From b7d9483bc2756a7586a9463035ab48fe963ee59c Mon Sep 17 00:00:00 2001 From: zhsama Date: Tue, 25 Nov 2025 17:24:36 +0800 Subject: [PATCH] feat(end-user): implement end-user credential management in plugin-auth component - Added support for end-user credentials with options for OAuth and API Key. - Introduced new props in PluginAuth for managing end-user credential types and their states. - Updated workflow types to include end-user credential fields. - Enhanced UI to allow users to select and manage end-user credentials. - Added translations for new UI elements related to end-user credentials. --- .../plugins/plugin-auth/plugin-auth.tsx | 325 +++++++++++++++++- .../workflow/block-selector/types.ts | 4 + .../_base/components/workflow-panel/index.tsx | 21 ++ web/app/components/workflow/types.ts | 2 + web/i18n/en-US/plugin.ts | 13 + 5 files changed, 362 insertions(+), 3 deletions(-) diff --git a/web/app/components/plugins/plugin-auth/plugin-auth.tsx b/web/app/components/plugins/plugin-auth/plugin-auth.tsx index a9bb287cdf..0d34fcefc9 100644 --- a/web/app/components/plugins/plugin-auth/plugin-auth.tsx +++ b/web/app/components/plugins/plugin-auth/plugin-auth.tsx @@ -1,20 +1,56 @@ -import { memo } from 'react' +import { + memo, + useCallback, + useEffect, + useMemo, + useState, +} from 'react' +import type { ReactNode } from 'react' +import { + RiAddLine, + RiArrowDownSLine, + RiCheckLine, + RiEqualizer2Line, + RiKey2Line, + RiUserStarLine, +} from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' import Authorize from './authorize' import Authorized from './authorized' +import AddApiKeyButton from './authorize/add-api-key-button' +import AddOAuthButton from './authorize/add-oauth-button' +import Item from './authorized/item' import type { PluginPayload } from './types' import { usePluginAuth } from './hooks/use-plugin-auth' +import Switch from '@/app/components/base/switch' import cn from '@/utils/classnames' type PluginAuthProps = { pluginPayload: PluginPayload children?: React.ReactNode className?: string + showConnectGuide?: boolean + endUserCredentialEnabled?: boolean + endUserCredentialType?: string + onEndUserCredentialTypeChange?: (type: string) => void + onEndUserCredentialChange?: (enabled: boolean) => void } const PluginAuth = ({ pluginPayload, children, className, + showConnectGuide, + endUserCredentialEnabled, + endUserCredentialType, + onEndUserCredentialTypeChange, + onEndUserCredentialChange, }: PluginAuthProps) => { + const { t } = useTranslation() const { isAuthorized, canOAuth, @@ -24,11 +60,294 @@ const PluginAuth = ({ invalidPluginCredentialInfo, notAllowCustomCredential, } = usePluginAuth(pluginPayload, !!pluginPayload.provider) + const shouldShowGuide = !!showConnectGuide + const [showCredentialPanel, setShowCredentialPanel] = useState(false) + const [showAddMenu, setShowAddMenu] = useState(false) + const [showEndUserTypeMenu, setShowEndUserTypeMenu] = useState(false) + const configuredDisabled = !!endUserCredentialEnabled + const availableEndUserTypes = useMemo(() => { + const list: { value: string; label: string; icon: ReactNode }[] = [] + if (canOAuth) { + list.push({ + value: 'oauth2', + label: t('plugin.auth.endUserCredentials.optionOAuth'), + icon: , + }) + } + if (canApiKey) { + list.push({ + value: 'api-key', + label: t('plugin.auth.endUserCredentials.optionApiKey'), + icon: , + }) + } + return list + }, [canOAuth, canApiKey, t]) + const endUserCredentialLabel = useMemo(() => { + const found = availableEndUserTypes.find(item => item.value === endUserCredentialType) + return found?.label || availableEndUserTypes[0]?.label || '-' + }, [availableEndUserTypes, endUserCredentialType]) + + useEffect(() => { + if (!endUserCredentialEnabled) { + setShowEndUserTypeMenu(false) + return + } + if (!availableEndUserTypes.length) + return + const isValid = availableEndUserTypes.some(item => item.value === endUserCredentialType) + if (!isValid) + onEndUserCredentialTypeChange?.(availableEndUserTypes[0].value) + }, [endUserCredentialEnabled, endUserCredentialType, availableEndUserTypes, onEndUserCredentialTypeChange]) + + const handleSelectEndUserType = useCallback((value: string) => { + onEndUserCredentialTypeChange?.(value) + setShowEndUserTypeMenu(false) + }, [onEndUserCredentialTypeChange]) + const containerClassName = useMemo(() => { + if (showConnectGuide) + return className + return !isAuthorized ? className : undefined + }, [isAuthorized, className, showConnectGuide]) + + useEffect(() => { + if (isAuthorized) + setShowCredentialPanel(false) + }, [isAuthorized]) + + const credentialList = useMemo(() => { + return ( +
+ { + credentials.length > 0 + ? ( +
+ {credentials.map(credential => ( + + ))} +
+ ) + : null + } +
+ ) + }, [credentials, t]) + + const endUserSwitch = ( +
+
+
+ +
+
+
+
+ {t('plugin.auth.endUserCredentials.title')} +
+ +
+
+ {t('plugin.auth.endUserCredentials.desc')} +
+ { + endUserCredentialEnabled && availableEndUserTypes.length > 0 && ( +
+
+ {t('plugin.auth.endUserCredentials.typeLabel')} +
+ + + + + +
+
+ {availableEndUserTypes.map(item => ( + + ))} +
+
+
+
+
+ ) + } +
+
+
+ ) return ( -
+
{ - !isAuthorized && ( + shouldShowGuide && ( + + + + + +
+
+
+
+ +
+
+ {t('plugin.auth.configuredCredentials.title')} +
+
+ {t('plugin.auth.configuredCredentials.desc')} +
+
+
+ + + + + +
+
+ { + canOAuth && ( + { + setShowAddMenu(false) + invalidPluginCredentialInfo() + }} + /> + ) + } + { + canApiKey && ( + { + setShowAddMenu(false) + invalidPluginCredentialInfo() + }} + /> + ) + } +
+
+
+
+
+
+ {credentialList} +
+ { + credentials.length === 0 && ( +
+ +
+ ) + } +
+ {endUserSwitch} +
+
+
+ ) + } + { + !shouldShowGuide && !isAuthorized && ( [] output_schema?: Record credential_id?: string + use_end_user_credentials?: boolean + end_user_credential_type?: string meta?: PluginMeta plugin_id?: string provider_icon?: Collection['icon'] @@ -86,6 +88,8 @@ export type ToolValue = { enabled?: boolean extra?: Record credential_id?: string + use_end_user_credentials?: boolean + end_user_credential_type?: string } export type DataSourceItem = { diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 0d3aebd06d..9e7da37910 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -325,6 +325,22 @@ const BasePanel: FC = ({ }, }) }, [handleNodeDataUpdateWithSyncDraft, id]) + const handleEndUserCredentialChange = useCallback((enabled: boolean) => { + handleNodeDataUpdateWithSyncDraft({ + id, + data: { + use_end_user_credentials: enabled, + }, + }) + }, [handleNodeDataUpdateWithSyncDraft, id]) + const handleEndUserCredentialTypeChange = useCallback((type: string) => { + handleNodeDataUpdateWithSyncDraft({ + id, + data: { + end_user_credential_type: type, + }, + }) + }, [handleNodeDataUpdateWithSyncDraft, id]) const { setShowAccountSettingModal } = useModalContext() @@ -516,6 +532,11 @@ const BasePanel: FC = ({ needsToolAuth && ( = { retry_config?: WorkflowRetryConfig default_value?: DefaultValueForm[] credential_id?: string + use_end_user_credentials?: boolean + end_user_credential_type?: string subscription_id?: string provider_id?: string _dimmed?: boolean diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index 62a5f35c0b..425b6f0d7e 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -308,6 +308,19 @@ const translation = { unavailable: 'Unavailable', connectedWorkspace: 'Connected Workspace', emptyAuth: 'Please configure authentication', + connectCredentials: 'Connect credentials to continue', + configuredCredentials: { + title: 'Configured Credentials', + desc: 'Set up by you or your team in advance', + empty: 'No workspace credentials configured yet', + }, + endUserCredentials: { + title: 'End-user Credentials', + desc: 'Credentials are provided by the end user at runtime', + typeLabel: 'Credential Type', + optionOAuth: 'OAuth', + optionApiKey: 'API Key', + }, }, readmeInfo: { title: 'README',