mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-14 21:25:52 +08:00
feat: permission and security fixes (#5266)
This commit is contained in:
parent
a1d8c86ee3
commit
cc4a4ec796
@ -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()
|
||||||
|
@ -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')
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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,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(() => {
|
||||||
|
@ -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,
|
||||||
|
@ -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'),
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 = {
|
||||||
|
@ -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}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user