From 080776c516dd7afb3e084067641adff54aabf0a0 Mon Sep 17 00:00:00 2001 From: NFish Date: Mon, 21 Apr 2025 15:36:46 +0800 Subject: [PATCH] feat: check user permission before publish web app --- .../components/app/app-publisher/index.tsx | 153 ++++++++++++------ web/i18n/en-US/app.ts | 4 + web/i18n/zh-Hans/app.ts | 4 + web/service/access-control.ts | 8 +- 4 files changed, 117 insertions(+), 52 deletions(-) diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 3ba35a7336..5c86c0c51d 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -1,13 +1,17 @@ import { memo, useCallback, + useEffect, useState, } from 'react' import { useTranslation } from 'react-i18next' import dayjs from 'dayjs' -import { RiArrowDownSLine, RiPlanetLine } from '@remixicon/react' +import { RiArrowDownSLine, RiArrowRightSLine, RiLockLine, RiPlanetLine } from '@remixicon/react' import Toast from '../../base/toast' import type { ModelAndParameter } from '../configuration/debug/types' +import Divider from '../../base/divider' +import AccessControl from '../app-access-control' +import Loading from '../../base/loading' import SuggestedAction from './suggested-action' import PublishWithMultipleModel from './publish-with-multiple-model' import Button from '@/app/components/base/button' @@ -27,6 +31,9 @@ import { FileText } from '@/app/components/base/icons/src/vender/line/files' import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button' import type { InputVar } from '@/app/components/workflow/types' import { appDefaultIconBackground } from '@/config' +import { useAppWhiteListSubjects, useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' +import { AccessMode } from '@/models/access-control' +import { fetchAppDetail } from '@/service/apps' export type AppPublisherProps = { disabled?: boolean @@ -65,10 +72,27 @@ const AppPublisher = ({ const [published, setPublished] = useState(false) const [open, setOpen] = useState(false) const appDetail = useAppStore(state => state.appDetail) + const setAppDetail = useAppStore(s => s.setAppDetail) const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {} const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode const appURL = `${appBaseURL}/${appMode}/${accessToken}` + const { data: appAccessMode, isPending: isGettingAppAccessMode } = useGetAppAccessMode({ appId: appDetail?.id }) + const { data: useCanAccessApp, isPending: isGettingUserCanAccessApp } = useGetUserCanAccessApp({ appId: appDetail?.id }) + const { data: appAccessSubjects, isPending: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open) + const [showAppAccessControl, setShowAppAccessControl] = useState(false) + const [isAppAccessSet, setIsAppAccessSet] = useState(false) + useEffect(() => { + if (appAccessMode && appAccessSubjects) { + if (appAccessMode.accessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS && appAccessSubjects.groups?.length > 0 && appAccessSubjects.members?.length > 0) + setIsAppAccessSet(false) + else + setIsAppAccessSet(true) + } + else { + setIsAppAccessSet(false) + } + }, [appAccessSubjects, appAccessMode]) const language = useGetLanguage() const formatTimeFromNow = useCallback((time: number) => { return dayjs(time).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow() @@ -120,6 +144,13 @@ const AppPublisher = ({ } }, [appDetail?.id]) + const handleAccessControlUpdate = useCallback(() => { + fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => { + setAppDetail(res) + setShowAppAccessControl(false) + }) + }, [appDetail, setAppDetail]) + const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false) return ( @@ -196,58 +227,83 @@ const AppPublisher = ({ ) } -
- }>{t('workflow.common.runApp')} - {appDetail?.mode === 'workflow' - ? ( - } - > - {t('workflow.common.batchRunApp')} - - ) - : ( + {(isGettingAppAccessMode || isGettingUserCanAccessApp || isGettingAppWhiteListSubjects) + ?
+ : <> + +
+
+

{t('app.publishApp.title')}

+
+
{ + setShowAppAccessControl(true) + }}> +
+ + {appAccessMode?.accessMode === AccessMode.ORGANIZATION &&

{t('app.accessControlDialog.accessItems.organization')}

} + {appAccessMode?.accessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS &&

{t('app.accessControlDialog.accessItems.specific')}

} + {appAccessMode?.accessMode === AccessMode.PUBLIC &&

{t('app.accessControlDialog.accessItems.anyone')}

} +
+ {!isAppAccessSet &&

{t('app.publishApp.notSet')}

} +
+ +
+
+
+
+ }>{t('workflow.common.runApp')} + {appDetail?.mode === 'workflow' + ? ( + } + > + {t('workflow.common.batchRunApp')} + + ) + : ( + { + setEmbeddingModalOpen(true) + handleTrigger() + }} + disabled={!publishedAt || !useCanAccessApp?.result} + icon={} + > + {t('workflow.common.embedIntoSite')} + + )} { - setEmbeddingModalOpen(true) - handleTrigger() + handleOpenInExplore() }} - disabled={!publishedAt} - icon={} + disabled={!publishedAt || !useCanAccessApp?.result} + icon={} > - {t('workflow.common.embedIntoSite')} + {t('workflow.common.openInExplore')} - )} - { - handleOpenInExplore() - }} - disabled={!publishedAt} - icon={} - > - {t('workflow.common.openInExplore')} - - }>{t('workflow.common.accessAPIReference')} - {appDetail?.mode === 'workflow' && ( - - )} -
+ }>{t('workflow.common.accessAPIReference')} + {appDetail?.mode === 'workflow' && ( + + )} +
+ } + {showAppAccessControl && { setShowAppAccessControl(false) }} />} ) } diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index c3cd439c25..fbed073950 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -198,6 +198,10 @@ const translation = { }, updateSuccess: 'Update successfully', }, + publishApp: { + title: 'Who can access web app', + notSet: 'Not set', + }, } export default translation diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index 2c68497d5b..2d62b19a24 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -199,6 +199,10 @@ const translation = { }, updateSuccess: '更新成功', }, + publishApp: { + title: '谁可以访问我的应用', + notSet: '未设置', + }, } export default translation diff --git a/web/service/access-control.ts b/web/service/access-control.ts index 06f14fe7a6..7c5391f00c 100644 --- a/web/service/access-control.ts +++ b/web/service/access-control.ts @@ -6,11 +6,11 @@ import type { App } from '@/types/app' const NAME_SPACE = 'access-control' -export const useAppWhiteListSubjects = (appId: string, enabled: boolean) => { +export const useAppWhiteListSubjects = (appId: string | undefined, enabled: boolean) => { return useQuery({ queryKey: [NAME_SPACE, 'app-whitelist-subjects', appId], queryFn: () => get<{ groups: AccessControlGroup[]; members: AccessControlAccount[] }>(`/enterprise/webapp/app/subjects?appId=${appId}`), - enabled, + enabled: !!appId && enabled, gcTime: 0, }) } @@ -66,7 +66,7 @@ export const useUpdateAccessMode = () => { }) } -export const useGetAppAccessMode = ({ appId, isInstalledApp }: { appId?: string; isInstalledApp: boolean }) => { +export const useGetAppAccessMode = ({ appId, isInstalledApp = true }: { appId?: string; isInstalledApp?: boolean }) => { return useQuery({ queryKey: [NAME_SPACE, 'app-access-mode', appId], queryFn: () => getAppAccessMode(appId!, isInstalledApp), @@ -75,7 +75,7 @@ export const useGetAppAccessMode = ({ appId, isInstalledApp }: { appId?: string; }) } -export const useGetUserCanAccessApp = ({ appId, isInstalledApp }: { appId?: string; isInstalledApp: boolean }) => { +export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true }: { appId?: string; isInstalledApp?: boolean }) => { return useQuery({ queryKey: [NAME_SPACE, 'user-can-access-app', appId], queryFn: () => getUserCanAccess(appId!, isInstalledApp),