diff --git a/web/app/(commonLayout)/explore/installed/[appId]/page.tsx b/web/app/(commonLayout)/explore/installed/[appId]/page.tsx index 938a03992..e288c62b5 100644 --- a/web/app/(commonLayout)/explore/installed/[appId]/page.tsx +++ b/web/app/(commonLayout)/explore/installed/[appId]/page.tsx @@ -1,16 +1,18 @@ -import type { FC } from 'react' import React from 'react' import Main from '@/app/components/explore/installed-app' export type IInstalledAppProps = { - params: Promise<{ + params: { appId: string - }> + } } -const InstalledApp: FC = async ({ params }) => { +// Using Next.js page convention for async server components +async function InstalledApp({ params }: IInstalledAppProps) { + const appId = (await params).appId return ( -
+
) } -export default React.memo(InstalledApp) + +export default InstalledApp diff --git a/web/app/(shareLayout)/chat/[token]/page.tsx b/web/app/(shareLayout)/chat/[token]/page.tsx index 640c40378..8ce67585f 100644 --- a/web/app/(shareLayout)/chat/[token]/page.tsx +++ b/web/app/(shareLayout)/chat/[token]/page.tsx @@ -1,10 +1,13 @@ 'use client' import React from 'react' import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history' +import AuthenticatedLayout from '../../components/authenticated-layout' const Chat = () => { return ( - + + + ) } diff --git a/web/app/(shareLayout)/chatbot/[token]/page.tsx b/web/app/(shareLayout)/chatbot/[token]/page.tsx index 6196afecc..5323d0dac 100644 --- a/web/app/(shareLayout)/chatbot/[token]/page.tsx +++ b/web/app/(shareLayout)/chatbot/[token]/page.tsx @@ -1,10 +1,13 @@ 'use client' import React from 'react' import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot' +import AuthenticatedLayout from '../../components/authenticated-layout' const Chatbot = () => { return ( - + + + ) } diff --git a/web/app/(shareLayout)/completion/[token]/page.tsx b/web/app/(shareLayout)/completion/[token]/page.tsx index e8bc9d79f..ae91338b9 100644 --- a/web/app/(shareLayout)/completion/[token]/page.tsx +++ b/web/app/(shareLayout)/completion/[token]/page.tsx @@ -1,9 +1,12 @@ import React from 'react' import Main from '@/app/components/share/text-generation' +import AuthenticatedLayout from '../../components/authenticated-layout' const Completion = () => { return ( -
+ +
+ ) } diff --git a/web/app/(shareLayout)/components/authenticated-layout.tsx b/web/app/(shareLayout)/components/authenticated-layout.tsx new file mode 100644 index 000000000..e3cfc8e6a --- /dev/null +++ b/web/app/(shareLayout)/components/authenticated-layout.tsx @@ -0,0 +1,84 @@ +'use client' + +import AppUnavailable from '@/app/components/base/app-unavailable' +import Loading from '@/app/components/base/loading' +import { removeAccessToken } from '@/app/components/share/utils' +import { useWebAppStore } from '@/context/web-app-context' +import { useGetUserCanAccessApp } from '@/service/access-control' +import { useGetWebAppInfo, useGetWebAppMeta, useGetWebAppParams } from '@/service/use-share' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import React, { useCallback, useEffect } from 'react' +import { useTranslation } from 'react-i18next' + +const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => { + const { t } = useTranslation() + const updateAppInfo = useWebAppStore(s => s.updateAppInfo) + const updateAppParams = useWebAppStore(s => s.updateAppParams) + const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta) + const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp) + const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetWebAppParams() + const { isFetching: isFetchingAppInfo, data: appInfo, error: appInfoError } = useGetWebAppInfo() + const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetWebAppMeta() + const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp: false }) + + useEffect(() => { + if (appInfo) + updateAppInfo(appInfo) + if (appParams) + updateAppParams(appParams) + if (appMeta) + updateWebAppMeta(appMeta) + updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) + }, [appInfo, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp]) + + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + params.set('redirect_url', pathname) + return `/webapp-signin?${params.toString()}` + }, [searchParams, pathname]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + + if (appInfoError) { + return
+ +
+ } + if (appParamsError) { + return
+ +
+ } + if (appMetaError) { + return
+ +
+ } + if (useCanAccessAppError) { + return
+ +
+ } + if (userCanAccessApp && !userCanAccessApp.result) { + return
+ + {t('common.userProfile.logout')} +
+ } + if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta) { + return
+ +
+ } + return <>{children} +} + +export default React.memo(AuthenticatedLayout) diff --git a/web/app/(shareLayout)/components/splash.tsx b/web/app/(shareLayout)/components/splash.tsx new file mode 100644 index 000000000..4fe9efe4d --- /dev/null +++ b/web/app/(shareLayout)/components/splash.tsx @@ -0,0 +1,80 @@ +'use client' +import type { FC, PropsWithChildren } from 'react' +import { useEffect } from 'react' +import { useCallback } from 'react' +import { useWebAppStore } from '@/context/web-app-context' +import { useRouter, useSearchParams } from 'next/navigation' +import AppUnavailable from '@/app/components/base/app-unavailable' +import { checkOrSetAccessToken, removeAccessToken, setAccessToken } from '@/app/components/share/utils' +import { useTranslation } from 'react-i18next' +import { fetchAccessToken } from '@/service/share' +import Loading from '@/app/components/base/loading' +import { AccessMode } from '@/models/access-control' + +const Splash: FC = ({ children }) => { + const { t } = useTranslation() + const shareCode = useWebAppStore(s => s.shareCode) + const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) + const searchParams = useSearchParams() + const router = useRouter() + const redirectUrl = searchParams.get('redirect_url') + const tokenFromUrl = searchParams.get('web_sso_token') + const message = searchParams.get('message') + const code = searchParams.get('code') + const getSigninUrl = useCallback(() => { + const params = new URLSearchParams(searchParams) + params.delete('message') + params.delete('code') + return `/webapp-signin?${params.toString()}` + }, [searchParams]) + + const backToHome = useCallback(() => { + removeAccessToken() + const url = getSigninUrl() + router.replace(url) + }, [getSigninUrl, router]) + + useEffect(() => { + (async () => { + if (message) + return + if (shareCode && tokenFromUrl && redirectUrl) { + localStorage.setItem('webapp_access_token', tokenFromUrl) + const tokenResp = await fetchAccessToken({ appCode: shareCode, webAppAccessToken: tokenFromUrl }) + await setAccessToken(shareCode, tokenResp.access_token) + router.replace(decodeURIComponent(redirectUrl)) + return + } + if (shareCode && redirectUrl && localStorage.getItem('webapp_access_token')) { + const tokenResp = await fetchAccessToken({ appCode: shareCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) + await setAccessToken(shareCode, tokenResp.access_token) + router.replace(decodeURIComponent(redirectUrl)) + return + } + if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) { + await checkOrSetAccessToken(shareCode) + router.replace(decodeURIComponent(redirectUrl)) + } + })() + }, [shareCode, redirectUrl, router, tokenFromUrl, message, webAppAccessMode]) + + if (message) { + return
+ + {code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')} +
+ } + if (tokenFromUrl) { + return
+ +
+ } + if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) { + return
+ +
+ } + return <>{children} +} + +export default Splash diff --git a/web/app/(shareLayout)/layout.tsx b/web/app/(shareLayout)/layout.tsx index d057ba759..5af913cac 100644 --- a/web/app/(shareLayout)/layout.tsx +++ b/web/app/(shareLayout)/layout.tsx @@ -1,54 +1,15 @@ -'use client' -import React, { useEffect, useState } from 'react' -import type { FC } from 'react' -import { usePathname, useSearchParams } from 'next/navigation' -import Loading from '../components/base/loading' -import { useGlobalPublicStore } from '@/context/global-public-context' -import { AccessMode } from '@/models/access-control' -import { getAppAccessModeByAppCode } from '@/service/share' +import type { FC, PropsWithChildren } from 'react' +import WebAppStoreProvider from '@/context/web-app-context' +import Splash from './components/splash' -const Layout: FC<{ - children: React.ReactNode -}> = ({ children }) => { - const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending) - const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode) - const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) - const pathname = usePathname() - const searchParams = useSearchParams() - const redirectUrl = searchParams.get('redirect_url') - const [isLoading, setIsLoading] = useState(true) - useEffect(() => { - (async () => { - if (!isGlobalPending && !systemFeatures.webapp_auth.enabled) { - setIsLoading(false) - return - } - - let appCode: string | null = null - if (redirectUrl) { - const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) - appCode = url.pathname.split('/').pop() || null - } - else { - appCode = pathname.split('/').pop() || null - } - - if (!appCode) - return - setIsLoading(true) - const ret = await getAppAccessModeByAppCode(appCode) - setWebAppAccessMode(ret?.accessMode || AccessMode.PUBLIC) - setIsLoading(false) - })() - }, [pathname, redirectUrl, setWebAppAccessMode, isGlobalPending, systemFeatures.webapp_auth.enabled]) - if (isLoading || isGlobalPending) { - return
- -
- } +const Layout: FC = ({ children }) => { return (
- {children} + + + {children} + +
) } diff --git a/web/app/(shareLayout)/webapp-signin/layout.tsx b/web/app/(shareLayout)/webapp-signin/layout.tsx index a03364d32..764998207 100644 --- a/web/app/(shareLayout)/webapp-signin/layout.tsx +++ b/web/app/(shareLayout)/webapp-signin/layout.tsx @@ -3,10 +3,13 @@ import cn from '@/utils/classnames' import { useGlobalPublicStore } from '@/context/global-public-context' import useDocumentTitle from '@/hooks/use-document-title' +import type { PropsWithChildren } from 'react' +import { useTranslation } from 'react-i18next' -export default function SignInLayout({ children }: any) { - const { systemFeatures } = useGlobalPublicStore() - useDocumentTitle('') +export default function SignInLayout({ children }: PropsWithChildren) { + const { t } = useTranslation() + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) + useDocumentTitle(t('login.webapp.login')) return <>
diff --git a/web/app/(shareLayout)/webapp-signin/normalForm.tsx b/web/app/(shareLayout)/webapp-signin/normalForm.tsx index d6bdf607b..44006a9f1 100644 --- a/web/app/(shareLayout)/webapp-signin/normalForm.tsx +++ b/web/app/(shareLayout)/webapp-signin/normalForm.tsx @@ -1,3 +1,4 @@ +'use client' import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import Link from 'next/link' diff --git a/web/app/(shareLayout)/webapp-signin/page.tsx b/web/app/(shareLayout)/webapp-signin/page.tsx index 967516c41..1c6209b90 100644 --- a/web/app/(shareLayout)/webapp-signin/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/page.tsx @@ -1,36 +1,30 @@ 'use client' import { useRouter, useSearchParams } from 'next/navigation' import type { FC } from 'react' -import React, { useCallback, useEffect } from 'react' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import Toast from '@/app/components/base/toast' -import { removeAccessToken, setAccessToken } from '@/app/components/share/utils' +import { removeAccessToken } from '@/app/components/share/utils' import { useGlobalPublicStore } from '@/context/global-public-context' -import Loading from '@/app/components/base/loading' import AppUnavailable from '@/app/components/base/app-unavailable' import NormalForm from './normalForm' import { AccessMode } from '@/models/access-control' import ExternalMemberSsoAuth from './components/external-member-sso-auth' -import { fetchAccessToken } from '@/service/share' +import { useWebAppStore } from '@/context/web-app-context' const WebSSOForm: FC = () => { const { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) - const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) + const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) const searchParams = useSearchParams() const router = useRouter() const redirectUrl = searchParams.get('redirect_url') - const tokenFromUrl = searchParams.get('web_sso_token') - const message = searchParams.get('message') - const code = searchParams.get('code') const getSigninUrl = useCallback(() => { - const params = new URLSearchParams(searchParams) - params.delete('message') - params.delete('code') + const params = new URLSearchParams() + params.append('redirect_url', redirectUrl || '') return `/webapp-signin?${params.toString()}` - }, [searchParams]) + }, [redirectUrl]) const backToHome = useCallback(() => { removeAccessToken() @@ -38,73 +32,12 @@ const WebSSOForm: FC = () => { router.replace(url) }, [getSigninUrl, router]) - const showErrorToast = (msg: string) => { - Toast.notify({ - type: 'error', - message: msg, - }) - } - - const getAppCodeFromRedirectUrl = useCallback(() => { - if (!redirectUrl) - return null - const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) - const appCode = url.pathname.split('/').pop() - if (!appCode) - return null - - return appCode - }, [redirectUrl]) - - useEffect(() => { - (async () => { - if (message) - return - - const appCode = getAppCodeFromRedirectUrl() - if (appCode && tokenFromUrl && redirectUrl) { - localStorage.setItem('webapp_access_token', tokenFromUrl) - const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: tokenFromUrl }) - await setAccessToken(appCode, tokenResp.access_token) - router.replace(decodeURIComponent(redirectUrl)) - return - } - if (appCode && redirectUrl && localStorage.getItem('webapp_access_token')) { - const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) - await setAccessToken(appCode, tokenResp.access_token) - router.replace(decodeURIComponent(redirectUrl)) - } - })() - }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message]) - - useEffect(() => { - if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl) - router.replace(decodeURIComponent(redirectUrl)) - }, [webAppAccessMode, router, redirectUrl]) - - if (tokenFromUrl) { - return
- -
- } - - if (message) { - return
- - {code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')} -
- } if (!redirectUrl) { - showErrorToast('redirect url is invalid.') return
} - if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) { - return
- -
- } + if (!systemFeatures.webapp_auth.enabled) { return

{t('login.webapp.disabled')}

diff --git a/web/app/(shareLayout)/workflow/[token]/page.tsx b/web/app/(shareLayout)/workflow/[token]/page.tsx index e93bc8c1a..4f5923e91 100644 --- a/web/app/(shareLayout)/workflow/[token]/page.tsx +++ b/web/app/(shareLayout)/workflow/[token]/page.tsx @@ -1,10 +1,13 @@ import React from 'react' import Main from '@/app/components/share/text-generation' +import AuthenticatedLayout from '../../components/authenticated-layout' const Workflow = () => { return ( -
+ +
+ ) } diff --git a/web/app/components/base/chat/chat-with-history/context.tsx b/web/app/components/base/chat/chat-with-history/context.tsx index 5bf151477..3a5dc793d 100644 --- a/web/app/components/base/chat/chat-with-history/context.tsx +++ b/web/app/components/base/chat/chat-with-history/context.tsx @@ -18,11 +18,8 @@ import type { import { noop } from 'lodash-es' export type ChatWithHistoryContextValue = { - appInfoError?: any - appInfoLoading?: boolean - appMeta?: AppMeta - appData?: AppData - userCanAccess?: boolean + appMeta?: AppMeta | null + appData?: AppData | null appParams?: ChatConfig appChatListDataLoading?: boolean currentConversationId: string @@ -62,7 +59,6 @@ export type ChatWithHistoryContextValue = { } export const ChatWithHistoryContext = createContext({ - userCanAccess: false, currentConversationId: '', appPrevChatTree: [], pinnedConversationList: [], diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 32f74e645..be935a70b 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -21,9 +21,6 @@ import { addFileInfos, sortAgentSorts } from '../../../tools/utils' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { delConversation, - fetchAppInfo, - fetchAppMeta, - fetchAppParams, fetchChatList, fetchConversations, generationConversationName, @@ -43,8 +40,7 @@ import { useAppFavicon } from '@/hooks/use-app-favicon' import { InputVarType } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' import { noop } from 'lodash-es' -import { useGetUserCanAccessApp } from '@/service/access-control' -import { useGlobalPublicStore } from '@/context/global-public-context' +import { useWebAppStore } from '@/context/web-app-context' function getFormattedChatList(messages: any[]) { const newChatList: ChatItem[] = [] @@ -74,13 +70,9 @@ function getFormattedChatList(messages: any[]) { export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) - const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) - const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) - const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ - appId: installedAppInfo?.app.id || appInfo?.app_id, - isInstalledApp, - enabled: systemFeatures.webapp_auth.enabled, - }) + const appInfo = useWebAppStore(s => s.appInfo) + const appParams = useWebAppStore(s => s.appParams) + const appMeta = useWebAppStore(s => s.appMeta) useAppFavicon({ enable: !installedAppInfo, @@ -107,6 +99,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { use_icon_as_answer_icon: app.use_icon_as_answer_icon, }, plan: 'basic', + custom_config: null, } as AppData } @@ -166,8 +159,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { return currentConversationId }, [currentConversationId, newConversationId]) - const { data: appParams } = useSWR(['appParams', isInstalledApp, appId], () => fetchAppParams(isInstalledApp, appId)) - const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta(isInstalledApp, appId)) const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100)) const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100)) const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) @@ -485,9 +476,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [isInstalledApp, appId, t, notify]) return { - appInfoError, - appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), - userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, isInstalledApp, appId, currentConversationId, diff --git a/web/app/components/base/chat/chat-with-history/index.tsx b/web/app/components/base/chat/chat-with-history/index.tsx index fe8e7b430..cceb21b29 100644 --- a/web/app/components/base/chat/chat-with-history/index.tsx +++ b/web/app/components/base/chat/chat-with-history/index.tsx @@ -1,7 +1,6 @@ 'use client' import type { FC } from 'react' import { - useCallback, useEffect, useState, } from 'react' @@ -19,12 +18,10 @@ import ChatWrapper from './chat-wrapper' import type { InstalledApp } from '@/models/explore' import Loading from '@/app/components/base/loading' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' +import { checkOrSetAccessToken } from '@/app/components/share/utils' import AppUnavailable from '@/app/components/base/app-unavailable' import cn from '@/utils/classnames' import useDocumentTitle from '@/hooks/use-document-title' -import { useTranslation } from 'react-i18next' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' type ChatWithHistoryProps = { className?: string @@ -33,16 +30,12 @@ const ChatWithHistory: FC = ({ className, }) => { const { - userCanAccess, - appInfoError, appData, - appInfoLoading, appChatListDataLoading, chatShouldReloadKey, isMobile, themeBuilder, sidebarCollapseState, - isInstalledApp, } = useChatWithHistoryContext() const isSidebarCollapsed = sidebarCollapseState const customConfig = appData?.custom_config @@ -56,41 +49,6 @@ const ChatWithHistory: FC = ({ useDocumentTitle(site?.title || 'Chat') - const { t } = useTranslation() - const searchParams = useSearchParams() - const router = useRouter() - const pathname = usePathname() - const getSigninUrl = useCallback(() => { - const params = new URLSearchParams(searchParams) - params.delete('message') - params.set('redirect_url', pathname) - return `/webapp-signin?${params.toString()}` - }, [searchParams, pathname]) - - const backToHome = useCallback(() => { - removeAccessToken() - const url = getSigninUrl() - router.replace(url) - }, [getSigninUrl, router]) - - if (appInfoLoading) { - return ( - - ) - } - if (!userCanAccess) { - return
- - {!isInstalledApp && {t('common.userProfile.logout')}} -
- } - - if (appInfoError) { - return ( - - ) - } - return (
= ({ const themeBuilder = useThemeContext() const { - appInfoError, - appInfoLoading, - userCanAccess, appData, appParams, appMeta, @@ -191,10 +146,7 @@ const ChatWithHistoryWrap: FC = ({ return ( { const { - userCanAccess, isMobile, allowResetChat, - appInfoError, - appInfoLoading, appData, appChatListDataLoading, chatShouldReloadKey, handleNewConversation, themeBuilder, - isInstalledApp, } = useEmbeddedChatbotContext() const { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) @@ -55,58 +45,6 @@ const Chatbot = () => { useDocumentTitle(site?.title || 'Chat') - const searchParams = useSearchParams() - const router = useRouter() - const pathname = usePathname() - const getSigninUrl = useCallback(() => { - const params = new URLSearchParams(searchParams) - params.delete('message') - params.set('redirect_url', pathname) - return `/webapp-signin?${params.toString()}` - }, [searchParams, pathname]) - - const backToHome = useCallback(() => { - removeAccessToken() - const url = getSigninUrl() - router.replace(url) - }, [getSigninUrl, router]) - - if (appInfoLoading) { - return ( - <> - {!isMobile && } - {isMobile && ( -
-
- -
-
- )} - - ) - } - - if (!userCanAccess) { - return
- - {!isInstalledApp && {t('common.userProfile.logout')}} -
- } - - if (appInfoError) { - return ( - <> - {!isMobile && } - {isMobile && ( -
-
- -
-
- )} - - ) - } return (
{ const themeBuilder = useThemeContext() const { - appInfoError, - appInfoLoading, appData, userCanAccess, appParams, @@ -200,8 +136,6 @@ const EmbeddedChatbotWrapper = () => { return { } const EmbeddedChatbot = () => { - const [initialized, setInitialized] = useState(false) - const [appUnavailable, setAppUnavailable] = useState(false) - const [isUnknownReason, setIsUnknownReason] = useState(false) - - useAsyncEffect(async () => { - if (!initialized) { - try { - await checkOrSetAccessToken() - } - catch (e: any) { - if (e.status === 404) { - setAppUnavailable(true) - } - else { - setIsUnknownReason(true) - setAppUnavailable(true) - } - } - setInitialized(true) - } - }, []) - - if (!initialized) - return null - - if (appUnavailable) - return - return } diff --git a/web/app/components/base/chat/types.ts b/web/app/components/base/chat/types.ts index 91f9bc976..c463879a5 100644 --- a/web/app/components/base/chat/types.ts +++ b/web/app/components/base/chat/types.ts @@ -49,6 +49,16 @@ export type ChatConfig = Omit & { questionEditEnable?: boolean supportFeedback?: boolean supportCitationHitInfo?: boolean + system_parameters: { + audio_file_size_limit: number + file_size_limit: number + image_file_size_limit: number + video_file_size_limit: number + workflow_file_upload_limit: number + } + more_like_this: { + enabled: boolean + } } export type WorkflowProcess = { diff --git a/web/app/components/explore/index.tsx b/web/app/components/explore/index.tsx index bae2610cb..e716de96f 100644 --- a/web/app/components/explore/index.tsx +++ b/web/app/components/explore/index.tsx @@ -22,6 +22,7 @@ const Explore: FC = ({ const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext() const [hasEditPermission, setHasEditPermission] = useState(false) const [installedApps, setInstalledApps] = useState([]) + const [isFetchingInstalledApps, setIsFetchingInstalledApps] = useState(false) const { t } = useTranslation() useDocumentTitle(t('common.menus.explore')) @@ -51,6 +52,8 @@ const Explore: FC = ({ hasEditPermission, installedApps, setInstalledApps, + isFetchingInstalledApps, + setIsFetchingInstalledApps, } } > diff --git a/web/app/components/explore/installed-app/index.tsx b/web/app/components/explore/installed-app/index.tsx index 71013fc2e..8032e173c 100644 --- a/web/app/components/explore/installed-app/index.tsx +++ b/web/app/components/explore/installed-app/index.tsx @@ -1,11 +1,17 @@ 'use client' import type { FC } from 'react' +import { useEffect } from 'react' import React from 'react' import { useContext } from 'use-context-selector' import ExploreContext from '@/context/explore-context' import TextGenerationApp from '@/app/components/share/text-generation' import Loading from '@/app/components/base/loading' import ChatWithHistory from '@/app/components/base/chat/chat-with-history' +import { useWebAppStore } from '@/context/web-app-context' +import AppUnavailable from '../../base/app-unavailable' +import { useGetUserCanAccessApp } from '@/service/access-control' +import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore' +import type { AppData } from '@/models/share' export type IInstalledAppProps = { id: string @@ -14,26 +20,95 @@ export type IInstalledAppProps = { const InstalledApp: FC = ({ id, }) => { - const { installedApps } = useContext(ExploreContext) + const { installedApps, isFetchingInstalledApps } = useContext(ExploreContext) + const updateAppInfo = useWebAppStore(s => s.updateAppInfo) const installedApp = installedApps.find(item => item.id === id) + const updateWebAppAccessMode = useWebAppStore(s => s.updateWebAppAccessMode) + const updateAppParams = useWebAppStore(s => s.updateAppParams) + const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta) + const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp) + const { isFetching: isFetchingWebAppAccessMode, data: webAppAccessMode, error: webAppAccessModeError } = useGetInstalledAppAccessModeByAppId(installedApp?.id ?? null) + const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetInstalledAppParams(installedApp?.id ?? null) + const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetInstalledAppMeta(installedApp?.id ?? null) + const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: installedApp?.app.id, isInstalledApp: true }) - if (!installedApp) { - return ( -
- -
- ) + useEffect(() => { + if (!installedApp) { + updateAppInfo(null) + } + else { + const { id, app } = installedApp + updateAppInfo({ + app_id: id, + site: { + title: app.name, + icon_type: app.icon_type, + icon: app.icon, + icon_background: app.icon_background, + icon_url: app.icon_url, + prompt_public: false, + copyright: '', + show_workflow_steps: true, + use_icon_as_answer_icon: app.use_icon_as_answer_icon, + }, + plan: 'basic', + custom_config: null, + } as AppData) + } + + if (appParams) + updateAppParams(appParams) + if (appMeta) + updateWebAppMeta(appMeta) + if (webAppAccessMode) + updateWebAppAccessMode(webAppAccessMode.accessMode) + updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) + }, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode]) + + if (appParamsError) { + return
+ +
+ } + if (appMetaError) { + return
+ +
+ } + if (useCanAccessAppError) { + return
+ +
+ } + if (webAppAccessModeError) { + return
+ +
+ } + if (userCanAccessApp && !userCanAccessApp.result) { + return
+ +
+ } + if (isFetchingAppParams || isFetchingAppMeta || isFetchingWebAppAccessMode || isFetchingInstalledApps) { + return
+ +
+ } + if (!installedApp) { + return
+ +
} - return (
- {installedApp.app.mode !== 'completion' && installedApp.app.mode !== 'workflow' && ( + {installedApp?.app.mode !== 'completion' && installedApp?.app.mode !== 'workflow' && ( )} - {installedApp.app.mode === 'completion' && ( + {installedApp?.app.mode === 'completion' && ( )} - {installedApp.app.mode === 'workflow' && ( + {installedApp?.app.mode === 'workflow' && ( )}
diff --git a/web/app/components/explore/sidebar/index.tsx b/web/app/components/explore/sidebar/index.tsx index fe5935bcd..74c397f4f 100644 --- a/web/app/components/explore/sidebar/index.tsx +++ b/web/app/components/explore/sidebar/index.tsx @@ -8,11 +8,11 @@ import Link from 'next/link' import Toast from '../../base/toast' import Item from './app-nav-item' import cn from '@/utils/classnames' -import { fetchInstalledAppList as doFetchInstalledAppList, uninstallApp, updatePinStatus } from '@/service/explore' import ExploreContext from '@/context/explore-context' import Confirm from '@/app/components/base/confirm' import Divider from '@/app/components/base/divider' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' +import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore' const SelectedDiscoveryIcon = () => ( @@ -50,16 +50,14 @@ const SideBar: FC = ({ const lastSegment = segments.slice(-1)[0] const isDiscoverySelected = lastSegment === 'apps' const isChatSelected = lastSegment === 'chat' - const { installedApps, setInstalledApps } = useContext(ExploreContext) + const { installedApps, setInstalledApps, setIsFetchingInstalledApps } = useContext(ExploreContext) + const { isFetching: isFetchingInstalledApps, data: ret, refetch: fetchInstalledAppList } = useGetInstalledApps() + const { mutateAsync: uninstallApp } = useUninstallApp() + const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus() const media = useBreakpoints() const isMobile = media === MediaType.mobile - const fetchInstalledAppList = async () => { - const { installed_apps }: any = await doFetchInstalledAppList() - setInstalledApps(installed_apps) - } - const [showConfirm, setShowConfirm] = useState(false) const [currId, setCurrId] = useState('') const handleDelete = async () => { @@ -70,25 +68,31 @@ const SideBar: FC = ({ type: 'success', message: t('common.api.remove'), }) - fetchInstalledAppList() } const handleUpdatePinStatus = async (id: string, isPinned: boolean) => { - await updatePinStatus(id, isPinned) + await updatePinStatus({ appId: id, isPinned }) Toast.notify({ type: 'success', message: t('common.api.success'), }) - fetchInstalledAppList() } useEffect(() => { - fetchInstalledAppList() - }, []) + const installed_apps = (ret as any)?.installed_apps + if (installed_apps && installed_apps.length > 0) + setInstalledApps(installed_apps) + else + setInstalledApps([]) + }, [ret, setInstalledApps]) + + useEffect(() => { + setIsFetchingInstalledApps(isFetchingInstalledApps) + }, [isFetchingInstalledApps, setIsFetchingInstalledApps]) useEffect(() => { fetchInstalledAppList() - }, [controlUpdateInstalledApps]) + }, [controlUpdateInstalledApps, fetchInstalledAppList]) const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length return ( diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 4be6b1895..ae6e733e4 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -7,16 +7,14 @@ import { RiErrorWarningFill, } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { useSearchParams } from 'next/navigation' import TabHeader from '../../base/tab-header' -import { checkOrSetAccessToken, removeAccessToken } from '../utils' import MenuDropdown from './menu-dropdown' import RunBatch from './run-batch' import ResDownload from './run-batch/res-download' -import AppUnavailable from '../../base/app-unavailable' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import RunOnce from '@/app/components/share/text-generation/run-once' -import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage } from '@/service/share' +import { fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share' import type { SiteInfo } from '@/models/share' import type { MoreLikeThisConfig, @@ -39,10 +37,10 @@ import { Resolution, TransferMethod } from '@/types/app' import { useAppFavicon } from '@/hooks/use-app-favicon' import DifyLogo from '@/app/components/base/logo/dify-logo' import cn from '@/utils/classnames' -import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' import { AccessMode } from '@/models/access-control' import { useGlobalPublicStore } from '@/context/global-public-context' import useDocumentTitle from '@/hooks/use-document-title' +import { useWebAppStore } from '@/context/web-app-context' const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group. enum TaskStatus { @@ -83,9 +81,6 @@ const TextGeneration: FC = ({ const mode = searchParams.get('mode') || 'create' const [currentTab, setCurrentTab] = useState(['create', 'batch'].includes(mode) ? mode : 'create') - const router = useRouter() - const pathname = usePathname() - // Notice this situation isCallBatchAPI but not in batch tab const [isCallBatchAPI, setIsCallBatchAPI] = useState(false) const isInBatchTab = currentTab === 'batch' @@ -103,30 +98,19 @@ const TextGeneration: FC = ({ const [moreLikeThisConfig, setMoreLikeThisConfig] = useState(null) const [textToSpeechConfig, setTextToSpeechConfig] = useState(null) - const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ - appId, - isInstalledApp, - enabled: systemFeatures.webapp_auth.enabled, - }) - const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ - appId, - isInstalledApp, - enabled: systemFeatures.webapp_auth.enabled, - }) - // save message const [savedMessages, setSavedMessages] = useState([]) - const fetchSavedMessage = async () => { - const res: any = await doFetchSavedMessage(isInstalledApp, installedAppInfo?.id) + const fetchSavedMessage = useCallback(async () => { + const res: any = await doFetchSavedMessage(isInstalledApp, appId) setSavedMessages(res.data) - } + }, [isInstalledApp, appId]) const handleSaveMessage = async (messageId: string) => { - await saveMessage(messageId, isInstalledApp, installedAppInfo?.id) + await saveMessage(messageId, isInstalledApp, appId) notify({ type: 'success', message: t('common.api.saved') }) fetchSavedMessage() } const handleRemoveSavedMessage = async (messageId: string) => { - await removeMessage(messageId, isInstalledApp, installedAppInfo?.id) + await removeMessage(messageId, isInstalledApp, appId) notify({ type: 'success', message: t('common.api.remove') }) fetchSavedMessage() } @@ -375,34 +359,14 @@ const TextGeneration: FC = ({ } } - const fetchInitData = async () => { - if (!isInstalledApp) - await checkOrSetAccessToken() - - return Promise.all([ - isInstalledApp - ? { - app_id: installedAppInfo?.id, - site: { - title: installedAppInfo?.app.name, - prompt_public: false, - copyright: '', - icon: installedAppInfo?.app.icon, - icon_background: installedAppInfo?.app.icon_background, - }, - plan: 'basic', - } - : fetchAppInfo(), - fetchAppParams(isInstalledApp, installedAppInfo?.id), - !isWorkflow - ? fetchSavedMessage() - : {}, - ]) - } - + const appData = useWebAppStore(s => s.appInfo) + const appParams = useWebAppStore(s => s.appParams) + const accessMode = useWebAppStore(s => s.webAppAccessMode) useEffect(() => { (async () => { - const [appData, appParams]: any = await fetchInitData() + if (!appData || !appParams) + return + !isWorkflow && fetchSavedMessage() const { app_id: appId, site: siteInfo, custom_config } = appData setAppId(appId) setSiteInfo(siteInfo as SiteInfo) @@ -413,11 +377,11 @@ const TextGeneration: FC = ({ setVisionConfig({ // legacy of image upload compatible ...file_upload, - transfer_methods: file_upload.allowed_file_upload_methods || file_upload.allowed_upload_methods, + transfer_methods: file_upload?.allowed_file_upload_methods || file_upload?.allowed_upload_methods, // legacy of image upload compatible - image_file_size_limit: appParams?.system_parameters?.image_file_size_limit, + image_file_size_limit: appParams?.system_parameters.image_file_size_limit, fileUploadConfig: appParams?.system_parameters, - }) + } as any) const prompt_variables = userInputsFormToPromptVariables(user_input_form) setPromptConfig({ prompt_template: '', // placeholder for future @@ -426,7 +390,7 @@ const TextGeneration: FC = ({ setMoreLikeThisConfig(more_like_this) setTextToSpeechConfig(text_to_speech) })() - }, []) + }, [appData, appParams, fetchSavedMessage, isWorkflow]) // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. useDocumentTitle(siteInfo?.title || t('share.generation.title')) @@ -528,32 +492,12 @@ const TextGeneration: FC = ({
) - const getSigninUrl = useCallback(() => { - const params = new URLSearchParams(searchParams) - params.delete('message') - params.set('redirect_url', pathname) - return `/webapp-signin?${params.toString()}` - }, [searchParams, pathname]) - - const backToHome = useCallback(() => { - removeAccessToken() - const url = getSigninUrl() - router.replace(url) - }, [getSigninUrl, router]) - - if (!appId || !siteInfo || !promptConfig || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission))) { + if (!appId || !siteInfo || !promptConfig) { return (
) } - if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) { - return
- - {!isInstalledApp && {t('common.userProfile.logout')}} -
- } - return (
= ({ imageUrl={siteInfo.icon_url} />
{siteInfo.title}
- +
{siteInfo.description && (
{siteInfo.description}
diff --git a/web/app/components/share/text-generation/menu-dropdown.tsx b/web/app/components/share/text-generation/menu-dropdown.tsx index adb926c7c..1c1b6adfe 100644 --- a/web/app/components/share/text-generation/menu-dropdown.tsx +++ b/web/app/components/share/text-generation/menu-dropdown.tsx @@ -18,8 +18,8 @@ import { import ThemeSwitcher from '@/app/components/base/theme-switcher' import type { SiteInfo } from '@/models/share' import cn from '@/utils/classnames' -import { useGlobalPublicStore } from '@/context/global-public-context' import { AccessMode } from '@/models/access-control' +import { useWebAppStore } from '@/context/web-app-context' type Props = { data?: SiteInfo @@ -32,7 +32,7 @@ const MenuDropdown: FC = ({ placement, hideLogout, }) => { - const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) + const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) const router = useRouter() const pathname = usePathname() const { t } = useTranslation() diff --git a/web/app/components/share/utils.ts b/web/app/components/share/utils.ts index 8a897ab59..0c6457fb0 100644 --- a/web/app/components/share/utils.ts +++ b/web/app/components/share/utils.ts @@ -10,7 +10,7 @@ export const getInitialTokenV2 = (): Record => ({ version: 2, }) -export const checkOrSetAccessToken = async (appCode?: string) => { +export const checkOrSetAccessToken = async (appCode?: string | null) => { const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0] const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) diff --git a/web/context/explore-context.ts b/web/context/explore-context.ts index 11124bd54..d8d64fb34 100644 --- a/web/context/explore-context.ts +++ b/web/context/explore-context.ts @@ -8,6 +8,8 @@ type IExplore = { hasEditPermission: boolean installedApps: InstalledApp[] setInstalledApps: (installedApps: InstalledApp[]) => void + isFetchingInstalledApps: boolean + setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void } const ExploreContext = createContext({ @@ -16,6 +18,8 @@ const ExploreContext = createContext({ hasEditPermission: false, installedApps: [], setInstalledApps: noop, + isFetchingInstalledApps: false, + setIsFetchingInstalledApps: noop, }) export default ExploreContext diff --git a/web/context/global-public-context.tsx b/web/context/global-public-context.tsx index 26ad84be6..324ac019c 100644 --- a/web/context/global-public-context.tsx +++ b/web/context/global-public-context.tsx @@ -7,15 +7,12 @@ import type { SystemFeatures } from '@/types/feature' import { defaultSystemFeatures } from '@/types/feature' import { getSystemFeatures } from '@/service/common' import Loading from '@/app/components/base/loading' -import { AccessMode } from '@/models/access-control' type GlobalPublicStore = { isGlobalPending: boolean setIsGlobalPending: (isPending: boolean) => void systemFeatures: SystemFeatures setSystemFeatures: (systemFeatures: SystemFeatures) => void - webAppAccessMode: AccessMode, - setWebAppAccessMode: (webAppAccessMode: AccessMode) => void } export const useGlobalPublicStore = create(set => ({ @@ -23,8 +20,6 @@ export const useGlobalPublicStore = create(set => ({ setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })), systemFeatures: defaultSystemFeatures, setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), - webAppAccessMode: AccessMode.PUBLIC, - setWebAppAccessMode: (webAppAccessMode: AccessMode) => set(() => ({ webAppAccessMode })), })) const GlobalPublicStoreProvider: FC = ({ diff --git a/web/context/web-app-context.tsx b/web/context/web-app-context.tsx new file mode 100644 index 000000000..55f95e481 --- /dev/null +++ b/web/context/web-app-context.tsx @@ -0,0 +1,87 @@ +'use client' + +import type { ChatConfig } from '@/app/components/base/chat/types' +import Loading from '@/app/components/base/loading' +import { AccessMode } from '@/models/access-control' +import type { AppData, AppMeta } from '@/models/share' +import { useGetWebAppAccessModeByCode } from '@/service/use-share' +import { usePathname, useSearchParams } from 'next/navigation' +import type { FC, PropsWithChildren } from 'react' +import { useEffect } from 'react' +import { useState } from 'react' +import { create } from 'zustand' + +type WebAppStore = { + shareCode: string | null + updateShareCode: (shareCode: string | null) => void + appInfo: AppData | null + updateAppInfo: (appInfo: AppData | null) => void + appParams: ChatConfig | null + updateAppParams: (appParams: ChatConfig | null) => void + webAppAccessMode: AccessMode + updateWebAppAccessMode: (accessMode: AccessMode) => void + appMeta: AppMeta | null + updateWebAppMeta: (appMeta: AppMeta | null) => void + userCanAccessApp: boolean + updateUserCanAccessApp: (canAccess: boolean) => void +} + +export const useWebAppStore = create(set => ({ + shareCode: null, + updateShareCode: (shareCode: string | null) => set(() => ({ shareCode })), + appInfo: null, + updateAppInfo: (appInfo: AppData | null) => set(() => ({ appInfo })), + appParams: null, + updateAppParams: (appParams: ChatConfig | null) => set(() => ({ appParams })), + webAppAccessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, + updateWebAppAccessMode: (accessMode: AccessMode) => set(() => ({ webAppAccessMode: accessMode })), + appMeta: null, + updateWebAppMeta: (appMeta: AppMeta | null) => set(() => ({ appMeta })), + userCanAccessApp: false, + updateUserCanAccessApp: (canAccess: boolean) => set(() => ({ userCanAccessApp: canAccess })), +})) + +const getShareCodeFromRedirectUrl = (redirectUrl: string | null): string | null => { + if (!redirectUrl || redirectUrl.length === 0) + return null + const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) + return url.pathname.split('/').pop() || null +} +const getShareCodeFromPathname = (pathname: string): string | null => { + const code = pathname.split('/').pop() || null + if (code === 'webapp-signin') + return null + return code +} + +const WebAppStoreProvider: FC = ({ children }) => { + const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode) + const updateShareCode = useWebAppStore(state => state.updateShareCode) + const pathname = usePathname() + const searchParams = useSearchParams() + const redirectUrlParam = searchParams.get('redirect_url') + const [shareCode, setShareCode] = useState(null) + useEffect(() => { + const shareCodeFromRedirect = getShareCodeFromRedirectUrl(redirectUrlParam) + const shareCodeFromPathname = getShareCodeFromPathname(pathname) + const newShareCode = shareCodeFromRedirect || shareCodeFromPathname + setShareCode(newShareCode) + updateShareCode(newShareCode) + }, [pathname, redirectUrlParam, updateShareCode]) + const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode) + useEffect(() => { + if (accessModeResult?.accessMode) + updateWebAppAccessMode(accessModeResult.accessMode) + }, [accessModeResult, updateWebAppAccessMode]) + if (isFetching) { + return
+ +
+ } + return ( + <> + {children} + + ) +} +export default WebAppStoreProvider diff --git a/web/i18n/en-US/login.ts b/web/i18n/en-US/login.ts index 0beb631d2..d47eb7c07 100644 --- a/web/i18n/en-US/login.ts +++ b/web/i18n/en-US/login.ts @@ -105,6 +105,7 @@ const translation = { licenseInactive: 'License Inactive', licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.', webapp: { + login: 'Login', noLoginMethod: 'Authentication method not configured for web app', noLoginMethodTip: 'Please contact the system admin to add an authentication method.', disabled: 'Webapp authentication is disabled. Please contact the system admin to enable it. You can try to use the app directly.', diff --git a/web/i18n/ja-JP/login.ts b/web/i18n/ja-JP/login.ts index 84ab9eecd..b37700eba 100644 --- a/web/i18n/ja-JP/login.ts +++ b/web/i18n/ja-JP/login.ts @@ -106,6 +106,7 @@ const translation = { licenseExpired: 'ライセンスの有効期限が切れています', licenseLostTip: 'Dify ライセンスサーバーへの接続に失敗しました。続けて Dify を使用するために管理者に連絡してください。', webapp: { + login: 'ログイン', noLoginMethod: 'Web アプリに対して認証方法が構成されていません', noLoginMethodTip: 'システム管理者に連絡して、認証方法を追加してください。', disabled: 'Web アプリの認証が無効になっています。システム管理者に連絡して有効にしてください。直接アプリを使用してみてください。', diff --git a/web/i18n/zh-Hans/login.ts b/web/i18n/zh-Hans/login.ts index a37fc104e..b63630e28 100644 --- a/web/i18n/zh-Hans/login.ts +++ b/web/i18n/zh-Hans/login.ts @@ -106,6 +106,7 @@ const translation = { licenseInactive: '许可证未激活', licenseInactiveTip: '您所在空间的 Dify Enterprise 许可证尚未激活,请联系管理员以继续使用 Dify。', webapp: { + login: '登录', noLoginMethod: 'Web 应用未配置身份认证方式', noLoginMethodTip: '请联系系统管理员添加身份认证方式', disabled: 'Web 应用身份认证已禁用,请联系系统管理员启用。您也可以尝试直接使用应用。', diff --git a/web/models/share.ts b/web/models/share.ts index 3521365e8..1e3b6d6bb 100644 --- a/web/models/share.ts +++ b/web/models/share.ts @@ -35,7 +35,7 @@ export type AppMeta = { export type AppData = { app_id: string can_replace_logo?: boolean - custom_config?: Record + custom_config: Record | null enable_site?: boolean end_user_id?: string site: SiteInfo diff --git a/web/service/access-control.ts b/web/service/access-control.ts index 36999bf8f..d4cc9eb79 100644 --- a/web/service/access-control.ts +++ b/web/service/access-control.ts @@ -1,8 +1,9 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { get, post } from './base' -import { getAppAccessMode, getUserCanAccess } from './share' +import { getUserCanAccess } from './share' import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control' import type { App } from '@/types/app' +import { useGlobalPublicStore } from '@/context/global-public-context' const NAME_SPACE = 'access-control' @@ -69,25 +70,18 @@ export const useUpdateAccessMode = () => { }) } -export const useGetAppAccessMode = ({ appId, isInstalledApp = true, enabled }: { appId?: string; isInstalledApp?: boolean; enabled: boolean }) => { - return useQuery({ - queryKey: [NAME_SPACE, 'app-access-mode', appId], - queryFn: () => getAppAccessMode(appId!, isInstalledApp), - enabled: !!appId && enabled, - staleTime: 0, - gcTime: 0, - }) -} - -export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true, enabled }: { appId?: string; isInstalledApp?: boolean; enabled: boolean }) => { +export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true }: { appId?: string; isInstalledApp?: boolean; }) => { + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return useQuery({ queryKey: [NAME_SPACE, 'user-can-access-app', appId], - queryFn: () => getUserCanAccess(appId!, isInstalledApp), - enabled: !!appId && enabled, + queryFn: () => { + if (systemFeatures.webapp_auth.enabled) + return getUserCanAccess(appId!, isInstalledApp) + else + return { result: true } + }, + enabled: !!appId, staleTime: 0, gcTime: 0, - initialData: { - result: !enabled, - }, }) } diff --git a/web/service/base.ts b/web/service/base.ts index 49377be91..8ffacaa0f 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -413,7 +413,7 @@ export const ssePost = async ( if (data.code === 'unauthorized') { removeAccessToken() - globalThis.location.reload() + requiredWebSSOLogin() } } }) @@ -507,7 +507,7 @@ export const request = async(url: string, options = {}, otherOptions?: IOther } = otherOptionsForBaseFetch if (isPublicAPI && code === 'unauthorized') { removeAccessToken() - globalThis.location.reload() + requiredWebSSOLogin() return Promise.reject(err) } if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) { diff --git a/web/service/explore.ts b/web/service/explore.ts index e9e17416d..6a440d7f5 100644 --- a/web/service/explore.ts +++ b/web/service/explore.ts @@ -1,5 +1,6 @@ import { del, get, patch, post } from './base' import type { App, AppCategory } from '@/models/explore' +import type { AccessMode } from '@/models/access-control' export const fetchAppList = () => { return get<{ @@ -39,3 +40,7 @@ export const updatePinStatus = (id: string, isPinned: boolean) => { export const getToolProviders = () => { return get('/workspaces/current/tool-providers') } + +export const getAppAccessModeByAppId = (appId: string) => { + return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) +} diff --git a/web/service/share.ts b/web/service/share.ts index 6a2a7e5b1..8c33b8552 100644 --- a/web/service/share.ts +++ b/web/service/share.ts @@ -296,13 +296,6 @@ export const fetchAccessToken = async ({ appCode, userId, webAppAccessToken }: { return get(url, { headers }) as Promise<{ access_token: string }> } -export const getAppAccessMode = (appId: string, isInstalledApp: boolean) => { - if (isInstalledApp) - return consoleGet<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) - - return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appId=${appId}`) -} - export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => { if (isInstalledApp) return consoleGet<{ result: boolean }>(`/enterprise/webapp/permission?appId=${appId}`) diff --git a/web/service/use-explore.ts b/web/service/use-explore.ts new file mode 100644 index 000000000..b7d078edb --- /dev/null +++ b/web/service/use-explore.ts @@ -0,0 +1,81 @@ +import { useGlobalPublicStore } from '@/context/global-public-context' +import { AccessMode } from '@/models/access-control' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore' +import { fetchAppMeta, fetchAppParams } from './share' + +const NAME_SPACE = 'explore' + +export const useGetInstalledApps = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'installedApps'], + queryFn: () => { + return fetchInstalledAppList() + }, + }) +} + +export const useUninstallApp = () => { + const client = useQueryClient() + return useMutation({ + mutationKey: [NAME_SPACE, 'uninstallApp'], + mutationFn: (appId: string) => uninstallApp(appId), + onSuccess: () => { + client.invalidateQueries({ queryKey: [NAME_SPACE, 'installedApps'] }) + }, + }) +} + +export const useUpdateAppPinStatus = () => { + const client = useQueryClient() + return useMutation({ + mutationKey: [NAME_SPACE, 'updateAppPinStatus'], + mutationFn: ({ appId, isPinned }: { appId: string; isPinned: boolean }) => updatePinStatus(appId, isPinned), + onSuccess: () => { + client.invalidateQueries({ queryKey: [NAME_SPACE, 'installedApps'] }) + }, + }) +} + +export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => { + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) + return useQuery({ + queryKey: [NAME_SPACE, 'appAccessMode', appId], + queryFn: () => { + if (systemFeatures.webapp_auth.enabled === false) { + return { + accessMode: AccessMode.PUBLIC, + } + } + if (!appId || appId.length === 0) + return Promise.reject(new Error('App code is required to get access mode')) + + return getAppAccessModeByAppId(appId) + }, + enabled: !!appId, + }) +} + +export const useGetInstalledAppParams = (appId: string | null) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appParams', appId], + queryFn: () => { + if (!appId || appId.length === 0) + return Promise.reject(new Error('App ID is required to get app params')) + return fetchAppParams(true, appId) + }, + enabled: !!appId, + }) +} + +export const useGetInstalledAppMeta = (appId: string | null) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appMeta', appId], + queryFn: () => { + if (!appId || appId.length === 0) + return Promise.reject(new Error('App ID is required to get app meta')) + return fetchAppMeta(true, appId) + }, + enabled: !!appId, + }) +} diff --git a/web/service/use-share.ts b/web/service/use-share.ts index b8f96f6cc..63f18bf0e 100644 --- a/web/service/use-share.ts +++ b/web/service/use-share.ts @@ -1,17 +1,52 @@ +import { useGlobalPublicStore } from '@/context/global-public-context' +import { AccessMode } from '@/models/access-control' import { useQuery } from '@tanstack/react-query' -import { getAppAccessModeByAppCode } from './share' +import { fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share' const NAME_SPACE = 'webapp' -export const useAppAccessModeByCode = (code: string | null) => { +export const useGetWebAppAccessModeByCode = (code: string | null) => { + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return useQuery({ queryKey: [NAME_SPACE, 'appAccessMode', code], queryFn: () => { - if (!code) - return null + if (systemFeatures.webapp_auth.enabled === false) { + return { + accessMode: AccessMode.PUBLIC, + } + } + if (!code || code.length === 0) + return Promise.reject(new Error('App code is required to get access mode')) return getAppAccessModeByAppCode(code) }, enabled: !!code, }) } + +export const useGetWebAppInfo = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'appInfo'], + queryFn: () => { + return fetchAppInfo() + }, + }) +} + +export const useGetWebAppParams = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'appParams'], + queryFn: () => { + return fetchAppParams(false) + }, + }) +} + +export const useGetWebAppMeta = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'appMeta'], + queryFn: () => { + return fetchAppMeta(false) + }, + }) +}