wip: Adjust the authentication process to improve user experience

pull/22524/head
NFish 7 months ago
parent 3bead19f19
commit e4b6c33bdd

@ -1,25 +1,81 @@
'use client' 'use client'
import AppUnavailable from '@/app/components/base/app-unavailable'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { removeAccessToken } from '@/app/components/share/utils'
import { useWebAppStore } from '@/context/web-app-context' import { useWebAppStore } from '@/context/web-app-context'
import React, { useEffect, useState } from 'react' 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 AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
const { t } = useTranslation()
const shareCode = useWebAppStore(s => s.shareCode) const shareCode = useWebAppStore(s => s.shareCode)
const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode)
const [isLoading, setIsLoading] = useState(true) 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 { isFetching: isFetchingUserCanAccessApp, data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp: false })
useEffect(() => { useEffect(() => {
(async () => { if (appInfo)
try { updateAppInfo(appInfo)
setIsLoading(true) if (appParams)
} updateAppParams(appParams)
catch (error) { console.error(error) } if (appMeta)
finally { updateWebAppMeta(appMeta)
setIsLoading(false) updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result))
} }, [appInfo, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp])
})()
}, [webAppAccessMode, shareCode]) const router = useRouter()
if (isLoading) { 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 || isFetchingUserCanAccessApp) {
return <div className='flex h-full items-center justify-center'> return <div className='flex h-full items-center justify-center'>
<Loading /> <Loading />
</div> </div>

@ -49,6 +49,16 @@ export type ChatConfig = Omit<ModelConfig, 'model'> & {
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 = {

@ -7,16 +7,14 @@ import {
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 { 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,
@ -39,10 +37,10 @@ import { Resolution, TransferMethod } from '@/types/app'
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 {
@ -83,9 +81,6 @@ const TextGeneration: FC<IMainProps> = ({
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'
@ -103,23 +98,12 @@ const TextGeneration: FC<IMainProps> = ({
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 fetchSavedMessage = useCallback(async () => {
const res: any = await doFetchSavedMessage(isInstalledApp, installedAppInfo?.id) const res: any = await doFetchSavedMessage(isInstalledApp, installedAppInfo?.id)
setSavedMessages(res.data) setSavedMessages(res.data)
} }, [isInstalledApp, installedAppInfo?.id])
const handleSaveMessage = async (messageId: string) => { const handleSaveMessage = async (messageId: string) => {
await saveMessage(messageId, isInstalledApp, installedAppInfo?.id) await saveMessage(messageId, isInstalledApp, installedAppInfo?.id)
notify({ type: 'success', message: t('common.api.saved') }) notify({ type: 'success', message: t('common.api.saved') })
@ -375,34 +359,14 @@ const TextGeneration: FC<IMainProps> = ({
} }
} }
const fetchInitData = async () => { const appData = useWebAppStore(s => s.appInfo)
// if (!isInstalledApp) const appParams = useWebAppStore(s => s.appParams)
// await checkOrSetAccessToken() const accessMode = useWebAppStore(s => s.webAppAccessMode)
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()
: {},
])
}
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)
@ -413,11 +377,11 @@ const TextGeneration: FC<IMainProps> = ({
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
@ -426,7 +390,7 @@ const TextGeneration: FC<IMainProps> = ({
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'))
@ -528,32 +492,12 @@ const TextGeneration: FC<IMainProps> = ({
</div> </div>
) )
const getSigninUrl = useCallback(() => { if (!appId || !siteInfo || !promptConfig) {
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))) {
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',
@ -578,7 +522,7 @@ const TextGeneration: FC<IMainProps> = ({
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>

@ -1,9 +1,10 @@
'use client' 'use client'
import type { ChatConfig } from '@/app/components/base/chat/types'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { AccessMode } from '@/models/access-control' import { AccessMode } from '@/models/access-control'
import { useAppAccessModeByCode } from '@/service/use-share' import type { AppData, AppMeta } from '@/models/share'
import type { App } from '@/types/app' import { useGetWebAppAccessModeByCode } from '@/service/use-share'
import { usePathname, useSearchParams } from 'next/navigation' import { usePathname, useSearchParams } from 'next/navigation'
import type { FC, PropsWithChildren } from 'react' import type { FC, PropsWithChildren } from 'react'
import { useEffect } from 'react' import { useEffect } from 'react'
@ -13,19 +14,31 @@ import { create } from 'zustand'
type WebAppStore = { type WebAppStore = {
shareCode: string | null shareCode: string | null
updateShareCode: (shareCode: string | null) => void updateShareCode: (shareCode: string | null) => void
appInfo: App | null appInfo: AppData | null
updateAppInfo: (appInfo: App | null) => void updateAppInfo: (appInfo: AppData | null) => void
appParams: ChatConfig | null
updateAppParams: (appParams: ChatConfig | null) => void
webAppAccessMode: AccessMode webAppAccessMode: AccessMode
updateWebAppAccessMode: (accessMode: AccessMode) => void updateWebAppAccessMode: (accessMode: AccessMode) => void
appMeta: AppMeta | null
updateWebAppMeta: (appMeta: AppMeta | null) => void
userCanAccessApp: boolean
updateUserCanAccessApp: (canAccess: boolean) => void
} }
export const useWebAppStore = create<WebAppStore>(set => ({ export const useWebAppStore = create<WebAppStore>(set => ({
shareCode: null, shareCode: null,
updateShareCode: (shareCode: string | null) => set(() => ({ shareCode })), updateShareCode: (shareCode: string | null) => set(() => ({ shareCode })),
appInfo: null, appInfo: null,
updateAppInfo: (appInfo: App | null) => set(() => ({ appInfo })), updateAppInfo: (appInfo: AppData | null) => set(() => ({ appInfo })),
appParams: null,
updateAppParams: (appParams: ChatConfig | null) => set(() => ({ appParams })),
webAppAccessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, webAppAccessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
updateWebAppAccessMode: (accessMode: AccessMode) => set(() => ({ webAppAccessMode: accessMode })), 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 => { const getShareCodeFromRedirectUrl = (redirectUrl: string | null): string | null => {
@ -55,7 +68,7 @@ const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
setShareCode(newShareCode) setShareCode(newShareCode)
updateShareCode(newShareCode) updateShareCode(newShareCode)
}, [pathname, redirectUrlParam, updateShareCode]) }, [pathname, redirectUrlParam, updateShareCode])
const { isFetching, data: accessModeResult } = useAppAccessModeByCode(shareCode) const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode)
useEffect(() => { useEffect(() => {
if (accessModeResult?.accessMode) if (accessModeResult?.accessMode)
updateWebAppAccessMode(accessModeResult.accessMode) updateWebAppAccessMode(accessModeResult.accessMode)

@ -35,7 +35,7 @@ export type AppMeta = {
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>
enable_site?: boolean enable_site?: boolean
end_user_id?: string end_user_id?: string
site: SiteInfo site: SiteInfo

@ -3,6 +3,7 @@ import { get, post } from './base'
import { getAppAccessMode, getUserCanAccess } from './share' import { getAppAccessMode, 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'
@ -79,15 +80,18 @@ export const useGetAppAccessMode = ({ appId, isInstalledApp = true, enabled }: {
}) })
} }
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), queryFn: () => {
enabled: !!appId && enabled, 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,
},
}) })
} }

@ -1,11 +1,11 @@
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control' 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) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
return useQuery({ return useQuery({
queryKey: [NAME_SPACE, 'appAccessMode', code], queryKey: [NAME_SPACE, 'appAccessMode', code],
@ -23,3 +23,30 @@ export const useAppAccessModeByCode = (code: string | null) => {
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