Răsfoiți Sursa

feat: permission and security fixes (#5266)

Charles Zhou 1 an în urmă
părinte
comite
cc4a4ec796

+ 15 - 0
api/controllers/console/app/app.py

@@ -129,6 +129,10 @@ class AppApi(Resource):
     @marshal_with(app_detail_fields_with_site)
     @marshal_with(app_detail_fields_with_site)
     def put(self, app_model):
     def put(self, app_model):
         """Update app"""
         """Update app"""
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         parser = reqparse.RequestParser()
         parser = reqparse.RequestParser()
         parser.add_argument('name', type=str, required=True, nullable=False, location='json')
         parser.add_argument('name', type=str, required=True, nullable=False, location='json')
         parser.add_argument('description', type=str, location='json')
         parser.add_argument('description', type=str, location='json')
@@ -147,6 +151,7 @@ class AppApi(Resource):
     @get_app_model
     @get_app_model
     def delete(self, app_model):
     def delete(self, app_model):
         """Delete app"""
         """Delete app"""
+        # The role of the current user in the ta table must be admin, owner, or editor
         if not current_user.is_editor:
         if not current_user.is_editor:
             raise Forbidden()
             raise Forbidden()
 
 
@@ -203,6 +208,10 @@ class AppNameApi(Resource):
     @get_app_model
     @get_app_model
     @marshal_with(app_detail_fields)
     @marshal_with(app_detail_fields)
     def post(self, app_model):
     def post(self, app_model):
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         parser = reqparse.RequestParser()
         parser = reqparse.RequestParser()
         parser.add_argument('name', type=str, required=True, location='json')
         parser.add_argument('name', type=str, required=True, location='json')
         args = parser.parse_args()
         args = parser.parse_args()
@@ -220,6 +229,10 @@ class AppIconApi(Resource):
     @get_app_model
     @get_app_model
     @marshal_with(app_detail_fields)
     @marshal_with(app_detail_fields)
     def post(self, app_model):
     def post(self, app_model):
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         parser = reqparse.RequestParser()
         parser = reqparse.RequestParser()
         parser.add_argument('icon', type=str, location='json')
         parser.add_argument('icon', type=str, location='json')
         parser.add_argument('icon_background', type=str, location='json')
         parser.add_argument('icon_background', type=str, location='json')
@@ -241,6 +254,7 @@ class AppSiteStatus(Resource):
         # The role of the current user in the ta table must be admin, owner, or editor
         # The role of the current user in the ta table must be admin, owner, or editor
         if not current_user.is_editor:
         if not current_user.is_editor:
             raise Forbidden()
             raise Forbidden()
+        
         parser = reqparse.RequestParser()
         parser = reqparse.RequestParser()
         parser.add_argument('enable_site', type=bool, required=True, location='json')
         parser.add_argument('enable_site', type=bool, required=True, location='json')
         args = parser.parse_args()
         args = parser.parse_args()
@@ -261,6 +275,7 @@ class AppApiStatus(Resource):
         # The role of the current user in the ta table must be admin or owner
         # The role of the current user in the ta table must be admin or owner
         if not current_user.is_admin_or_owner:
         if not current_user.is_admin_or_owner:
             raise Forbidden()
             raise Forbidden()
+        
         parser = reqparse.RequestParser()
         parser = reqparse.RequestParser()
         parser.add_argument('enable_api', type=bool, required=True, location='json')
         parser.add_argument('enable_api', type=bool, required=True, location='json')
         args = parser.parse_args()
         args = parser.parse_args()

+ 53 - 1
api/controllers/console/app/workflow.py

@@ -3,7 +3,7 @@ import logging
 
 
 from flask import abort, request
 from flask import abort, request
 from flask_restful import Resource, marshal_with, reqparse
 from flask_restful import Resource, marshal_with, reqparse
-from werkzeug.exceptions import InternalServerError, NotFound
+from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
 
 
 import services
 import services
 from controllers.console import api
 from controllers.console import api
@@ -36,6 +36,10 @@ class DraftWorkflowApi(Resource):
         """
         """
         Get draft workflow
         Get draft workflow
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         # fetch draft workflow by app_model
         # fetch draft workflow by app_model
         workflow_service = WorkflowService()
         workflow_service = WorkflowService()
         workflow = workflow_service.get_draft_workflow(app_model=app_model)
         workflow = workflow_service.get_draft_workflow(app_model=app_model)
@@ -54,6 +58,10 @@ class DraftWorkflowApi(Resource):
         """
         """
         Sync draft workflow
         Sync draft workflow
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         content_type = request.headers.get('Content-Type')
         content_type = request.headers.get('Content-Type')
 
 
         if 'application/json' in content_type:
         if 'application/json' in content_type:
@@ -110,6 +118,10 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
         """
         """
         Run draft workflow
         Run draft workflow
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         parser = reqparse.RequestParser()
         parser = reqparse.RequestParser()
         parser.add_argument('inputs', type=dict, location='json')
         parser.add_argument('inputs', type=dict, location='json')
         parser.add_argument('query', type=str, required=True, location='json', default='')
         parser.add_argument('query', type=str, required=True, location='json', default='')
@@ -146,6 +158,10 @@ class AdvancedChatDraftRunIterationNodeApi(Resource):
         """
         """
         Run draft workflow iteration node
         Run draft workflow iteration node
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         parser = reqparse.RequestParser()
         parser = reqparse.RequestParser()
         parser.add_argument('inputs', type=dict, location='json')
         parser.add_argument('inputs', type=dict, location='json')
         args = parser.parse_args()
         args = parser.parse_args()
@@ -179,6 +195,10 @@ class WorkflowDraftRunIterationNodeApi(Resource):
         """
         """
         Run draft workflow iteration node
         Run draft workflow iteration node
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         parser = reqparse.RequestParser()
         parser = reqparse.RequestParser()
         parser.add_argument('inputs', type=dict, location='json')
         parser.add_argument('inputs', type=dict, location='json')
         args = parser.parse_args()
         args = parser.parse_args()
@@ -212,6 +232,10 @@ class DraftWorkflowRunApi(Resource):
         """
         """
         Run draft workflow
         Run draft workflow
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         parser = reqparse.RequestParser()
         parser = reqparse.RequestParser()
         parser.add_argument('inputs', type=dict, required=True, nullable=False, location='json')
         parser.add_argument('inputs', type=dict, required=True, nullable=False, location='json')
         parser.add_argument('files', type=list, required=False, location='json')
         parser.add_argument('files', type=list, required=False, location='json')
@@ -243,6 +267,10 @@ class WorkflowTaskStopApi(Resource):
         """
         """
         Stop workflow task
         Stop workflow task
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         AppQueueManager.set_stop_flag(task_id, InvokeFrom.DEBUGGER, current_user.id)
         AppQueueManager.set_stop_flag(task_id, InvokeFrom.DEBUGGER, current_user.id)
 
 
         return {
         return {
@@ -260,6 +288,10 @@ class DraftWorkflowNodeRunApi(Resource):
         """
         """
         Run draft workflow node
         Run draft workflow node
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         parser = reqparse.RequestParser()
         parser = reqparse.RequestParser()
         parser.add_argument('inputs', type=dict, required=True, nullable=False, location='json')
         parser.add_argument('inputs', type=dict, required=True, nullable=False, location='json')
         args = parser.parse_args()
         args = parser.parse_args()
@@ -286,6 +318,10 @@ class PublishedWorkflowApi(Resource):
         """
         """
         Get published workflow
         Get published workflow
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         # fetch published workflow by app_model
         # fetch published workflow by app_model
         workflow_service = WorkflowService()
         workflow_service = WorkflowService()
         workflow = workflow_service.get_published_workflow(app_model=app_model)
         workflow = workflow_service.get_published_workflow(app_model=app_model)
@@ -301,6 +337,10 @@ class PublishedWorkflowApi(Resource):
         """
         """
         Publish workflow
         Publish workflow
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         workflow_service = WorkflowService()
         workflow_service = WorkflowService()
         workflow = workflow_service.publish_workflow(app_model=app_model, account=current_user)
         workflow = workflow_service.publish_workflow(app_model=app_model, account=current_user)
 
 
@@ -319,6 +359,10 @@ class DefaultBlockConfigsApi(Resource):
         """
         """
         Get default block config
         Get default block config
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         # Get default block configs
         # Get default block configs
         workflow_service = WorkflowService()
         workflow_service = WorkflowService()
         return workflow_service.get_default_block_configs()
         return workflow_service.get_default_block_configs()
@@ -333,6 +377,10 @@ class DefaultBlockConfigApi(Resource):
         """
         """
         Get default block config
         Get default block config
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         parser = reqparse.RequestParser()
         parser = reqparse.RequestParser()
         parser.add_argument('q', type=str, location='args')
         parser.add_argument('q', type=str, location='args')
         args = parser.parse_args()
         args = parser.parse_args()
@@ -363,6 +411,10 @@ class ConvertToWorkflowApi(Resource):
         Convert expert mode of chatbot app to workflow mode
         Convert expert mode of chatbot app to workflow mode
         Convert Completion App to Workflow App
         Convert Completion App to Workflow App
         """
         """
+        # The role of the current user in the ta table must be admin, owner, or editor
+        if not current_user.is_editor:
+            raise Forbidden()
+        
         if request.data:
         if request.data:
             parser = reqparse.RequestParser()
             parser = reqparse.RequestParser()
             parser.add_argument('name', type=str, required=False, nullable=True, location='json')
             parser.add_argument('name', type=str, required=False, nullable=True, location='json')

+ 12 - 6
api/controllers/console/auth/data_source_bearer_auth.py

@@ -16,15 +16,21 @@ class ApiKeyAuthDataSource(Resource):
     @login_required
     @login_required
     @account_initialization_required
     @account_initialization_required
     def get(self):
     def get(self):
-        # The role of the current user in the table must be admin or owner
-        if not current_user.is_admin_or_owner:
-            raise Forbidden()
         data_source_api_key_bindings = ApiKeyAuthService.get_provider_auth_list(current_user.current_tenant_id)
         data_source_api_key_bindings = ApiKeyAuthService.get_provider_auth_list(current_user.current_tenant_id)
         if data_source_api_key_bindings:
         if data_source_api_key_bindings:
             return {
             return {
-                'settings': [data_source_api_key_binding.to_dict() for data_source_api_key_binding in
+                'sources': [{
-                             data_source_api_key_bindings]}
+                                'id': data_source_api_key_binding.id,
-        return {'settings': []}
+                                'category': data_source_api_key_binding.category,
+                                'provider': data_source_api_key_binding.provider,
+                                'disabled': data_source_api_key_binding.disabled,
+                                'created_at': int(data_source_api_key_binding.created_at.timestamp()),
+                                'updated_at': int(data_source_api_key_binding.updated_at.timestamp()),
+                            }
+                            for data_source_api_key_binding in
+                             data_source_api_key_bindings]
+            }
+        return {'sources': []}
 
 
 
 
 class ApiKeyAuthDataSourceBinding(Resource):
 class ApiKeyAuthDataSourceBinding(Resource):

+ 19 - 19
web/app/(commonLayout)/apps/AppCard.tsx

@@ -279,27 +279,27 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
           'items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
           'items-center shrink-0 mt-1 pt-1 pl-[14px] pr-[6px] pb-[6px] h-[42px]',
           tags.length ? 'flex' : '!hidden group-hover:!flex',
           tags.length ? 'flex' : '!hidden group-hover:!flex',
         )}>
         )}>
-          <div className={cn('grow flex items-center gap-1 w-0')} onClick={(e) => {
-            e.stopPropagation()
-            e.preventDefault()
-          }}>
-            <div className={cn(
-              'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
-              tags.length ? '!block' : '!hidden',
-            )}>
-              <TagSelector
-                position='bl'
-                type='app'
-                targetID={app.id}
-                value={tags.map(tag => tag.id)}
-                selectedTags={tags}
-                onCacheUpdate={setTags}
-                onChange={onRefresh}
-              />
-            </div>
-          </div>
           {isCurrentWorkspaceEditor && (
           {isCurrentWorkspaceEditor && (
             <>
             <>
+              <div className={cn('grow flex items-center gap-1 w-0')} onClick={(e) => {
+                e.stopPropagation()
+                e.preventDefault()
+              }}>
+                <div className={cn(
+                  'group-hover:!block group-hover:!mr-0 mr-[41px] grow w-full',
+                  tags.length ? '!block' : '!hidden',
+                )}>
+                  <TagSelector
+                    position='bl'
+                    type='app'
+                    targetID={app.id}
+                    value={tags.map(tag => tag.id)}
+                    selectedTags={tags}
+                    onCacheUpdate={setTags}
+                    onChange={onRefresh}
+                  />
+                </div>
+              </div>
               <div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200'/>
               <div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200'/>
               <div className='!hidden group-hover:!flex shrink-0'>
               <div className='!hidden group-hover:!flex shrink-0'>
                 <CustomPopover
                 <CustomPopover

+ 9 - 4
web/app/components/app-sidebar/app-info.tsx

@@ -16,7 +16,7 @@ import Divider from '@/app/components/base/divider'
 import Confirm from '@/app/components/base/confirm'
 import Confirm from '@/app/components/base/confirm'
 import { useStore as useAppStore } from '@/app/components/app/store'
 import { useStore as useAppStore } from '@/app/components/app/store'
 import { ToastContext } from '@/app/components/base/toast'
 import { ToastContext } from '@/app/components/base/toast'
-import AppsContext from '@/context/app-context'
+import AppsContext, { useAppContext } from '@/context/app-context'
 import { useProviderContext } from '@/context/provider-context'
 import { useProviderContext } from '@/context/provider-context'
 import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
 import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
 import DuplicateAppModal from '@/app/components/app/duplicate-modal'
 import DuplicateAppModal from '@/app/components/app/duplicate-modal'
@@ -142,6 +142,8 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
     setShowConfirmDelete(false)
     setShowConfirmDelete(false)
   }, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, t])
   }, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, t])
 
 
+  const { isCurrentWorkspaceEditor } = useAppContext()
+
   if (!appDetail)
   if (!appDetail)
     return null
     return null
 
 
@@ -154,10 +156,13 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
     >
     >
       <div className='relative'>
       <div className='relative'>
         <PortalToFollowElemTrigger
         <PortalToFollowElemTrigger
-          onClick={() => setOpen(v => !v)}
+          onClick={() => {
+            if (isCurrentWorkspaceEditor)
+              setOpen(v => !v)
+          }}
           className='block'
           className='block'
         >
         >
-          <div className={cn('flex cursor-pointer p-1 rounded-lg hover:bg-gray-100', open && 'bg-gray-100')}>
+          <div className={cn('flex p-1 rounded-lg', open && 'bg-gray-100', isCurrentWorkspaceEditor && 'hover:bg-gray-100 cursor-pointer')}>
             <div className='relative shrink-0 mr-2'>
             <div className='relative shrink-0 mr-2'>
               <AppIcon size={expand ? 'large' : 'small'} icon={appDetail.icon} background={appDetail.icon_background} />
               <AppIcon size={expand ? 'large' : 'small'} icon={appDetail.icon} background={appDetail.icon_background} />
               <span className={cn(
               <span className={cn(
@@ -185,7 +190,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
               <div className="grow w-0">
               <div className="grow w-0">
                 <div className='flex justify-between items-center text-sm leading-5 font-medium text-gray-900'>
                 <div className='flex justify-between items-center text-sm leading-5 font-medium text-gray-900'>
                   <div className='truncate' title={appDetail.name}>{appDetail.name}</div>
                   <div className='truncate' title={appDetail.name}>{appDetail.name}</div>
-                  <ChevronDown className='shrink-0 ml-[2px] w-3 h-3 text-gray-500' />
+                  {isCurrentWorkspaceEditor && <ChevronDown className='shrink-0 ml-[2px] w-3 h-3 text-gray-500' />}
                 </div>
                 </div>
                 <div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
                 <div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
                   {appDetail.mode === 'advanced-chat' && (
                   {appDetail.mode === 'advanced-chat' && (

+ 5 - 5
web/app/components/datasets/create/website/index.tsx

@@ -5,8 +5,8 @@ import NoData from './no-data'
 import Firecrawl from './firecrawl'
 import Firecrawl from './firecrawl'
 import { useModalContext } from '@/context/modal-context'
 import { useModalContext } from '@/context/modal-context'
 import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
 import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
-import { fetchFirecrawlApiKey } from '@/service/datasets'
+import { fetchDataSources } from '@/service/datasets'
-import { type DataSourceWebsiteItem, WebsiteProvider } from '@/models/common'
+import { type DataSourceItem, DataSourceProvider } from '@/models/common'
 
 
 type Props = {
 type Props = {
   onPreview: (payload: CrawlResultItem) => void
   onPreview: (payload: CrawlResultItem) => void
@@ -29,9 +29,9 @@ const Website: FC<Props> = ({
   const [isLoaded, setIsLoaded] = useState(false)
   const [isLoaded, setIsLoaded] = useState(false)
   const [isSetFirecrawlApiKey, setIsSetFirecrawlApiKey] = useState(false)
   const [isSetFirecrawlApiKey, setIsSetFirecrawlApiKey] = useState(false)
   const checkSetApiKey = useCallback(async () => {
   const checkSetApiKey = useCallback(async () => {
-    const res = await fetchFirecrawlApiKey() as any
+    const res = await fetchDataSources() as any
-    const list = res.settings.filter((item: DataSourceWebsiteItem) => item.provider === WebsiteProvider.fireCrawl && !item.disabled)
+    const isFirecrawlSet = res.sources.some((item: DataSourceItem) => item.provider === DataSourceProvider.fireCrawl)
-    setIsSetFirecrawlApiKey(list.length > 0)
+    setIsSetFirecrawlApiKey(isFirecrawlSet)
   }, [])
   }, [])
 
 
   useEffect(() => {
   useEffect(() => {

+ 1 - 1
web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx

@@ -58,7 +58,7 @@ const DataSourceNotion: FC<Props> = ({
       type={DataSourceType.notion}
       type={DataSourceType.notion}
       isConfigured={connected}
       isConfigured={connected}
       onConfigure={handleConnectNotion}
       onConfigure={handleConnectNotion}
-      readonly={!isCurrentWorkspaceManager}
+      readOnly={!isCurrentWorkspaceManager}
       isSupportList
       isSupportList
       configuredList={workspaces.map(workspace => ({
       configuredList={workspaces.map(workspace => ({
         id: workspace.id,
         id: workspace.id,

+ 2 - 2
web/app/components/header/account-setting/data-source-page/data-source-website/config-firecrawl-modal.tsx

@@ -11,7 +11,7 @@ import Button from '@/app/components/base/button'
 import type { FirecrawlConfig } from '@/models/common'
 import type { FirecrawlConfig } from '@/models/common'
 import Field from '@/app/components/datasets/create/website/firecrawl/base/field'
 import Field from '@/app/components/datasets/create/website/firecrawl/base/field'
 import Toast from '@/app/components/base/toast'
 import Toast from '@/app/components/base/toast'
-import { createFirecrawlApiKey } from '@/service/datasets'
+import { createDataSourceApiKeyBinding } from '@/service/datasets'
 import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
 import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
 type Props = {
 type Props = {
   onCancel: () => void
   onCancel: () => void
@@ -76,7 +76,7 @@ const ConfigFirecrawlModal: FC<Props> = ({
     }
     }
     try {
     try {
       setIsSaving(true)
       setIsSaving(true)
-      await createFirecrawlApiKey(postData)
+      await createDataSourceApiKeyBinding(postData)
       Toast.notify({
       Toast.notify({
         type: 'success',
         type: 'success',
         message: t('common.api.success'),
         message: t('common.api.success'),

+ 29 - 19
web/app/components/header/account-setting/data-source-page/data-source-website/index.tsx

@@ -7,15 +7,15 @@ import cn from 'classnames'
 import Panel from '../panel'
 import Panel from '../panel'
 import { DataSourceType } from '../panel/types'
 import { DataSourceType } from '../panel/types'
 import ConfigFirecrawlModal from './config-firecrawl-modal'
 import ConfigFirecrawlModal from './config-firecrawl-modal'
-import { fetchFirecrawlApiKey, removeFirecrawlApiKey } from '@/service/datasets'
+import { fetchDataSources, removeDataSourceApiKeyBinding } from '@/service/datasets'
 
 
 import type {
 import type {
-  DataSourceWebsiteItem,
+  DataSourceItem,
 } from '@/models/common'
 } from '@/models/common'
 import { useAppContext } from '@/context/app-context'
 import { useAppContext } from '@/context/app-context'
 
 
 import {
 import {
-  WebsiteProvider,
+  DataSourceProvider,
 } from '@/models/common'
 } from '@/models/common'
 import Toast from '@/app/components/base/toast'
 import Toast from '@/app/components/base/toast'
 
 
@@ -24,11 +24,11 @@ type Props = {}
 const DataSourceWebsite: FC<Props> = () => {
 const DataSourceWebsite: FC<Props> = () => {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const { isCurrentWorkspaceManager } = useAppContext()
   const { isCurrentWorkspaceManager } = useAppContext()
-  const [list, setList] = useState<DataSourceWebsiteItem[]>([])
+  const [sources, setSources] = useState<DataSourceItem[]>([])
   const checkSetApiKey = useCallback(async () => {
   const checkSetApiKey = useCallback(async () => {
-    const res = await fetchFirecrawlApiKey() as any
+    const res = await fetchDataSources() as any
-    const list = res.settings.filter((item: DataSourceWebsiteItem) => item.provider === WebsiteProvider.fireCrawl && !item.disabled)
+    const list = res.sources
-    setList(list)
+    setSources(list)
   }, [])
   }, [])
 
 
   useEffect(() => {
   useEffect(() => {
@@ -46,23 +46,33 @@ const DataSourceWebsite: FC<Props> = () => {
     hideConfig()
     hideConfig()
   }, [checkSetApiKey, hideConfig])
   }, [checkSetApiKey, hideConfig])
 
 
-  const handleRemove = useCallback(async () => {
+  const getIdByProvider = (provider: string): string | undefined => {
-    await removeFirecrawlApiKey(list[0].id)
+    const source = sources.find(item => item.provider === provider)
-    setList([])
+    return source?.id
-    Toast.notify({
+  }
-      type: 'success',
+
-      message: t('common.api.remove'),
+  const handleRemove = useCallback((provider: string) => {
-    })
+    return async () => {
-  }, [list, t])
+      const dataSourceId = getIdByProvider(provider)
+      if (dataSourceId) {
+        await removeDataSourceApiKeyBinding(dataSourceId)
+        setSources(sources.filter(item => item.provider !== provider))
+        Toast.notify({
+          type: 'success',
+          message: t('common.api.remove'),
+        })
+      }
+    }
+  }, [sources, t])
 
 
   return (
   return (
     <>
     <>
       <Panel
       <Panel
         type={DataSourceType.website}
         type={DataSourceType.website}
-        isConfigured={list.length > 0}
+        isConfigured={sources.length > 0}
         onConfigure={showConfig}
         onConfigure={showConfig}
-        readonly={!isCurrentWorkspaceManager}
+        readOnly={!isCurrentWorkspaceManager}
-        configuredList={list.map(item => ({
+        configuredList={sources.map(item => ({
           id: item.id,
           id: item.id,
           logo: ({ className }: { className: string }) => (
           logo: ({ className }: { className: string }) => (
             <div className={cn(className, 'flex items-center justify-center w-5 h-5 bg-white border border-gray-100 text-xs font-medium text-gray-500 rounded ml-3')}>🔥</div>
             <div className={cn(className, 'flex items-center justify-center w-5 h-5 bg-white border border-gray-100 text-xs font-medium text-gray-500 rounded ml-3')}>🔥</div>
@@ -70,7 +80,7 @@ const DataSourceWebsite: FC<Props> = () => {
           name: 'FireCrawl',
           name: 'FireCrawl',
           isActive: true,
           isActive: true,
         }))}
         }))}
-        onRemove={handleRemove}
+        onRemove={handleRemove(DataSourceProvider.fireCrawl)}
       />
       />
       {isShowConfig && (
       {isShowConfig && (
         <ConfigFirecrawlModal onSaved={handleAdded} onCancel={hideConfig} />
         <ConfigFirecrawlModal onSaved={handleAdded} onCancel={hideConfig} />

+ 3 - 1
web/app/components/header/account-setting/data-source-page/panel/config-item.tsx

@@ -26,6 +26,7 @@ type Props = {
   notionActions?: {
   notionActions?: {
     onChangeAuthorizedPage: () => void
     onChangeAuthorizedPage: () => void
   }
   }
+  readOnly: boolean
 }
 }
 
 
 const ConfigItem: FC<Props> = ({
 const ConfigItem: FC<Props> = ({
@@ -33,6 +34,7 @@ const ConfigItem: FC<Props> = ({
   payload,
   payload,
   onRemove,
   onRemove,
   notionActions,
   notionActions,
+  readOnly,
 }) => {
 }) => {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const isNotion = type === DataSourceType.notion
   const isNotion = type === DataSourceType.notion
@@ -65,7 +67,7 @@ const ConfigItem: FC<Props> = ({
       )}
       )}
 
 
       {
       {
-        isWebsite && (
+        isWebsite && !readOnly && (
           <div className='p-2 text-gray-500 cursor-pointer rounded-md hover:bg-black/5' onClick={onRemove} >
           <div className='p-2 text-gray-500 cursor-pointer rounded-md hover:bg-black/5' onClick={onRemove} >
             <Trash03 className='w-4 h-4 ' />
             <Trash03 className='w-4 h-4 ' />
           </div>
           </div>

+ 28 - 28
web/app/components/header/account-setting/data-source-page/panel/index.tsx

@@ -14,7 +14,7 @@ type Props = {
   type: DataSourceType
   type: DataSourceType
   isConfigured: boolean
   isConfigured: boolean
   onConfigure: () => void
   onConfigure: () => void
-  readonly: boolean
+  readOnly: boolean
   isSupportList?: boolean
   isSupportList?: boolean
   configuredList: ConfigItemType[]
   configuredList: ConfigItemType[]
   onRemove: () => void
   onRemove: () => void
@@ -27,7 +27,7 @@ const Panel: FC<Props> = ({
   type,
   type,
   isConfigured,
   isConfigured,
   onConfigure,
   onConfigure,
-  readonly,
+  readOnly,
   configuredList,
   configuredList,
   isSupportList,
   isSupportList,
   onRemove,
   onRemove,
@@ -67,7 +67,7 @@ const Panel: FC<Props> = ({
                     className={
                     className={
                       `flex items-center ml-3 px-3 h-7 bg-white border border-gray-200
                       `flex items-center ml-3 px-3 h-7 bg-white border border-gray-200
                   rounded-md text-xs font-medium text-gray-700
                   rounded-md text-xs font-medium text-gray-700
-                  ${!readonly ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
+                  ${!readOnly ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
                     }
                     }
                     onClick={onConfigure}
                     onClick={onConfigure}
                   >
                   >
@@ -79,7 +79,7 @@ const Panel: FC<Props> = ({
                     {isSupportList && <div
                     {isSupportList && <div
                       className={
                       className={
                         `flex items-center px-3 py-1 min-h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md
                         `flex items-center px-3 py-1 min-h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md
-                  ${!readonly ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
+                  ${!readOnly ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
                       }
                       }
                       onClick={onConfigure}
                       onClick={onConfigure}
                     >
                     >
@@ -96,10 +96,10 @@ const Panel: FC<Props> = ({
           <div
           <div
             className={
             className={
               `flex items-center ml-3 px-3 h-7 bg-white border border-gray-200
               `flex items-center ml-3 px-3 h-7 bg-white border border-gray-200
-        rounded-md text-xs font-medium text-gray-700
+              rounded-md text-xs font-medium text-gray-700
-        ${!readonly ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
+              ${!readOnly ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
             }
             }
-            onClick={onConfigure}
+            onClick={!readOnly ? onConfigure : undefined}
           >
           >
             {t('common.dataSource.configure')}
             {t('common.dataSource.configure')}
           </div>
           </div>
@@ -108,28 +108,28 @@ const Panel: FC<Props> = ({
       </div>
       </div>
       {
       {
         isConfigured && (
         isConfigured && (
-          <div className='flex items-center px-3 h-[18px]'>
+          <>
-            <div className='text-xs font-medium text-gray-500'>
+            <div className='flex items-center px-3 h-[18px]'>
-              {isNotion ? t('common.dataSource.notion.connectedWorkspace') : t('common.dataSource.website.configuredCrawlers')}
+              <div className='text-xs font-medium text-gray-500'>
+                {isNotion ? t('common.dataSource.notion.connectedWorkspace') : t('common.dataSource.website.configuredCrawlers')}
+              </div>
+              <div className='grow ml-3 border-t border-t-gray-100' />
             </div>
             </div>
-            <div className='grow ml-3 border-t border-t-gray-100' />
+            <div className='px-3 pt-2 pb-3'>
-          </div>
+              {
-        )
+                configuredList.map(item => (
-      }
+                  <ConfigItem
-      {
+                    key={item.id}
-        isConfigured && (
+                    type={type}
-          <div className='px-3 pt-2 pb-3'>
+                    payload={item}
-            {
+                    onRemove={onRemove}
-              configuredList.map(item => (
+                    notionActions={notionActions}
-                <ConfigItem
+                    readOnly={readOnly}
-                  key={item.id}
+                  />
-                  type={type}
+                ))
-                  payload={item}
+              }
-                  onRemove={onRemove}
+            </div>
-                  notionActions={notionActions} />
+          </>
-              ))
-            }
-          </div>
         )
         )
       }
       }
     </div>
     </div>

+ 7 - 15
web/models/common.ts

@@ -175,34 +175,26 @@ export type DataSourceNotion = {
 export enum DataSourceCategory {
 export enum DataSourceCategory {
   website = 'website',
   website = 'website',
 }
 }
-export enum WebsiteProvider {
+export enum DataSourceProvider {
   fireCrawl = 'firecrawl',
   fireCrawl = 'firecrawl',
 }
 }
 
 
-export type WebsiteCredentials = {
-  auth_type: 'bearer'
-  config: {
-    base_url: string
-    api_key: string
-  }
-}
-
 export type FirecrawlConfig = {
 export type FirecrawlConfig = {
   api_key: string
   api_key: string
   base_url: string
   base_url: string
 }
 }
 
 
-export type DataSourceWebsiteItem = {
+export type DataSourceItem = {
   id: string
   id: string
-  category: DataSourceCategory.website
+  category: DataSourceCategory
-  provider: WebsiteProvider
+  provider: DataSourceProvider
-  credentials: WebsiteCredentials
   disabled: boolean
   disabled: boolean
   created_at: number
   created_at: number
   updated_at: number
   updated_at: number
 }
 }
-export type DataSourceWebsite = {
+
-  settings: DataSourceWebsiteItem[]
+export type DataSources = {
+  sources: DataSourceItem[]
 }
 }
 
 
 export type GithubRepo = {
 export type GithubRepo = {

+ 3 - 3
web/service/datasets.ts

@@ -231,15 +231,15 @@ export const fetchDatasetApiBaseUrl: Fetcher<{ api_base_url: string }, string> =
   return get<{ api_base_url: string }>(url)
   return get<{ api_base_url: string }>(url)
 }
 }
 
 
-export const fetchFirecrawlApiKey = () => {
+export const fetchDataSources = () => {
   return get<CommonResponse>('api-key-auth/data-source')
   return get<CommonResponse>('api-key-auth/data-source')
 }
 }
 
 
-export const createFirecrawlApiKey: Fetcher<CommonResponse, Record<string, any>> = (body) => {
+export const createDataSourceApiKeyBinding: Fetcher<CommonResponse, Record<string, any>> = (body) => {
   return post<CommonResponse>('api-key-auth/data-source/binding', { body })
   return post<CommonResponse>('api-key-auth/data-source/binding', { body })
 }
 }
 
 
-export const removeFirecrawlApiKey: Fetcher<CommonResponse, string> = (id: string) => {
+export const removeDataSourceApiKeyBinding: Fetcher<CommonResponse, string> = (id: string) => {
   return del<CommonResponse>(`api-key-auth/data-source/${id}`)
   return del<CommonResponse>(`api-key-auth/data-source/${id}`)
 }
 }