fix: web app support logout

This commit is contained in:
NFish 2025-05-29 17:27:05 +08:00
parent 35fea50077
commit ce635d7e24
11 changed files with 77 additions and 52 deletions

View File

@ -1,14 +1,42 @@
import React from 'react'
'use client'
import React, { useEffect, useState } from 'react'
import type { FC } from 'react'
import type { Metadata } from 'next'
export const metadata: Metadata = {
icons: 'data:,', // prevent browser from using default favicon
}
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'
const Layout: FC<{
children: React.ReactNode
}> = ({ children }) => {
const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending)
const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode)
const pathname = usePathname()
const searchParams = useSearchParams()
const redirectUrl = searchParams.get('redirect_url')
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
(async () => {
let appCode: string | null = null
if (redirectUrl)
appCode = redirectUrl?.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])
if (isLoading || isGlobalPending) {
return <div className='flex h-full w-full items-center justify-center'>
<Loading />
</div>
}
return (
<div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]">
{children}

View File

@ -9,14 +9,13 @@ 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 { useAppAccessModeByCode } from '@/service/use-share'
import { AccessMode } from '@/models/access-control'
import ExternalMemberSsoAuth from './components/external-member-sso-auth'
const WebSSOForm: FC = () => {
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const isGettingSystemFeatures = useGlobalPublicStore(s => s.isPending)
const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode)
const searchParams = useSearchParams()
const router = useRouter()
@ -39,8 +38,6 @@ const WebSSOForm: FC = () => {
return appCode
}, [redirectUrl])
const { isLoading, data } = useAppAccessModeByCode(getAppCodeFromRedirectUrl())
useEffect(() => {
(async () => {
const appCode = getAppCodeFromRedirectUrl()
@ -53,11 +50,11 @@ const WebSSOForm: FC = () => {
}, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl])
useEffect(() => {
if (data && data.accessMode === AccessMode.PUBLIC && redirectUrl)
if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl)
router.replace(redirectUrl)
}, [data, router, redirectUrl])
}, [webAppAccessMode, router, redirectUrl])
if (isGettingSystemFeatures || isLoading || tokenFromUrl) {
if (tokenFromUrl) {
return <div className='flex h-full items-center justify-center'>
<Loading />
</div>
@ -74,7 +71,7 @@ const WebSSOForm: FC = () => {
<AppUnavailable code={'App Unavailable'} unknownReason='redirect url is invalid.' />
</div>
}
if (data && data.accessMode === AccessMode.PUBLIC) {
if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) {
return <div className='flex h-full items-center justify-center'>
<Loading />
</div>
@ -84,13 +81,13 @@ const WebSSOForm: FC = () => {
<p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p>
</div>
}
if (data && (data.accessMode === AccessMode.ORGANIZATION || data.accessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS)) {
if (webAppAccessMode && (webAppAccessMode === AccessMode.ORGANIZATION || webAppAccessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS)) {
return <div className='w-[400px]'>
<NormalForm />
</div>
}
if (data && data.accessMode === AccessMode.EXTERNAL_MEMBERS)
if (webAppAccessMode && webAppAccessMode === AccessMode.EXTERNAL_MEMBERS)
return <ExternalMemberSsoAuth />
return <div className='flex h-full items-center justify-center'>

View File

@ -16,14 +16,12 @@ import type {
ConversationItem,
} from '@/models/share'
import { noop } from 'lodash-es'
import { AccessMode } from '@/models/access-control'
export type ChatWithHistoryContextValue = {
appInfoError?: any
appInfoLoading?: boolean
appMeta?: AppMeta
appData?: AppData
accessMode?: AccessMode
userCanAccess?: boolean
appParams?: ChatConfig
appChatListDataLoading?: boolean
@ -64,7 +62,6 @@ export type ChatWithHistoryContextValue = {
}
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
userCanAccess: false,
currentConversationId: '',
appPrevChatTree: [],

View File

@ -43,9 +43,8 @@ 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 { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
@ -77,11 +76,6 @@ 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: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({
appId: installedAppInfo?.app.id || appInfo?.app_id,
isInstalledApp,
enabled: systemFeatures.webapp_auth.enabled,
})
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({
appId: installedAppInfo?.app.id || appInfo?.app_id,
isInstalledApp,
@ -469,8 +463,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
return {
appInfoError,
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)),
accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC,
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission),
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
isInstalledApp,
appId,

View File

@ -15,10 +15,8 @@ import type {
ConversationItem,
} from '@/models/share'
import { noop } from 'lodash-es'
import { AccessMode } from '@/models/access-control'
export type EmbeddedChatbotContextValue = {
accessMode?: AccessMode
userCanAccess?: boolean
appInfoError?: any
appInfoLoading?: boolean
@ -58,7 +56,6 @@ export type EmbeddedChatbotContextValue = {
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
userCanAccess: false,
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
currentConversationId: '',
appPrevChatList: [],
pinnedConversationList: [],

View File

@ -36,9 +36,8 @@ import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { noop } from 'lodash-es'
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
@ -70,11 +69,6 @@ export const useEmbeddedChatbot = () => {
const isInstalledApp = false
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo)
const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({
appId: appInfo?.app_id,
isInstalledApp,
enabled: systemFeatures.webapp_auth.enabled,
})
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({
appId: appInfo?.app_id,
isInstalledApp,
@ -385,8 +379,7 @@ export const useEmbeddedChatbot = () => {
return {
appInfoError,
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)),
accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC,
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission),
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
isInstalledApp,
allowResetChat,

View File

@ -6,7 +6,7 @@ import type { Placement } from '@floating-ui/react'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import { useRouter } from 'next/navigation'
import { usePathname, useRouter } from 'next/navigation'
import Divider from '../../base/divider'
import { removeAccessToken } from '../utils'
import InfoModal from './info-modal'
@ -19,6 +19,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'
type Props = {
data?: SiteInfo
@ -31,7 +33,9 @@ const MenuDropdown: FC<Props> = ({
placement,
hideLogout,
}) => {
const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode)
const router = useRouter()
const pathname = usePathname()
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const openRef = useRef(open)
@ -46,8 +50,8 @@ const MenuDropdown: FC<Props> = ({
const handleLogout = useCallback(() => {
removeAccessToken()
router.replace(`/webapp-signin?redirect_url=${window.location.href}`)
}, [router])
router.replace(`/webapp-signin?redirect_url=${pathname}`)
}, [router, pathname])
const [show, setShow] = useState(false)
@ -92,6 +96,16 @@ const MenuDropdown: FC<Props> = ({
className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
>{t('common.userProfile.about')}</div>
</div>
{!(hideLogout || webAppAccessMode === AccessMode.EXTERNAL_MEMBERS || webAppAccessMode === AccessMode.PUBLIC) && (
<div className='p-1'>
<div
onClick={handleLogout}
className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
>
{t('common.userProfile.logout')}
</div>
</div>
)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@ -70,6 +70,7 @@ export const removeAccessToken = () => {
}
localStorage.removeItem(CONVERSATION_ID_INFO)
localStorage.removeItem('webAppAccessToken')
delete accessTokenJson[sharedToken]
localStorage.setItem('token', JSON.stringify(accessTokenJson))

View File

@ -7,19 +7,24 @@ 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 = {
isPending: boolean
setIsPending: (isPending: boolean) => void
isGlobalPending: boolean
setIsGlobalPending: (isPending: boolean) => void
systemFeatures: SystemFeatures
setSystemFeatures: (systemFeatures: SystemFeatures) => void
webAppAccessMode: AccessMode,
setWebAppAccessMode: (webAppAccessMode: AccessMode) => void
}
export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({
isPending: true,
setIsPending: (isPending: boolean) => set(() => ({ isPending })),
isGlobalPending: true,
setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })),
systemFeatures: defaultSystemFeatures,
setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
webAppAccessMode: AccessMode.PUBLIC,
setWebAppAccessMode: (webAppAccessMode: AccessMode) => set(() => ({ webAppAccessMode })),
}))
const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
@ -29,7 +34,7 @@ const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
queryKey: ['systemFeatures'],
queryFn: getSystemFeatures,
})
const { setSystemFeatures, setIsPending } = useGlobalPublicStore()
const { setSystemFeatures, setIsGlobalPending: setIsPending } = useGlobalPublicStore()
useEffect(() => {
if (data)
setSystemFeatures({ ...defaultSystemFeatures, ...data })

View File

@ -11,7 +11,7 @@ describe('title should be empty if systemFeatures is pending', () => {
act(() => {
useGlobalPublicStore.setState({
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
isPending: true,
isGlobalPending: true,
})
})
it('document title should be empty if set title', () => {
@ -28,7 +28,7 @@ describe('use default branding', () => {
beforeEach(() => {
act(() => {
useGlobalPublicStore.setState({
isPending: false,
isGlobalPending: false,
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
})
})
@ -48,7 +48,7 @@ describe('use specific branding', () => {
beforeEach(() => {
act(() => {
useGlobalPublicStore.setState({
isPending: false,
isGlobalPending: false,
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } },
})
})

View File

@ -3,7 +3,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
import { useFavicon, useTitle } from 'ahooks'
export default function useDocumentTitle(title: string) {
const isPending = useGlobalPublicStore(s => s.isPending)
const isPending = useGlobalPublicStore(s => s.isGlobalPending)
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const prefix = title ? `${title} - ` : ''
let titleStr = ''