Browse Source

Perf/web app authrozation (#22524)

tags/1.7.0
NFish 3 months ago
parent
commit
a324d3942e
No account linked to committer's email address
35 changed files with 591 additions and 440 deletions
  1. 8
    6
      web/app/(commonLayout)/explore/installed/[appId]/page.tsx
  2. 4
    1
      web/app/(shareLayout)/chat/[token]/page.tsx
  3. 4
    1
      web/app/(shareLayout)/chatbot/[token]/page.tsx
  4. 4
    1
      web/app/(shareLayout)/completion/[token]/page.tsx
  5. 84
    0
      web/app/(shareLayout)/components/authenticated-layout.tsx
  6. 80
    0
      web/app/(shareLayout)/components/splash.tsx
  7. 9
    48
      web/app/(shareLayout)/layout.tsx
  8. 6
    3
      web/app/(shareLayout)/webapp-signin/layout.tsx
  9. 1
    0
      web/app/(shareLayout)/webapp-signin/normalForm.tsx
  10. 8
    75
      web/app/(shareLayout)/webapp-signin/page.tsx
  11. 4
    1
      web/app/(shareLayout)/workflow/[token]/page.tsx
  12. 2
    6
      web/app/components/base/chat/chat-with-history/context.tsx
  13. 5
    17
      web/app/components/base/chat/chat-with-history/hooks.tsx
  14. 1
    49
      web/app/components/base/chat/chat-with-history/index.tsx
  15. 0
    94
      web/app/components/base/chat/embedded-chatbot/index.tsx
  16. 10
    0
      web/app/components/base/chat/types.ts
  17. 3
    0
      web/app/components/explore/index.tsx
  18. 85
    10
      web/app/components/explore/installed-app/index.tsx
  19. 17
    13
      web/app/components/explore/sidebar/index.tsx
  20. 20
    76
      web/app/components/share/text-generation/index.tsx
  21. 2
    2
      web/app/components/share/text-generation/menu-dropdown.tsx
  22. 1
    1
      web/app/components/share/utils.ts
  23. 4
    0
      web/context/explore-context.ts
  24. 0
    5
      web/context/global-public-context.tsx
  25. 87
    0
      web/context/web-app-context.tsx
  26. 1
    0
      web/i18n/en-US/login.ts
  27. 1
    0
      web/i18n/ja-JP/login.ts
  28. 1
    0
      web/i18n/zh-Hans/login.ts
  29. 1
    1
      web/models/share.ts
  30. 11
    17
      web/service/access-control.ts
  31. 2
    2
      web/service/base.ts
  32. 5
    0
      web/service/explore.ts
  33. 0
    7
      web/service/share.ts
  34. 81
    0
      web/service/use-explore.ts
  35. 39
    4
      web/service/use-share.ts

+ 8
- 6
web/app/(commonLayout)/explore/installed/[appId]/page.tsx View File

import type { FC } from 'react'
import React from 'react' import React from 'react'
import Main from '@/app/components/explore/installed-app' import Main from '@/app/components/explore/installed-app'


export type IInstalledAppProps = { export type IInstalledAppProps = {
params: Promise<{
params: {
appId: string appId: string
}>
}
} }


const InstalledApp: FC<IInstalledAppProps> = async ({ params }) => {
// Using Next.js page convention for async server components
async function InstalledApp({ params }: IInstalledAppProps) {
const appId = (await params).appId
return ( return (
<Main id={(await params).appId} />
<Main id={appId} />
) )
} }
export default React.memo(InstalledApp)

export default InstalledApp

+ 4
- 1
web/app/(shareLayout)/chat/[token]/page.tsx View File

'use client' 'use client'
import React from 'react' import React from 'react'
import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history' import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history'
import AuthenticatedLayout from '../../components/authenticated-layout'


const Chat = () => { const Chat = () => {
return ( return (
<ChatWithHistoryWrap />
<AuthenticatedLayout>
<ChatWithHistoryWrap />
</AuthenticatedLayout>
) )
} }



+ 4
- 1
web/app/(shareLayout)/chatbot/[token]/page.tsx View File

'use client' 'use client'
import React from 'react' import React from 'react'
import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot' import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot'
import AuthenticatedLayout from '../../components/authenticated-layout'


const Chatbot = () => { const Chatbot = () => {
return ( return (
<EmbeddedChatbot />
<AuthenticatedLayout>
<EmbeddedChatbot />
</AuthenticatedLayout>
) )
} }



+ 4
- 1
web/app/(shareLayout)/completion/[token]/page.tsx View File

import React from 'react' import React from 'react'
import Main from '@/app/components/share/text-generation' import Main from '@/app/components/share/text-generation'
import AuthenticatedLayout from '../../components/authenticated-layout'


const Completion = () => { const Completion = () => {
return ( return (
<Main />
<AuthenticatedLayout>
<Main />
</AuthenticatedLayout>
) )
} }



+ 84
- 0
web/app/(shareLayout)/components/authenticated-layout.tsx View File

'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 <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={appInfoError.message} />
</div>
}
if (appParamsError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={appParamsError.message} />
</div>
}
if (appMetaError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={appMetaError.message} />
</div>
}
if (useCanAccessAppError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={useCanAccessAppError.message} />
</div>
}
if (userCanAccessApp && !userCanAccessApp.result) {
return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
<AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
<span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>
</div>
}
if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta) {
return <div className='flex h-full items-center justify-center'>
<Loading />
</div>
}
return <>{children}</>
}

export default React.memo(AuthenticatedLayout)

+ 80
- 0
web/app/(shareLayout)/components/splash.tsx View File

'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<PropsWithChildren> = ({ 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 <div className='flex h-full flex-col items-center justify-center gap-y-4'>
<AppUnavailable className='h-auto w-auto' code={code || t('share.common.appUnavailable')} unknownReason={message} />
<span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span>
</div>
}
if (tokenFromUrl) {
return <div className='flex h-full items-center justify-center'>
<Loading />
</div>
}
if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) {
return <div className='flex h-full items-center justify-center'>
<Loading />
</div>
}
return <>{children}</>
}

export default Splash

+ 9
- 48
web/app/(shareLayout)/layout.tsx View File

'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 <div className='flex h-full w-full items-center justify-center'>
<Loading />
</div>
}
const Layout: FC<PropsWithChildren> = ({ children }) => {
return ( return (
<div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]"> <div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]">
{children}
<WebAppStoreProvider>
<Splash>
{children}
</Splash>
</WebAppStoreProvider>
</div> </div>
) )
} }

+ 6
- 3
web/app/(shareLayout)/webapp-signin/layout.tsx View File

import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title' 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 <> return <>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}> <div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}> <div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>

+ 1
- 0
web/app/(shareLayout)/webapp-signin/normalForm.tsx View File

'use client'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Link from 'next/link' import Link from 'next/link'

+ 8
- 75
web/app/(shareLayout)/webapp-signin/page.tsx View File

'use client' 'use client'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback, useEffect } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next' 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 { useGlobalPublicStore } from '@/context/global-public-context'
import Loading from '@/app/components/base/loading'
import AppUnavailable from '@/app/components/base/app-unavailable' import AppUnavailable from '@/app/components/base/app-unavailable'
import NormalForm from './normalForm' import NormalForm from './normalForm'
import { AccessMode } from '@/models/access-control' import { AccessMode } from '@/models/access-control'
import ExternalMemberSsoAuth from './components/external-member-sso-auth' import ExternalMemberSsoAuth from './components/external-member-sso-auth'
import { fetchAccessToken } from '@/service/share'
import { useWebAppStore } from '@/context/web-app-context'


const WebSSOForm: FC = () => { const WebSSOForm: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode)
const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode)
const searchParams = useSearchParams() const searchParams = useSearchParams()
const router = useRouter() const router = useRouter()


const redirectUrl = searchParams.get('redirect_url') 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 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()}` return `/webapp-signin?${params.toString()}`
}, [searchParams])
}, [redirectUrl])


const backToHome = useCallback(() => { const backToHome = useCallback(() => {
removeAccessToken() removeAccessToken()
router.replace(url) router.replace(url)
}, [getSigninUrl, router]) }, [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 <div className='flex h-full items-center justify-center'>
<Loading />
</div>
}

if (message) {
return <div className='flex h-full flex-col items-center justify-center gap-y-4'>
<AppUnavailable className='h-auto w-auto' code={code || t('share.common.appUnavailable')} unknownReason={message} />
<span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span>
</div>
}
if (!redirectUrl) { if (!redirectUrl) {
showErrorToast('redirect url is invalid.')
return <div className='flex h-full items-center justify-center'> return <div className='flex h-full items-center justify-center'>
<AppUnavailable code={t('share.common.appUnavailable')} unknownReason='redirect url is invalid.' /> <AppUnavailable code={t('share.common.appUnavailable')} unknownReason='redirect url is invalid.' />
</div> </div>
} }
if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) {
return <div className='flex h-full items-center justify-center'>
<Loading />
</div>
}

if (!systemFeatures.webapp_auth.enabled) { if (!systemFeatures.webapp_auth.enabled) {
return <div className="flex h-full items-center justify-center"> return <div className="flex h-full items-center justify-center">
<p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p> <p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p>

+ 4
- 1
web/app/(shareLayout)/workflow/[token]/page.tsx View File

import React from 'react' import React from 'react'


import Main from '@/app/components/share/text-generation' import Main from '@/app/components/share/text-generation'
import AuthenticatedLayout from '../../components/authenticated-layout'


const Workflow = () => { const Workflow = () => {
return ( return (
<Main isWorkflow />
<AuthenticatedLayout>
<Main isWorkflow />
</AuthenticatedLayout>
) )
} }



+ 2
- 6
web/app/components/base/chat/chat-with-history/context.tsx View File

import { noop } from 'lodash-es' import { noop } from 'lodash-es'


export type ChatWithHistoryContextValue = { export type ChatWithHistoryContextValue = {
appInfoError?: any
appInfoLoading?: boolean
appMeta?: AppMeta
appData?: AppData
userCanAccess?: boolean
appMeta?: AppMeta | null
appData?: AppData | null
appParams?: ChatConfig appParams?: ChatConfig
appChatListDataLoading?: boolean appChatListDataLoading?: boolean
currentConversationId: string currentConversationId: string
} }


export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
userCanAccess: false,
currentConversationId: '', currentConversationId: '',
appPrevChatTree: [], appPrevChatTree: [],
pinnedConversationList: [], pinnedConversationList: [],

+ 5
- 17
web/app/components/base/chat/chat-with-history/hooks.tsx View File

import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
import { import {
delConversation, delConversation,
fetchAppInfo,
fetchAppMeta,
fetchAppParams,
fetchChatList, fetchChatList,
fetchConversations, fetchConversations,
generationConversationName, generationConversationName,
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { noop } from 'lodash-es' 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[]) { function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = [] const newChatList: ChatItem[] = []


export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) 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({ useAppFavicon({
enable: !installedAppInfo, enable: !installedAppInfo,
use_icon_as_answer_icon: app.use_icon_as_answer_icon, use_icon_as_answer_icon: app.use_icon_as_answer_icon,
}, },
plan: 'basic', plan: 'basic',
custom_config: null,
} as AppData } as AppData
} }


return currentConversationId return currentConversationId
}, [currentConversationId, newConversationId]) }, [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: 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: 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)) const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
}, [isInstalledApp, appId, t, notify]) }, [isInstalledApp, appId, t, notify])


return { return {
appInfoError,
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission),
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
isInstalledApp, isInstalledApp,
appId, appId,
currentConversationId, currentConversationId,

+ 1
- 49
web/app/components/base/chat/chat-with-history/index.tsx View File

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { import {
useCallback,
useEffect, useEffect,
useState, useState,
} from 'react' } from 'react'
import type { InstalledApp } from '@/models/explore' import type { InstalledApp } from '@/models/explore'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' 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 AppUnavailable from '@/app/components/base/app-unavailable'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title' import useDocumentTitle from '@/hooks/use-document-title'
import { useTranslation } from 'react-i18next'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'


type ChatWithHistoryProps = { type ChatWithHistoryProps = {
className?: string className?: string
className, className,
}) => { }) => {
const { const {
userCanAccess,
appInfoError,
appData, appData,
appInfoLoading,
appChatListDataLoading, appChatListDataLoading,
chatShouldReloadKey, chatShouldReloadKey,
isMobile, isMobile,
themeBuilder, themeBuilder,
sidebarCollapseState, sidebarCollapseState,
isInstalledApp,
} = useChatWithHistoryContext() } = useChatWithHistoryContext()
const isSidebarCollapsed = sidebarCollapseState const isSidebarCollapsed = sidebarCollapseState
const customConfig = appData?.custom_config const customConfig = appData?.custom_config


useDocumentTitle(site?.title || 'Chat') 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 (
<Loading type='app' />
)
}
if (!userCanAccess) {
return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
<AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
{!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>}
</div>
}

if (appInfoError) {
return (
<AppUnavailable />
)
}

return ( return (
<div className={cn( <div className={cn(
'flex h-full bg-background-default-burn', 'flex h-full bg-background-default-burn',
const themeBuilder = useThemeContext() const themeBuilder = useThemeContext()


const { const {
appInfoError,
appInfoLoading,
userCanAccess,
appData, appData,
appParams, appParams,
appMeta, appMeta,


return ( return (
<ChatWithHistoryContext.Provider value={{ <ChatWithHistoryContext.Provider value={{
appInfoError,
appInfoLoading,
appData, appData,
userCanAccess,
appParams, appParams,
appMeta, appMeta,
appChatListDataLoading, appChatListDataLoading,

+ 0
- 94
web/app/components/base/chat/embedded-chatbot/index.tsx View File

'use client' 'use client'
import { import {
useCallback,
useEffect, useEffect,
useState,
} from 'react' } from 'react'
import { useAsyncEffect } from 'ahooks'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
EmbeddedChatbotContext, EmbeddedChatbotContext,
import { isDify } from './utils' import { isDify } from './utils'
import { useThemeContext } from './theme/theme-context' import { useThemeContext } from './theme/theme-context'
import { CssTransform } from './theme/utils' import { CssTransform } from './theme/utils'
import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header' import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title' import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'


const Chatbot = () => { const Chatbot = () => {
const { const {
userCanAccess,
isMobile, isMobile,
allowResetChat, allowResetChat,
appInfoError,
appInfoLoading,
appData, appData,
appChatListDataLoading, appChatListDataLoading,
chatShouldReloadKey, chatShouldReloadKey,
handleNewConversation, handleNewConversation,
themeBuilder, themeBuilder,
isInstalledApp,
} = useEmbeddedChatbotContext() } = useEmbeddedChatbotContext()
const { t } = useTranslation() const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)


useDocumentTitle(site?.title || 'Chat') 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 && <Loading type='app' />}
{isMobile && (
<div className={cn('relative')}>
<div className={cn('flex h-[calc(100vh_-_60px)] flex-col rounded-2xl border-[0.5px] border-components-panel-border shadow-xs')}>
<Loading type='app' />
</div>
</div>
)}
</>
)
}

if (!userCanAccess) {
return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
<AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
{!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>}
</div>
}

if (appInfoError) {
return (
<>
{!isMobile && <AppUnavailable />}
{isMobile && (
<div className={cn('relative')}>
<div className={cn('flex h-[calc(100vh_-_60px)] flex-col rounded-2xl border-[0.5px] border-components-panel-border shadow-xs')}>
<AppUnavailable />
</div>
</div>
)}
</>
)
}
return ( return (
<div className='relative'> <div className='relative'>
<div <div
const themeBuilder = useThemeContext() const themeBuilder = useThemeContext()


const { const {
appInfoError,
appInfoLoading,
appData, appData,
userCanAccess, userCanAccess,
appParams, appParams,


return <EmbeddedChatbotContext.Provider value={{ return <EmbeddedChatbotContext.Provider value={{
userCanAccess, userCanAccess,
appInfoError,
appInfoLoading,
appData, appData,
appParams, appParams,
appMeta, appMeta,
} }


const EmbeddedChatbot = () => { const EmbeddedChatbot = () => {
const [initialized, setInitialized] = useState(false)
const [appUnavailable, setAppUnavailable] = useState<boolean>(false)
const [isUnknownReason, setIsUnknownReason] = useState<boolean>(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 <AppUnavailable isUnknownReason={isUnknownReason} />

return <EmbeddedChatbotWrapper /> return <EmbeddedChatbotWrapper />
} }



+ 10
- 0
web/app/components/base/chat/types.ts View File

questionEditEnable?: boolean questionEditEnable?: boolean
supportFeedback?: boolean supportFeedback?: boolean
supportCitationHitInfo?: 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 = { export type WorkflowProcess = {

+ 3
- 0
web/app/components/explore/index.tsx View File

const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext() const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext()
const [hasEditPermission, setHasEditPermission] = useState(false) const [hasEditPermission, setHasEditPermission] = useState(false)
const [installedApps, setInstalledApps] = useState<InstalledApp[]>([]) const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
const [isFetchingInstalledApps, setIsFetchingInstalledApps] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()


useDocumentTitle(t('common.menus.explore')) useDocumentTitle(t('common.menus.explore'))
hasEditPermission, hasEditPermission,
installedApps, installedApps,
setInstalledApps, setInstalledApps,
isFetchingInstalledApps,
setIsFetchingInstalledApps,
} }
} }
> >

+ 85
- 10
web/app/components/explore/installed-app/index.tsx View File

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useEffect } from 'react'
import React from 'react' import React from 'react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import ExploreContext from '@/context/explore-context' import ExploreContext from '@/context/explore-context'
import TextGenerationApp from '@/app/components/share/text-generation' import TextGenerationApp from '@/app/components/share/text-generation'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import ChatWithHistory from '@/app/components/base/chat/chat-with-history' 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 = { export type IInstalledAppProps = {
id: string id: string
const InstalledApp: FC<IInstalledAppProps> = ({ const InstalledApp: FC<IInstalledAppProps> = ({
id, 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 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 })


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 <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={appParamsError.message} />
</div>
}
if (appMetaError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={appMetaError.message} />
</div>
}
if (useCanAccessAppError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={useCanAccessAppError.message} />
</div>
}
if (webAppAccessModeError) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable unknownReason={webAppAccessModeError.message} />
</div>
}
if (userCanAccessApp && !userCanAccessApp.result) {
return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
<AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
</div>
}
if (isFetchingAppParams || isFetchingAppMeta || isFetchingWebAppAccessMode || isFetchingInstalledApps) {
return <div className='flex h-full items-center justify-center'>
<Loading />
</div>
}
if (!installedApp) { if (!installedApp) {
return (
<div className='flex h-full items-center'>
<Loading type='area' />
</div>
)
return <div className='flex h-full items-center justify-center'>
<AppUnavailable code={404} isUnknownReason />
</div>
} }

return ( return (
<div className='h-full bg-background-default py-2 pl-0 pr-2 sm:p-2'> <div className='h-full bg-background-default py-2 pl-0 pr-2 sm:p-2'>
{installedApp.app.mode !== 'completion' && installedApp.app.mode !== 'workflow' && (
{installedApp?.app.mode !== 'completion' && installedApp?.app.mode !== 'workflow' && (
<ChatWithHistory installedAppInfo={installedApp} className='overflow-hidden rounded-2xl shadow-md' /> <ChatWithHistory installedAppInfo={installedApp} className='overflow-hidden rounded-2xl shadow-md' />
)} )}
{installedApp.app.mode === 'completion' && (
{installedApp?.app.mode === 'completion' && (
<TextGenerationApp isInstalledApp installedAppInfo={installedApp} /> <TextGenerationApp isInstalledApp installedAppInfo={installedApp} />
)} )}
{installedApp.app.mode === 'workflow' && (
{installedApp?.app.mode === 'workflow' && (
<TextGenerationApp isWorkflow isInstalledApp installedAppInfo={installedApp} /> <TextGenerationApp isWorkflow isInstalledApp installedAppInfo={installedApp} />
)} )}
</div> </div>

+ 17
- 13
web/app/components/explore/sidebar/index.tsx View File

import Toast from '../../base/toast' import Toast from '../../base/toast'
import Item from './app-nav-item' import Item from './app-nav-item'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { fetchInstalledAppList as doFetchInstalledAppList, uninstallApp, updatePinStatus } from '@/service/explore'
import ExploreContext from '@/context/explore-context' import ExploreContext from '@/context/explore-context'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'


const SelectedDiscoveryIcon = () => ( const SelectedDiscoveryIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg">
const lastSegment = segments.slice(-1)[0] const lastSegment = segments.slice(-1)[0]
const isDiscoverySelected = lastSegment === 'apps' const isDiscoverySelected = lastSegment === 'apps'
const isChatSelected = lastSegment === 'chat' 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 media = useBreakpoints()
const isMobile = media === MediaType.mobile const isMobile = media === MediaType.mobile


const fetchInstalledAppList = async () => {
const { installed_apps }: any = await doFetchInstalledAppList()
setInstalledApps(installed_apps)
}

const [showConfirm, setShowConfirm] = useState(false) const [showConfirm, setShowConfirm] = useState(false)
const [currId, setCurrId] = useState('') const [currId, setCurrId] = useState('')
const handleDelete = async () => { const handleDelete = async () => {
type: 'success', type: 'success',
message: t('common.api.remove'), message: t('common.api.remove'),
}) })
fetchInstalledAppList()
} }


const handleUpdatePinStatus = async (id: string, isPinned: boolean) => { const handleUpdatePinStatus = async (id: string, isPinned: boolean) => {
await updatePinStatus(id, isPinned)
await updatePinStatus({ appId: id, isPinned })
Toast.notify({ Toast.notify({
type: 'success', type: 'success',
message: t('common.api.success'), message: t('common.api.success'),
}) })
fetchInstalledAppList()
} }


useEffect(() => { 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(() => { useEffect(() => {
fetchInstalledAppList() fetchInstalledAppList()
}, [controlUpdateInstalledApps])
}, [controlUpdateInstalledApps, fetchInstalledAppList])


const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
return ( return (

+ 20
- 76
web/app/components/share/text-generation/index.tsx View File

RiErrorWarningFill, RiErrorWarningFill,
} from '@remixicon/react' } from '@remixicon/react'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useSearchParams } from 'next/navigation'
import TabHeader from '../../base/tab-header' import TabHeader from '../../base/tab-header'
import { checkOrSetAccessToken, removeAccessToken } from '../utils'
import MenuDropdown from './menu-dropdown' import MenuDropdown from './menu-dropdown'
import RunBatch from './run-batch' import RunBatch from './run-batch'
import ResDownload from './run-batch/res-download' import ResDownload from './run-batch/res-download'
import AppUnavailable from '../../base/app-unavailable'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import RunOnce from '@/app/components/share/text-generation/run-once' 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 { SiteInfo } from '@/models/share'
import type { import type {
MoreLikeThisConfig, MoreLikeThisConfig,
import { useAppFavicon } from '@/hooks/use-app-favicon' import { useAppFavicon } from '@/hooks/use-app-favicon'
import DifyLogo from '@/app/components/base/logo/dify-logo' import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
import { AccessMode } from '@/models/access-control' import { AccessMode } from '@/models/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title' 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. const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
enum TaskStatus { enum TaskStatus {
const mode = searchParams.get('mode') || 'create' const mode = searchParams.get('mode') || 'create'
const [currentTab, setCurrentTab] = useState<string>(['create', 'batch'].includes(mode) ? mode : 'create') const [currentTab, setCurrentTab] = useState<string>(['create', 'batch'].includes(mode) ? mode : 'create')


const router = useRouter()
const pathname = usePathname()

// Notice this situation isCallBatchAPI but not in batch tab // Notice this situation isCallBatchAPI but not in batch tab
const [isCallBatchAPI, setIsCallBatchAPI] = useState(false) const [isCallBatchAPI, setIsCallBatchAPI] = useState(false)
const isInBatchTab = currentTab === 'batch' const isInBatchTab = currentTab === 'batch'
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null) const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null)
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null) const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(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 // save message
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([]) const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
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) setSavedMessages(res.data)
}
}, [isInstalledApp, appId])
const handleSaveMessage = async (messageId: string) => { const handleSaveMessage = async (messageId: string) => {
await saveMessage(messageId, isInstalledApp, installedAppInfo?.id)
await saveMessage(messageId, isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.saved') }) notify({ type: 'success', message: t('common.api.saved') })
fetchSavedMessage() fetchSavedMessage()
} }
const handleRemoveSavedMessage = async (messageId: string) => { const handleRemoveSavedMessage = async (messageId: string) => {
await removeMessage(messageId, isInstalledApp, installedAppInfo?.id)
await removeMessage(messageId, isInstalledApp, appId)
notify({ type: 'success', message: t('common.api.remove') }) notify({ type: 'success', message: t('common.api.remove') })
fetchSavedMessage() fetchSavedMessage()
} }
} }
} }


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(() => { useEffect(() => {
(async () => { (async () => {
const [appData, appParams]: any = await fetchInitData()
if (!appData || !appParams)
return
!isWorkflow && fetchSavedMessage()
const { app_id: appId, site: siteInfo, custom_config } = appData const { app_id: appId, site: siteInfo, custom_config } = appData
setAppId(appId) setAppId(appId)
setSiteInfo(siteInfo as SiteInfo) setSiteInfo(siteInfo as SiteInfo)
setVisionConfig({ setVisionConfig({
// legacy of image upload compatible // legacy of image upload compatible
...file_upload, ...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 // 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, fileUploadConfig: appParams?.system_parameters,
})
} as any)
const prompt_variables = userInputsFormToPromptVariables(user_input_form) const prompt_variables = userInputsFormToPromptVariables(user_input_form)
setPromptConfig({ setPromptConfig({
prompt_template: '', // placeholder for future prompt_template: '', // placeholder for future
setMoreLikeThisConfig(more_like_this) setMoreLikeThisConfig(more_like_this)
setTextToSpeechConfig(text_to_speech) 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. // 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')) useDocumentTitle(siteInfo?.title || t('share.generation.title'))
</div> </div>
) )


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 ( return (
<div className='flex h-screen items-center'> <div className='flex h-screen items-center'>
<Loading type='app' /> <Loading type='app' />
</div>) </div>)
} }
if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) {
return <div className='flex h-full flex-col items-center justify-center gap-y-2'>
<AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' />
{!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>}
</div>
}

return ( return (
<div className={cn( <div className={cn(
'bg-background-default-burn', 'bg-background-default-burn',
imageUrl={siteInfo.icon_url} imageUrl={siteInfo.icon_url}
/> />
<div className='system-md-semibold grow truncate text-text-secondary'>{siteInfo.title}</div> <div className='system-md-semibold grow truncate text-text-secondary'>{siteInfo.title}</div>
<MenuDropdown hideLogout={isInstalledApp || appAccessMode?.accessMode === AccessMode.PUBLIC} data={siteInfo} />
<MenuDropdown hideLogout={isInstalledApp || accessMode === AccessMode.PUBLIC} data={siteInfo} />
</div> </div>
{siteInfo.description && ( {siteInfo.description && (
<div className='system-xs-regular text-text-tertiary'>{siteInfo.description}</div> <div className='system-xs-regular text-text-tertiary'>{siteInfo.description}</div>

+ 2
- 2
web/app/components/share/text-generation/menu-dropdown.tsx View File

import ThemeSwitcher from '@/app/components/base/theme-switcher' import ThemeSwitcher from '@/app/components/base/theme-switcher'
import type { SiteInfo } from '@/models/share' import type { SiteInfo } from '@/models/share'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control' import { AccessMode } from '@/models/access-control'
import { useWebAppStore } from '@/context/web-app-context'


type Props = { type Props = {
data?: SiteInfo data?: SiteInfo
placement, placement,
hideLogout, hideLogout,
}) => { }) => {
const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode)
const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode)
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const { t } = useTranslation() const { t } = useTranslation()

+ 1
- 1
web/app/components/share/utils.ts View File

version: 2, 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 sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0]
const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id
const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2())

+ 4
- 0
web/context/explore-context.ts View File

hasEditPermission: boolean hasEditPermission: boolean
installedApps: InstalledApp[] installedApps: InstalledApp[]
setInstalledApps: (installedApps: InstalledApp[]) => void setInstalledApps: (installedApps: InstalledApp[]) => void
isFetchingInstalledApps: boolean
setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void
} }


const ExploreContext = createContext<IExplore>({ const ExploreContext = createContext<IExplore>({
hasEditPermission: false, hasEditPermission: false,
installedApps: [], installedApps: [],
setInstalledApps: noop, setInstalledApps: noop,
isFetchingInstalledApps: false,
setIsFetchingInstalledApps: noop,
}) })


export default ExploreContext export default ExploreContext

+ 0
- 5
web/context/global-public-context.tsx View File

import { defaultSystemFeatures } from '@/types/feature' import { defaultSystemFeatures } from '@/types/feature'
import { getSystemFeatures } from '@/service/common' import { getSystemFeatures } from '@/service/common'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { AccessMode } from '@/models/access-control'


type GlobalPublicStore = { type GlobalPublicStore = {
isGlobalPending: boolean isGlobalPending: boolean
setIsGlobalPending: (isPending: boolean) => void setIsGlobalPending: (isPending: boolean) => void
systemFeatures: SystemFeatures systemFeatures: SystemFeatures
setSystemFeatures: (systemFeatures: SystemFeatures) => void setSystemFeatures: (systemFeatures: SystemFeatures) => void
webAppAccessMode: AccessMode,
setWebAppAccessMode: (webAppAccessMode: AccessMode) => void
} }


export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({ export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({
setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })), setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })),
systemFeatures: defaultSystemFeatures, systemFeatures: defaultSystemFeatures,
setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
webAppAccessMode: AccessMode.PUBLIC,
setWebAppAccessMode: (webAppAccessMode: AccessMode) => set(() => ({ webAppAccessMode })),
})) }))


const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({ const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({

+ 87
- 0
web/context/web-app-context.tsx View File

'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<WebAppStore>(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<PropsWithChildren> = ({ 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<string | null>(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 <div className='flex h-full w-full items-center justify-center'>
<Loading />
</div>
}
return (
<>
{children}
</>
)
}
export default WebAppStoreProvider

+ 1
- 0
web/i18n/en-US/login.ts View File

licenseInactive: 'License Inactive', licenseInactive: 'License Inactive',
licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.', licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.',
webapp: { webapp: {
login: 'Login',
noLoginMethod: 'Authentication method not configured for web app', noLoginMethod: 'Authentication method not configured for web app',
noLoginMethodTip: 'Please contact the system admin to add an authentication method.', 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.', disabled: 'Webapp authentication is disabled. Please contact the system admin to enable it. You can try to use the app directly.',

+ 1
- 0
web/i18n/ja-JP/login.ts View File

licenseExpired: 'ライセンスの有効期限が切れています', licenseExpired: 'ライセンスの有効期限が切れています',
licenseLostTip: 'Dify ライセンスサーバーへの接続に失敗しました。続けて Dify を使用するために管理者に連絡してください。', licenseLostTip: 'Dify ライセンスサーバーへの接続に失敗しました。続けて Dify を使用するために管理者に連絡してください。',
webapp: { webapp: {
login: 'ログイン',
noLoginMethod: 'Web アプリに対して認証方法が構成されていません', noLoginMethod: 'Web アプリに対して認証方法が構成されていません',
noLoginMethodTip: 'システム管理者に連絡して、認証方法を追加してください。', noLoginMethodTip: 'システム管理者に連絡して、認証方法を追加してください。',
disabled: 'Web アプリの認証が無効になっています。システム管理者に連絡して有効にしてください。直接アプリを使用してみてください。', disabled: 'Web アプリの認証が無効になっています。システム管理者に連絡して有効にしてください。直接アプリを使用してみてください。',

+ 1
- 0
web/i18n/zh-Hans/login.ts View File

licenseInactive: '许可证未激活', licenseInactive: '许可证未激活',
licenseInactiveTip: '您所在空间的 Dify Enterprise 许可证尚未激活,请联系管理员以继续使用 Dify。', licenseInactiveTip: '您所在空间的 Dify Enterprise 许可证尚未激活,请联系管理员以继续使用 Dify。',
webapp: { webapp: {
login: '登录',
noLoginMethod: 'Web 应用未配置身份认证方式', noLoginMethod: 'Web 应用未配置身份认证方式',
noLoginMethodTip: '请联系系统管理员添加身份认证方式', noLoginMethodTip: '请联系系统管理员添加身份认证方式',
disabled: 'Web 应用身份认证已禁用,请联系系统管理员启用。您也可以尝试直接使用应用。', disabled: 'Web 应用身份认证已禁用,请联系系统管理员启用。您也可以尝试直接使用应用。',

+ 1
- 1
web/models/share.ts View File

export type AppData = { export type AppData = {
app_id: string app_id: string
can_replace_logo?: boolean can_replace_logo?: boolean
custom_config?: Record<string, any>
custom_config: Record<string, any> | null
enable_site?: boolean enable_site?: boolean
end_user_id?: string end_user_id?: string
site: SiteInfo site: SiteInfo

+ 11
- 17
web/service/access-control.ts View File

import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { get, post } from './base' 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 { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control'
import type { App } from '@/types/app' import type { App } from '@/types/app'
import { useGlobalPublicStore } from '@/context/global-public-context'


const NAME_SPACE = 'access-control' const NAME_SPACE = 'access-control'


}) })
} }


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({ return useQuery({
queryKey: [NAME_SPACE, 'user-can-access-app', appId], 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, staleTime: 0,
gcTime: 0, gcTime: 0,
initialData: {
result: !enabled,
},
}) })
} }

+ 2
- 2
web/service/base.ts View File



if (data.code === 'unauthorized') { if (data.code === 'unauthorized') {
removeAccessToken() removeAccessToken()
globalThis.location.reload()
requiredWebSSOLogin()
} }
} }
}) })
} = otherOptionsForBaseFetch } = otherOptionsForBaseFetch
if (isPublicAPI && code === 'unauthorized') { if (isPublicAPI && code === 'unauthorized') {
removeAccessToken() removeAccessToken()
globalThis.location.reload()
requiredWebSSOLogin()
return Promise.reject(err) return Promise.reject(err)
} }
if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) { if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) {

+ 5
- 0
web/service/explore.ts View File

import { del, get, patch, post } from './base' import { del, get, patch, post } from './base'
import type { App, AppCategory } from '@/models/explore' import type { App, AppCategory } from '@/models/explore'
import type { AccessMode } from '@/models/access-control'


export const fetchAppList = () => { export const fetchAppList = () => {
return get<{ return get<{
export const getToolProviders = () => { export const getToolProviders = () => {
return get('/workspaces/current/tool-providers') return get('/workspaces/current/tool-providers')
} }

export const getAppAccessModeByAppId = (appId: string) => {
return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`)
}

+ 0
- 7
web/service/share.ts View File

return get(url, { headers }) as Promise<{ access_token: string }> 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) => { export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => {
if (isInstalledApp) if (isInstalledApp)
return consoleGet<{ result: boolean }>(`/enterprise/webapp/permission?appId=${appId}`) return consoleGet<{ result: boolean }>(`/enterprise/webapp/permission?appId=${appId}`)

+ 81
- 0
web/service/use-explore.ts View File

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,
})
}

+ 39
- 4
web/service/use-share.ts View File

import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { getAppAccessModeByAppCode } from './share'
import { fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share'


const NAME_SPACE = 'webapp' const NAME_SPACE = 'webapp'


export const useAppAccessModeByCode = (code: string | null) => {
export const useGetWebAppAccessModeByCode = (code: string | null) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
return useQuery({ return useQuery({
queryKey: [NAME_SPACE, 'appAccessMode', code], queryKey: [NAME_SPACE, 'appAccessMode', code],
queryFn: () => { 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) return getAppAccessModeByAppCode(code)
}, },
enabled: !!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)
},
})
}

Loading…
Cancel
Save