| const redirectUrl = searchParams.get('redirect_url') | const redirectUrl = searchParams.get('redirect_url') | ||||
| const tokenFromUrl = searchParams.get('web_sso_token') | const tokenFromUrl = searchParams.get('web_sso_token') | ||||
| const message = searchParams.get('message') | const message = searchParams.get('message') | ||||
| const code = searchParams.get('code') | |||||
| const getSigninUrl = useCallback(() => { | const getSigninUrl = useCallback(() => { | ||||
| const params = new URLSearchParams(searchParams) | const params = new URLSearchParams(searchParams) | ||||
| params.delete('message') | params.delete('message') | ||||
| params.delete('code') | |||||
| return `/webapp-signin?${params.toString()}` | return `/webapp-signin?${params.toString()}` | ||||
| }, [searchParams]) | }, [searchParams]) | ||||
| if (message) { | if (message) { | ||||
| return <div className='flex h-full flex-col items-center justify-center gap-y-4'> | return <div className='flex h-full flex-col items-center justify-center gap-y-4'> | ||||
| <AppUnavailable className='h-auto w-auto' code={t('share.common.appUnavailable')} unknownReason={message} /> | |||||
| <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('share.login.backToHome')}</span> | |||||
| <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> | </div> | ||||
| } | } | ||||
| if (!redirectUrl) { | if (!redirectUrl) { | 
| onClick={() => { | onClick={() => { | ||||
| setShowAppAccessControl(true) | setShowAppAccessControl(true) | ||||
| }}> | }}> | ||||
| <div className='flex grow items-center gap-x-1.5 pr-1'> | |||||
| <div className='flex grow items-center gap-x-1.5 overflow-hidden pr-1'> | |||||
| {appDetail?.access_mode === AccessMode.ORGANIZATION | {appDetail?.access_mode === AccessMode.ORGANIZATION | ||||
| && <> | && <> | ||||
| <RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' /> | <RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' /> | ||||
| {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS | {appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS | ||||
| && <> | && <> | ||||
| <RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' /> | <RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' /> | ||||
| <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p> | |||||
| <div className='grow truncate'> | |||||
| <span className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</span> | |||||
| </div> | |||||
| </> | </> | ||||
| } | } | ||||
| {appDetail?.access_mode === AccessMode.PUBLIC | {appDetail?.access_mode === AccessMode.PUBLIC | 
| return ( | return ( | ||||
| <div className={classNames('flex h-screen w-screen items-center justify-center', className)}> | <div className={classNames('flex h-screen w-screen items-center justify-center', className)}> | ||||
| <h1 className='mr-5 h-[50px] pr-5 text-[24px] font-medium leading-[50px]' | |||||
| <h1 className='mr-5 h-[50px] shrink-0 pr-5 text-[24px] font-medium leading-[50px]' | |||||
| style={{ | style={{ | ||||
| borderRight: '1px solid rgba(0,0,0,.3)', | borderRight: '1px solid rgba(0,0,0,.3)', | ||||
| }}>{code}</h1> | }}>{code}</h1> | 
| '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 } from '@/app/components/share/utils' | |||||
| import { checkOrSetAccessToken, removeAccessToken } 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 | ||||
| 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) { | if (appInfoLoading) { | ||||
| return ( | return ( | ||||
| <Loading type='app' /> | <Loading type='app' /> | ||||
| ) | ) | ||||
| } | } | ||||
| if (!userCanAccess) | |||||
| return <AppUnavailable code={403} unknownReason='no permission.' /> | |||||
| 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) { | if (appInfoError) { | ||||
| return ( | return ( | 
| 'use client' | |||||
| import { | import { | ||||
| useCallback, | |||||
| useEffect, | useEffect, | ||||
| useState, | useState, | ||||
| } from 'react' | } from 'react' | ||||
| 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 } from '@/app/components/share/utils' | |||||
| import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' | |||||
| import AppUnavailable from '@/app/components/base/app-unavailable' | 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 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 { | ||||
| 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) { | if (appInfoLoading) { | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| ) | ) | ||||
| } | } | ||||
| if (!userCanAccess) | |||||
| return <AppUnavailable code={403} unknownReason='no permission.' /> | |||||
| 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) { | if (appInfoError) { | ||||
| return ( | return ( | ||||
| appInfoError, | appInfoError, | ||||
| appInfoLoading, | appInfoLoading, | ||||
| appData, | appData, | ||||
| accessMode, | |||||
| userCanAccess, | userCanAccess, | ||||
| appParams, | appParams, | ||||
| appMeta, | appMeta, | ||||
| return <EmbeddedChatbotContext.Provider value={{ | return <EmbeddedChatbotContext.Provider value={{ | ||||
| userCanAccess, | userCanAccess, | ||||
| accessMode, | |||||
| appInfoError, | appInfoError, | ||||
| appInfoLoading, | appInfoLoading, | ||||
| appData, | appData, | 
| import { useBoolean } from 'ahooks' | import { useBoolean } from 'ahooks' | ||||
| import { usePathname, useRouter, useSearchParams } from 'next/navigation' | import { usePathname, useRouter, useSearchParams } from 'next/navigation' | ||||
| import TabHeader from '../../base/tab-header' | import TabHeader from '../../base/tab-header' | ||||
| import { checkOrSetAccessToken } from '../utils' | |||||
| 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' | ||||
| </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 || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission))) { | ||||
| 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 <AppUnavailable code={403} unknownReason='no permission.' /> | |||||
| 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( | 
| } | } | ||||
| export const removeAccessToken = () => { | export const removeAccessToken = () => { | ||||
| const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] | |||||
| const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) | |||||
| let accessTokenJson = getInitialTokenV2() | |||||
| try { | |||||
| accessTokenJson = JSON.parse(accessToken) | |||||
| if (isTokenV1(accessTokenJson)) | |||||
| accessTokenJson = getInitialTokenV2() | |||||
| } | |||||
| catch { | |||||
| } | |||||
| localStorage.removeItem(CONVERSATION_ID_INFO) | |||||
| localStorage.removeItem('token') | |||||
| localStorage.removeItem('webapp_access_token') | localStorage.removeItem('webapp_access_token') | ||||
| delete accessTokenJson[sharedToken] | |||||
| localStorage.setItem('token', JSON.stringify(accessTokenJson)) | |||||
| } | } | 
| }) | }) | ||||
| } | } | ||||
| function requiredWebSSOLogin(message?: string) { | |||||
| removeAccessToken() | |||||
| function requiredWebSSOLogin(message?: string, code?: number) { | |||||
| const params = new URLSearchParams() | const params = new URLSearchParams() | ||||
| params.append('redirect_url', globalThis.location.pathname) | params.append('redirect_url', globalThis.location.pathname) | ||||
| if (message) | if (message) | ||||
| params.append('message', message) | params.append('message', message) | ||||
| if (code) | |||||
| params.append('code', String(code)) | |||||
| globalThis.location.href = `/webapp-signin?${params.toString()}` | globalThis.location.href = `/webapp-signin?${params.toString()}` | ||||
| } | } | ||||
| res.json().then((data: any) => { | res.json().then((data: any) => { | ||||
| if (isPublicAPI) { | if (isPublicAPI) { | ||||
| if (data.code === 'web_app_access_denied') | if (data.code === 'web_app_access_denied') | ||||
| requiredWebSSOLogin(data.message) | |||||
| requiredWebSSOLogin(data.message, 403) | |||||
| if (data.code === 'web_sso_auth_required') | |||||
| if (data.code === 'web_sso_auth_required') { | |||||
| removeAccessToken() | |||||
| requiredWebSSOLogin() | requiredWebSSOLogin() | ||||
| } | |||||
| if (data.code === 'unauthorized') { | if (data.code === 'unauthorized') { | ||||
| removeAccessToken() | removeAccessToken() | ||||
| const { code, message } = errRespData | const { code, message } = errRespData | ||||
| // webapp sso | // webapp sso | ||||
| if (code === 'web_app_access_denied') { | if (code === 'web_app_access_denied') { | ||||
| requiredWebSSOLogin(message) | |||||
| requiredWebSSOLogin(message, 403) | |||||
| return Promise.reject(err) | return Promise.reject(err) | ||||
| } | } | ||||
| if (code === 'web_sso_auth_required') { | if (code === 'web_sso_auth_required') { | ||||
| removeAccessToken() | |||||
| requiredWebSSOLogin() | requiredWebSSOLogin() | ||||
| return Promise.reject(err) | return Promise.reject(err) | ||||
| } | } |