feat: permission and security fixes (#5266)

This commit is contained in:
Charles Zhou 2024-06-17 03:06:32 -05:00 committed by GitHub
parent a1d8c86ee3
commit cc4a4ec796
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 186 additions and 104 deletions

View File

@ -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()

View File

@ -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')

View File

@ -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):

View File

@ -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

View File

@ -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' && (

View File

@ -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(() => {

View File

@ -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,

View File

@ -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'),

View File

@ -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} />

View File

@ -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>

View File

@ -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>

View File

@ -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 = {

View File

@ -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}`)
} }