Merge remote-tracking branch 'origin/main' into feat/collaboration

This commit is contained in:
lyzno1
2025-10-20 10:03:57 +08:00
313 changed files with 8233 additions and 4914 deletions

View File

@ -2,16 +2,17 @@
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 { webAppLogout } from '@/service/webapp-auth'
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 shareCode = useWebAppStore(s => s.shareCode)
const updateAppInfo = useWebAppStore(s => s.updateAppInfo)
const updateAppParams = useWebAppStore(s => s.updateAppParams)
const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta)
@ -41,11 +42,11 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
return `/webapp-signin?${params.toString()}`
}, [searchParams, pathname])
const backToHome = useCallback(() => {
removeAccessToken()
const backToHome = useCallback(async () => {
await webAppLogout(shareCode!)
const url = getSigninUrl()
router.replace(url)
}, [getSigninUrl, router])
}, [getSigninUrl, router, webAppLogout, shareCode])
if (appInfoError) {
return <div className='flex h-full items-center justify-center'>

View File

@ -1,15 +1,16 @@
'use client'
import type { FC, PropsWithChildren } from 'react'
import { useEffect } from 'react'
import { useEffect, useState } 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 { AccessMode } from '@/models/access-control'
import { webAppLoginStatus, webAppLogout } from '@/service/webapp-auth'
import { fetchAccessToken } from '@/service/share'
import Loading from '@/app/components/base/loading'
import { AccessMode } from '@/models/access-control'
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
const Splash: FC<PropsWithChildren> = ({ children }) => {
const { t } = useTranslation()
@ -18,9 +19,9 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
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 tokenFromUrl = searchParams.get('web_sso_token')
const getSigninUrl = useCallback(() => {
const params = new URLSearchParams(searchParams)
params.delete('message')
@ -28,35 +29,66 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
return `/webapp-signin?${params.toString()}`
}, [searchParams])
const backToHome = useCallback(() => {
removeAccessToken()
const backToHome = useCallback(async () => {
await webAppLogout(shareCode!)
const url = getSigninUrl()
router.replace(url)
}, [getSigninUrl, router])
}, [getSigninUrl, router, webAppLogout, shareCode])
const needCheckIsLogin = webAppAccessMode !== AccessMode.PUBLIC
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
if (message) {
setIsLoading(false)
return
}
if(tokenFromUrl)
setWebAppAccessToken(tokenFromUrl)
const redirectOrFinish = () => {
if (redirectUrl)
router.replace(decodeURIComponent(redirectUrl))
else
setIsLoading(false)
}
const proceedToAuth = () => {
setIsLoading(false)
}
(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
const { userLoggedIn, appLoggedIn } = await webAppLoginStatus(needCheckIsLogin, shareCode!)
if (userLoggedIn && appLoggedIn) {
redirectOrFinish()
}
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
else if (!userLoggedIn && !appLoggedIn) {
proceedToAuth()
}
if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) {
await checkOrSetAccessToken(shareCode)
router.replace(decodeURIComponent(redirectUrl))
else if (!userLoggedIn && appLoggedIn) {
redirectOrFinish()
}
else if (userLoggedIn && !appLoggedIn) {
try {
const { access_token } = await fetchAccessToken({ appCode: shareCode! })
setWebAppPassport(shareCode!, access_token)
redirectOrFinish()
}
catch (error) {
await webAppLogout(shareCode!)
proceedToAuth()
}
}
})()
}, [shareCode, redirectUrl, router, tokenFromUrl, message, webAppAccessMode])
}, [
shareCode,
redirectUrl,
router,
message,
webAppAccessMode,
needCheckIsLogin,
tokenFromUrl])
if (message) {
return <div className='flex h-full flex-col items-center justify-center gap-y-4'>
@ -64,12 +96,8 @@ const Splash: FC<PropsWithChildren> = ({ children }) => {
<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) {
if (isLoading) {
return <div className='flex h-full items-center justify-center'>
<Loading />
</div>

View File

@ -10,7 +10,7 @@ import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
import I18NContext from '@/context/i18n'
import { setAccessToken } from '@/app/components/share/utils'
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
import { fetchAccessToken } from '@/service/share'
export default function CheckCode() {
@ -62,9 +62,9 @@ export default function CheckCode() {
setIsLoading(true)
const ret = await webAppEmailLoginWithCode({ email, code, token })
if (ret.result === 'success') {
localStorage.setItem('webapp_access_token', ret.data.access_token)
const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: ret.data.access_token })
await setAccessToken(appCode, tokenResp.access_token)
setWebAppAccessToken(ret.data.access_token)
const { access_token } = await fetchAccessToken({ appCode: appCode! })
setWebAppPassport(appCode!, access_token)
router.replace(decodeURIComponent(redirectUrl))
}
}

View File

@ -11,15 +11,13 @@ import { webAppLogin } from '@/service/common'
import Input from '@/app/components/base/input'
import I18NContext from '@/context/i18n'
import { noop } from 'lodash-es'
import { setAccessToken } from '@/app/components/share/utils'
import { fetchAccessToken } from '@/service/share'
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
type MailAndPasswordAuthProps = {
isEmailSetup: boolean
}
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAuthProps) {
const { t } = useTranslation()
const { locale } = useContext(I18NContext)
@ -43,8 +41,8 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
return appCode
}, [redirectUrl])
const appCode = getAppCodeFromRedirectUrl()
const handleEmailPasswordLogin = async () => {
const appCode = getAppCodeFromRedirectUrl()
if (!email) {
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
return
@ -60,13 +58,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
Toast.notify({ type: 'error', message: t('login.error.passwordEmpty') })
return
}
if (!passwordRegex.test(password)) {
Toast.notify({
type: 'error',
message: t('login.error.passwordInvalid'),
})
return
}
if (!redirectUrl || !appCode) {
Toast.notify({
type: 'error',
@ -88,9 +80,10 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
body: loginData,
})
if (res.result === 'success') {
localStorage.setItem('webapp_access_token', res.data.access_token)
const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: res.data.access_token })
await setAccessToken(appCode, tokenResp.access_token)
setWebAppAccessToken(res.data.access_token)
const { access_token } = await fetchAccessToken({ appCode: appCode! })
setWebAppPassport(appCode!, access_token)
router.replace(decodeURIComponent(redirectUrl))
}
else {
@ -141,9 +134,9 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
</label>
<div className="relative mt-1">
<Input
id="password"
value={password}
onChange={e => setPassword(e.target.value)}
id="password"
onKeyDown={(e) => {
if (e.key === 'Enter')
handleEmailPasswordLogin()

View File

@ -3,13 +3,13 @@ import { useRouter, useSearchParams } from 'next/navigation'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { removeAccessToken } from '@/app/components/share/utils'
import { useGlobalPublicStore } from '@/context/global-public-context'
import AppUnavailable from '@/app/components/base/app-unavailable'
import NormalForm from './normalForm'
import { AccessMode } from '@/models/access-control'
import ExternalMemberSsoAuth from './components/external-member-sso-auth'
import { useWebAppStore } from '@/context/web-app-context'
import { webAppLogout } from '@/service/webapp-auth'
const WebSSOForm: FC = () => {
const { t } = useTranslation()
@ -26,11 +26,12 @@ const WebSSOForm: FC = () => {
return `/webapp-signin?${params.toString()}`
}, [redirectUrl])
const backToHome = useCallback(() => {
removeAccessToken()
const shareCode = useWebAppStore(s => s.shareCode)
const backToHome = useCallback(async () => {
await webAppLogout(shareCode!)
const url = getSigninUrl()
router.replace(url)
}, [getSigninUrl, router])
}, [getSigninUrl, router, webAppLogout, shareCode])
if (!redirectUrl) {
return <div className='flex h-full items-center justify-center'>

View File

@ -9,7 +9,6 @@ import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import {
checkEmailExisted,
logout,
resetEmail,
sendVerifyCode,
verifyEmail,
@ -17,6 +16,7 @@ import {
import { noop } from 'lodash-es'
import { asyncRunSafe } from '@/utils'
import type { ResponseError } from '@/service/fetch'
import { useLogout } from '@/service/use-common'
type Props = {
show: boolean
@ -167,15 +167,12 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
setStep(STEP.verifyNew)
}
const { mutateAsync: logout } = useLogout()
const handleLogout = async () => {
await logout({
url: '/logout',
params: {},
})
await logout()
localStorage.removeItem('setup_status')
localStorage.removeItem('console_token')
localStorage.removeItem('refresh_token')
// Tokens are now stored in cookies and cleared by backend
router.push('/signin')
}

View File

@ -7,11 +7,11 @@ import {
} from '@remixicon/react'
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
import Avatar from '@/app/components/base/avatar'
import { logout } from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
import PremiumBadge from '@/app/components/base/premium-badge'
import { useLogout } from '@/service/use-common'
export type IAppSelector = {
isMobile: boolean
@ -23,15 +23,12 @@ export default function AppSelector() {
const { userProfile } = useAppContext()
const { isEducationAccount } = useProviderContext()
const { mutateAsync: logout } = useLogout()
const handleLogout = async () => {
await logout({
url: '/logout',
params: {},
})
await logout()
localStorage.removeItem('setup_status')
localStorage.removeItem('console_token')
localStorage.removeItem('refresh_token')
// Tokens are now stored in cookies and cleared by backend
router.push('/signin')
}

View File

@ -8,7 +8,7 @@ import Button from '@/app/components/base/button'
import CustomDialog from '@/app/components/base/dialog'
import Textarea from '@/app/components/base/textarea'
import Toast from '@/app/components/base/toast'
import { logout } from '@/service/common'
import { useLogout } from '@/service/use-common'
type DeleteAccountProps = {
onCancel: () => void
@ -22,14 +22,11 @@ export default function FeedBack(props: DeleteAccountProps) {
const [userFeedback, setUserFeedback] = useState('')
const { isPending, mutateAsync: sendFeedback } = useDeleteAccountFeedback()
const { mutateAsync: logout } = useLogout()
const handleSuccess = useCallback(async () => {
try {
await logout({
url: '/logout',
params: {},
})
localStorage.removeItem('refresh_token')
localStorage.removeItem('console_token')
await logout()
// Tokens are now stored in cookies and cleared by backend
router.push('/signin')
Toast.notify({ type: 'info', message: t('common.account.deleteSuccessTip') })
}

View File

@ -5,17 +5,22 @@ import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { AppContextProvider } from '@/context/app-context'
import { useMemo } from 'react'
import { useIsLogin } from '@/service/use-common'
import Loading from '@/app/components/base/loading'
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
useDocumentTitle('')
const isLoggedIn = useMemo(() => {
try {
return Boolean(localStorage.getItem('console_token') && localStorage.getItem('refresh_token'))
}
catch { return false }
}, [])
const { isLoading, data: loginData } = useIsLogin()
const isLoggedIn = loginData?.logged_in
if(isLoading) {
return (
<div className='flex min-h-screen w-full justify-center bg-background-default-burn'>
<Loading />
</div>
)
}
return <>
<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 items-center rounded-2xl border border-effects-highlight bg-background-default-subtle')}>

View File

@ -1,6 +1,6 @@
'use client'
import React, { useEffect, useMemo, useRef } from 'react'
import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useRouter, useSearchParams } from 'next/navigation'
import Button from '@/app/components/base/button'
@ -18,6 +18,7 @@ import {
RiTranslate2,
} from '@remixicon/react'
import dayjs from 'dayjs'
import { useIsLogin } from '@/service/use-common'
export const OAUTH_AUTHORIZE_PENDING_KEY = 'oauth_authorize_pending'
export const REDIRECT_URL_KEY = 'oauth_redirect_url'
@ -74,17 +75,13 @@ export default function OAuthAuthorize() {
const client_id = decodeURIComponent(searchParams.get('client_id') || '')
const redirect_uri = decodeURIComponent(searchParams.get('redirect_uri') || '')
const { userProfile } = useAppContext()
const { data: authAppInfo, isLoading, isError } = useOAuthAppInfo(client_id, redirect_uri)
const { data: authAppInfo, isLoading: isOAuthLoading, isError } = useOAuthAppInfo(client_id, redirect_uri)
const { mutateAsync: authorize, isPending: authorizing } = useAuthorizeOAuthApp()
const hasNotifiedRef = useRef(false)
const isLoggedIn = useMemo(() => {
try {
return Boolean(localStorage.getItem('console_token') && localStorage.getItem('refresh_token'))
}
catch { return false }
}, [])
const { isLoading: isIsLoginLoading, data: loginData } = useIsLogin()
const isLoggedIn = loginData?.logged_in
const isLoading = isOAuthLoading || isIsLoginLoading
const onLoginSwitchClick = () => {
try {
const returnUrl = buildReturnUrl('/account/oauth/authorize', `?client_id=${encodeURIComponent(client_id)}&redirect_uri=${encodeURIComponent(redirect_uri)}`)

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiDeleteBinLine, RiEditFill, RiEditLine } from '@remixicon/react'
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
@ -16,7 +16,7 @@ type Props = {
type: EditItemType
content: string
readonly?: boolean
onSave: (content: string) => void
onSave: (content: string) => Promise<void>
}
export const EditTitle: FC<{ className?: string; title: string }> = ({ className, title }) => (
@ -46,8 +46,13 @@ const EditItem: FC<Props> = ({
const placeholder = type === EditItemType.Query ? t('appAnnotation.editModal.queryPlaceholder') : t('appAnnotation.editModal.answerPlaceholder')
const [isEdit, setIsEdit] = useState(false)
const handleSave = () => {
onSave(newContent)
// Reset newContent when content prop changes
useEffect(() => {
setNewContent('')
}, [content])
const handleSave = async () => {
await onSave(newContent)
setIsEdit(false)
}

View File

@ -21,7 +21,7 @@ type Props = {
isShow: boolean
onHide: () => void
item: AnnotationItem
onSave: (editedQuery: string, editedAnswer: string) => void
onSave: (editedQuery: string, editedAnswer: string) => Promise<void>
onRemove: () => void
}
@ -46,6 +46,16 @@ const ViewAnnotationModal: FC<Props> = ({
const [currPage, setCurrPage] = React.useState<number>(0)
const [total, setTotal] = useState(0)
const [hitHistoryList, setHitHistoryList] = useState<HitHistoryItem[]>([])
// Update local state when item prop changes (e.g., when modal is reopened with updated data)
useEffect(() => {
setNewQuery(question)
setNewAnswer(answer)
setCurrPage(0)
setTotal(0)
setHitHistoryList([])
}, [question, answer, id])
const fetchHitHistory = async (page = 1) => {
try {
const { data, total }: any = await fetchHitHistoryList(appId, id, {
@ -63,6 +73,12 @@ const ViewAnnotationModal: FC<Props> = ({
fetchHitHistory(currPage + 1)
}, [currPage])
// Fetch hit history when item changes
useEffect(() => {
if (isShow && id)
fetchHitHistory(1)
}, [id, isShow])
const tabs = [
{ value: TabType.annotation, text: t('appAnnotation.viewModal.annotatedResponse') },
{
@ -82,14 +98,20 @@ const ViewAnnotationModal: FC<Props> = ({
},
]
const [activeTab, setActiveTab] = useState(TabType.annotation)
const handleSave = (type: EditItemType, editedContent: string) => {
if (type === EditItemType.Query) {
setNewQuery(editedContent)
onSave(editedContent, newAnswer)
const handleSave = async (type: EditItemType, editedContent: string) => {
try {
if (type === EditItemType.Query) {
await onSave(editedContent, newAnswer)
setNewQuery(editedContent)
}
else {
await onSave(newQuestion, editedContent)
setNewAnswer(editedContent)
}
}
else {
setNewAnswer(editedContent)
onSave(newQuestion, editedContent)
catch (error) {
// If save fails, don't update local state
console.error('Failed to save annotation:', error)
}
}
const [showModal, setShowModal] = useState(false)

View File

@ -22,7 +22,7 @@ const AccessControlDialog = ({
}, [onClose])
return (
<Transition appear show={show} as={Fragment}>
<Dialog as="div" open={true} className="relative z-20" onClose={() => null}>
<Dialog as="div" open={true} className="relative z-[99]" onClose={() => null}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@ -32,7 +32,7 @@ const AccessControlDialog = ({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="bg-background-overlay/25 fixed inset-0" />
<div className="fixed inset-0 bg-background-overlay" />
</Transition.Child>
<div className="fixed inset-0 flex items-center justify-center">

View File

@ -52,7 +52,7 @@ export default function AddMemberOrGroupDialog() {
</Button>
</PortalToFollowElemTrigger>
{open && <FloatingOverlay />}
<PortalToFollowElemContent className='z-[25]'>
<PortalToFollowElemContent className='z-[100]'>
<div className='relative flex max-h-[400px] w-[400px] flex-col overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]'>
<div className='sticky top-0 z-10 bg-components-panel-bg-blur p-2 pb-0.5 backdrop-blur-[5px]'>
<Input value={keyword} onChange={handleKeywordChange} showLeftIcon placeholder={t('app.accessControlDialog.operateGroupAndMember.searchPlaceholder') as string} />

View File

@ -4,7 +4,6 @@ import {
useEffect,
useState,
} from 'react'
import { useAsyncEffect } from 'ahooks'
import { useThemeContext } from '../embedded-chatbot/theme/theme-context'
import {
ChatWithHistoryContext,
@ -18,8 +17,6 @@ import ChatWrapper from './chat-wrapper'
import type { InstalledApp } from '@/models/explore'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { checkOrSetAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable'
import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
@ -201,36 +198,6 @@ const ChatWithHistoryWrapWithCheckToken: FC<ChatWithHistoryWrapProps> = ({
installedAppInfo,
className,
}) => {
const [initialized, setInitialized] = useState(false)
const [appUnavailable, setAppUnavailable] = useState<boolean>(false)
const [isUnknownReason, setIsUnknownReason] = useState<boolean>(false)
useAsyncEffect(async () => {
if (!initialized) {
if (!installedAppInfo) {
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 (
<ChatWithHistoryWrap
installedAppInfo={installedAppInfo}

View File

@ -25,7 +25,6 @@ import Compliance from './compliance'
import PremiumBadge from '@/app/components/base/premium-badge'
import Avatar from '@/app/components/base/avatar'
import ThemeSwitcher from '@/app/components/base/theme-switcher'
import { logout } from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
@ -33,6 +32,7 @@ import { IS_CLOUD_EDITION } from '@/config'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useDocLink } from '@/context/i18n'
import { useLogout } from '@/service/use-common'
export default function AppSelector() {
const itemClassName = `
@ -49,15 +49,12 @@ export default function AppSelector() {
const { isEducationAccount } = useProviderContext()
const { setShowAccountSettingModal } = useModalContext()
const { mutateAsync: logout } = useLogout()
const handleLogout = async () => {
await logout({
url: '/logout',
params: {},
})
await logout()
localStorage.removeItem('setup_status')
localStorage.removeItem('console_token')
localStorage.removeItem('refresh_token')
// Tokens are now stored in cookies and cleared by backend
// To avoid use other account's education notice info
localStorage.removeItem('education-reverify-prev-expire-at')

View File

@ -77,7 +77,7 @@ export const useNodesSyncDraft = () => {
if (postParams) {
navigator.sendBeacon(
`${API_PREFIX}${postParams.url}?_token=${localStorage.getItem('console_token')}`,
`${API_PREFIX}${postParams.url}`,
JSON.stringify(postParams.params),
)
}

View File

@ -20,6 +20,7 @@ import type { SiteInfo } from '@/models/share'
import cn from '@/utils/classnames'
import { AccessMode } from '@/models/access-control'
import { useWebAppStore } from '@/context/web-app-context'
import { webAppLogout } from '@/service/webapp-auth'
type Props = {
data?: SiteInfo
@ -49,11 +50,11 @@ const MenuDropdown: FC<Props> = ({
setOpen(!openRef.current)
}, [setOpen])
const handleLogout = useCallback(() => {
localStorage.removeItem('token')
localStorage.removeItem('webapp_access_token')
const shareCode = useWebAppStore(s => s.shareCode)
const handleLogout = useCallback(async () => {
await webAppLogout(shareCode!)
router.replace(`/webapp-signin?redirect_url=${pathname}`)
}, [router, pathname])
}, [router, pathname, webAppLogout, shareCode])
const [show, setShow] = useState(false)

View File

@ -1,7 +1,3 @@
import { CONVERSATION_ID_INFO } from '../base/chat/constants'
import { fetchAccessToken } from '@/service/share'
import { getProcessedSystemVariablesFromUrlParams } from '../base/chat/utils'
export const isTokenV1 = (token: Record<string, any>) => {
return !token.version
}
@ -9,55 +5,3 @@ export const isTokenV1 = (token: Record<string, any>) => {
export const getInitialTokenV2 = (): Record<string, any> => ({
version: 2,
})
export const checkOrSetAccessToken = async (appCode?: string | null) => {
const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0]
const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id
const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2())
let accessTokenJson = getInitialTokenV2()
try {
accessTokenJson = JSON.parse(accessToken)
if (isTokenV1(accessTokenJson))
accessTokenJson = getInitialTokenV2()
}
catch {
}
if (!accessTokenJson[sharedToken]?.[userId || 'DEFAULT']) {
const webAppAccessToken = localStorage.getItem('webapp_access_token')
const res = await fetchAccessToken({ appCode: sharedToken, userId, webAppAccessToken })
accessTokenJson[sharedToken] = {
...accessTokenJson[sharedToken],
[userId || 'DEFAULT']: res.access_token,
}
localStorage.setItem('token', JSON.stringify(accessTokenJson))
localStorage.removeItem(CONVERSATION_ID_INFO)
}
}
export const setAccessToken = (sharedToken: string, token: string, user_id?: string) => {
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)
accessTokenJson[sharedToken] = {
...accessTokenJson[sharedToken],
[user_id || 'DEFAULT']: token,
}
localStorage.setItem('token', JSON.stringify(accessTokenJson))
}
export const removeAccessToken = () => {
localStorage.removeItem('token')
localStorage.removeItem('webapp_access_token')
}

View File

@ -19,10 +19,7 @@ const SwrInitializer = ({
}: SwrInitializerProps) => {
const router = useRouter()
const searchParams = useSearchParams()
const consoleToken = decodeURIComponent(searchParams.get('access_token') || '')
const refreshToken = decodeURIComponent(searchParams.get('refresh_token') || '')
const consoleTokenFromLocalStorage = localStorage?.getItem('console_token')
const refreshTokenFromLocalStorage = localStorage?.getItem('refresh_token')
// Tokens are now stored in cookies, no need to check localStorage
const pathname = usePathname()
const [init, setInit] = useState(false)
@ -57,21 +54,12 @@ const SwrInitializer = ({
router.replace('/install')
return
}
if (!((consoleToken && refreshToken) || (consoleTokenFromLocalStorage && refreshTokenFromLocalStorage))) {
router.replace('/signin')
return
}
if (searchParams.has('access_token') || searchParams.has('refresh_token')) {
if (consoleToken)
localStorage.setItem('console_token', consoleToken)
if (refreshToken)
localStorage.setItem('refresh_token', refreshToken)
const redirectUrl = resolvePostLoginRedirect(searchParams)
if (redirectUrl)
location.replace(redirectUrl)
else
router.replace(pathname)
}
const redirectUrl = resolvePostLoginRedirect(searchParams)
if (redirectUrl)
location.replace(redirectUrl)
else
router.replace(pathname)
setInit(true)
}
@ -79,7 +67,7 @@ const SwrInitializer = ({
router.replace('/signin')
}
})()
}, [isSetupFinished, router, pathname, searchParams, consoleToken, refreshToken, consoleTokenFromLocalStorage, refreshTokenFromLocalStorage])
}, [isSetupFinished, router, pathname, searchParams])
return init
? (

View File

@ -110,7 +110,7 @@ export const useNodesSyncDraft = () => {
if (postParams) {
console.log('Leader syncing workflow draft on page close')
navigator.sendBeacon(
`${API_PREFIX}/apps/${params.appId}/workflows/draft?_token=${localStorage.getItem('console_token')}`,
`${API_PREFIX}/apps/${params.appId}/workflows/draft`,
JSON.stringify(postParams.params),
)
}

View File

@ -178,6 +178,7 @@ const ToolPicker: FC<Props> = ({
mcpTools={mcpTools || []}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
onTagsChange={setTags}
/>
</div>
</PortalToFollowElemContent>

View File

@ -39,7 +39,6 @@ const Item: FC<Props> = ({
key={tool.id}
payload={tool}
viewType={ViewType.tree}
isShowLetterIndex={false}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}

View File

@ -37,6 +37,7 @@ export type ToolDefaultValue = {
paramSchemas: Record<string, any>[]
credential_id?: string
meta?: PluginMeta
output_schema?: Record<string, any>
}
export type DataSourceDefaultValue = {

View File

@ -8,8 +8,8 @@ export enum ScrollPosition {
}
type Params = {
wrapElemRef: React.RefObject<HTMLElement>
nextToStickyELemRef: React.RefObject<HTMLElement>
wrapElemRef: React.RefObject<HTMLElement | null>
nextToStickyELemRef: React.RefObject<HTMLElement | null>
}
const useStickyScroll = ({
wrapElemRef,

View File

@ -21,7 +21,7 @@ const DatasetsDetailProvider: FC<DatasetsDetailProviderProps> = ({
nodes,
children,
}) => {
const storeRef = useRef<DatasetsDetailStoreApi>()
const storeRef = useRef<DatasetsDetailStoreApi>(undefined)
if (!storeRef.current)
storeRef.current = createDatasetsDetailStore()

View File

@ -15,7 +15,7 @@ import {
getOutgoers,
useReactFlow,
} from 'reactflow'
import type { ToolDefaultValue } from '../block-selector/types'
import type { DataSourceDefaultValue, ToolDefaultValue } from '../block-selector/types'
import type { Edge, Node, OnNodeAdd } from '../types'
import { BlockEnum } from '../types'
import { useWorkflowStore } from '../store'
@ -1264,7 +1264,7 @@ export const useNodesInteractions = () => {
currentNodeId: string,
nodeType: BlockEnum,
sourceHandle: string,
toolDefaultValue?: ToolDefaultValue,
toolDefaultValue?: ToolDefaultValue | DataSourceDefaultValue,
) => {
if (getNodesReadOnly()) return

View File

@ -212,7 +212,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
agent_strategy_name: tool!.tool_name,
agent_strategy_provider_name: tool!.provider_name,
agent_strategy_label: tool!.tool_label,
agent_output_schema: tool!.output_schema,
agent_output_schema: tool!.output_schema || {},
plugin_unique_identifier: tool!.provider_id,
meta: tool!.meta,
})

View File

@ -28,7 +28,6 @@ const getIcon = (type: InputVarType) => {
[InputVarType.jsonObject]: RiBracesLine,
[InputVarType.singleFile]: RiFileList2Line,
[InputVarType.multiFiles]: RiFileCopy2Line,
[InputVarType.checkbox]: RiCheckboxLine,
} as any)[type] || RiTextSnippet
}

View File

@ -16,7 +16,7 @@ import {
} from '../../../types'
import type { Node } from '../../../types'
import BlockSelector from '../../../block-selector'
import type { ToolDefaultValue } from '../../../block-selector/types'
import type { DataSourceDefaultValue, ToolDefaultValue } from '../../../block-selector/types'
import {
useAvailableBlocks,
useIsChatMode,
@ -57,7 +57,7 @@ export const NodeTargetHandle = memo(({
if (!connected)
setOpen(v => !v)
}, [connected])
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => {
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue | DataSourceDefaultValue) => {
handleNodeAdd(
{
nodeType: type,
@ -140,7 +140,7 @@ export const NodeSourceHandle = memo(({
e.stopPropagation()
setOpen(v => !v)
}, [])
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => {
const handleSelect = useCallback((type: BlockEnum, toolDefaultValue?: ToolDefaultValue | DataSourceDefaultValue) => {
handleNodeAdd(
{
nodeType: type,

View File

@ -42,17 +42,17 @@ export const useVarColor = (variables: string[], isExceptionVariable?: boolean,
return 'text-util-colors-teal-teal-700'
return 'text-text-accent'
}, [variables, isExceptionVariable])
}, [variables, isExceptionVariable, variableCategory])
}
export const useVarName = (variables: string[], notShowFullPath?: boolean) => {
let variableFullPathName = variables.slice(1).join('.')
if (isRagVariableVar(variables))
variableFullPathName = variables.slice(2).join('.')
const variablesLength = variables.length
const varName = useMemo(() => {
let variableFullPathName = variables.slice(1).join('.')
if (isRagVariableVar(variables))
variableFullPathName = variables.slice(2).join('.')
const variablesLength = variables.length
const isSystem = isSystemVar(variables)
const varName = notShowFullPath ? variables[variablesLength - 1] : variableFullPathName
return `${isSystem ? 'sys.' : ''}${varName}`

View File

@ -53,8 +53,13 @@ import { useAppContext } from '@/context/app-context'
import { useStore } from '@/app/components/workflow/store'
import { useCollaboration } from '@/app/components/workflow/collaboration/hooks/use-collaboration'
type NodeChildProps = {
id: string
data: NodeProps['data']
}
type BaseNodeProps = {
children: ReactElement
children: ReactElement<Partial<NodeChildProps>>
id: NodeProps['id']
data: NodeProps['data']
}

View File

@ -11,6 +11,11 @@ export type OutputVar = Record<string, {
children: null // support nest in the future,
}>
export type CodeDependency = {
name: string
version?: string
}
export type CodeNodeType = CommonNodeType & {
variables: Variable[]
code_language: CodeLanguage

View File

@ -16,7 +16,7 @@ import {
const useConfig = (id: string, payload: HttpNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type]
const defaultConfig = useStore(s => s.nodesDefaultConfigs?.[payload.type])
const { inputs, setInputs } = useNodeCrud<HttpNodeType>(id, payload)

View File

@ -209,7 +209,7 @@ const ConditionItem = ({
onRemoveCondition?.(caseId, condition.id)
}, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])
const { getMatchedSchemaType } = useMatchSchemaType()
const { schemaTypeDefinitions } = useMatchSchemaType()
const handleVarChange = useCallback((valueSelector: ValueSelector, _varItem: Var) => {
const {
conversationVariables,
@ -226,7 +226,7 @@ const ConditionItem = ({
workflowTools,
dataSourceList: dataSourceList ?? [],
},
getMatchedSchemaType,
schemaTypeDefinitions,
})
const newCondition = produce(condition, (draft) => {
@ -241,7 +241,7 @@ const ConditionItem = ({
})
doUpdateCondition(newCondition)
setOpen(false)
}, [condition, doUpdateCondition, availableNodes, isChatMode, setControlPromptEditorRerenderKey])
}, [condition, doUpdateCondition, availableNodes, isChatMode, setControlPromptEditorRerenderKey, schemaTypeDefinitions])
const showBooleanInput = useMemo(() => {
if(condition.varType === VarType.boolean)

View File

@ -2,24 +2,21 @@ import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import Button from '@/app/components/base/button'
import { useAppContext } from '@/context/app-context'
import { logout } from '@/service/common'
import Avatar from '@/app/components/base/avatar'
import { Triangle } from '@/app/components/base/icons/src/public/education'
import { useLogout } from '@/service/use-common'
const UserInfo = () => {
const router = useRouter()
const { t } = useTranslation()
const { userProfile } = useAppContext()
const { mutateAsync: logout } = useLogout()
const handleLogout = async () => {
await logout({
url: '/logout',
params: {},
})
await logout()
localStorage.removeItem('setup_status')
localStorage.removeItem('console_token')
localStorage.removeItem('refresh_token')
// Tokens are now stored in cookies and cleared by backend
router.push('/signin')
}

View File

@ -72,8 +72,6 @@ const InstallForm = () => {
// Store tokens and redirect to apps if login successful
if (loginRes.result === 'success') {
localStorage.setItem('console_token', loginRes.data.access_token)
localStorage.setItem('refresh_token', loginRes.data.refresh_token)
router.replace('/apps')
}
else {

View File

@ -42,8 +42,6 @@ export default function CheckCode() {
setIsLoading(true)
const ret = await emailLoginWithCode({ email, code, token })
if (ret.result === 'success') {
localStorage.setItem('console_token', ret.data.access_token)
localStorage.setItem('refresh_token', ret.data.refresh_token)
if (invite_token) {
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
}

View File

@ -30,6 +30,7 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
const [password, setPassword] = useState('')
const [isLoading, setIsLoading] = useState(false)
const handleEmailPasswordLogin = async () => {
if (!email) {
Toast.notify({ type: 'error', message: t('login.error.emailEmpty') })
@ -66,8 +67,6 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
}
else {
localStorage.setItem('console_token', res.data.access_token)
localStorage.setItem('refresh_token', res.data.refresh_token)
const redirectUrl = resolvePostLoginRedirect(searchParams)
router.replace(redirectUrl || '/apps')
}

View File

@ -58,8 +58,7 @@ export default function InviteSettingsPage() {
},
})
if (res.result === 'success') {
localStorage.setItem('console_token', res.data.access_token)
localStorage.setItem('refresh_token', res.data.refresh_token)
// Tokens are now stored in cookies by the backend
await setLocaleOnClient(language, false)
const redirectUrl = resolvePostLoginRedirect(searchParams)
router.replace(redirectUrl || '/apps')

View File

@ -16,16 +16,18 @@ import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { resolvePostLoginRedirect } from './utils/post-login-redirect'
import Split from './split'
import { useIsLogin } from '@/service/use-common'
const NormalForm = () => {
const { t } = useTranslation()
const router = useRouter()
const searchParams = useSearchParams()
const consoleToken = decodeURIComponent(searchParams.get('access_token') || '')
const refreshToken = decodeURIComponent(searchParams.get('refresh_token') || '')
const { isLoading: isCheckLoading, data: loginData } = useIsLogin()
const isLoggedIn = loginData?.logged_in
const message = decodeURIComponent(searchParams.get('message') || '')
const invite_token = decodeURIComponent(searchParams.get('invite_token') || '')
const [isLoading, setIsLoading] = useState(true)
const [isInitCheckLoading, setInitCheckLoading] = useState(true)
const isLoading = isCheckLoading || loginData?.logged_in || isInitCheckLoading
const { systemFeatures } = useGlobalPublicStore()
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
const [showORLine, setShowORLine] = useState(false)
@ -36,9 +38,7 @@ const NormalForm = () => {
const init = useCallback(async () => {
try {
if (consoleToken && refreshToken) {
localStorage.setItem('console_token', consoleToken)
localStorage.setItem('refresh_token', refreshToken)
if (isLoggedIn) {
const redirectUrl = resolvePostLoginRedirect(searchParams)
router.replace(redirectUrl || '/apps')
return
@ -67,12 +67,12 @@ const NormalForm = () => {
console.error(error)
setAllMethodsAreDisabled(true)
}
finally { setIsLoading(false) }
}, [consoleToken, refreshToken, message, router, invite_token, isInviteLink, systemFeatures])
finally { setInitCheckLoading(false) }
}, [isLoggedIn, message, router, invite_token, isInviteLink, systemFeatures])
useEffect(() => {
init()
}, [init])
if (isLoading || consoleToken) {
if (isLoading) {
return <div className={
cn(
'flex w-full grow flex-col items-center justify-center',

View File

@ -52,14 +52,12 @@ const ChangePasswordForm = () => {
new_password: password,
password_confirm: confirmPassword,
})
const { result, data } = res as MailRegisterResponse
const { result } = res as MailRegisterResponse
if (result === 'success') {
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
localStorage.setItem('console_token', data.access_token)
localStorage.setItem('refresh_token', data.refresh_token)
router.replace('/apps')
}
}

View File

@ -144,6 +144,17 @@ export const getMaxToken = (modelId: string) => {
export const LOCALE_COOKIE_NAME = 'locale'
export const CSRF_COOKIE_NAME = () => {
const isSecure = API_PREFIX.startsWith('https://')
return isSecure ? '__Host-csrf_token' : 'csrf_token'
}
export const CSRF_HEADER_NAME = 'X-CSRF-Token'
export const ACCESS_TOKEN_LOCAL_STORAGE_NAME = 'access_token'
export const PASSPORT_LOCAL_STORAGE_NAME = (appCode: string) => `passport-${appCode}`
export const PASSPORT_HEADER_NAME = 'X-App-Passport'
export const WEB_APP_SHARE_CODE_HEADER_NAME = 'X-App-Code'
export const DEFAULT_VALUE_MAX_LEN = 48
export const DEFAULT_PARAGRAPH_VALUE_MAX_LEN = 1000

View File

@ -2,14 +2,12 @@
import type { ChatConfig } from '@/app/components/base/chat/types'
import Loading from '@/app/components/base/loading'
import { checkOrSetAccessToken } from '@/app/components/share/utils'
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'
import { useGlobalPublicStore } from './global-public-context'
@ -71,24 +69,13 @@ const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
}, [shareCode, updateShareCode])
const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode)
const [isFetchingAccessToken, setIsFetchingAccessToken] = useState(true)
useEffect(() => {
if (accessModeResult?.accessMode) {
if (accessModeResult?.accessMode)
updateWebAppAccessMode(accessModeResult.accessMode)
if (accessModeResult.accessMode === AccessMode.PUBLIC) {
setIsFetchingAccessToken(true)
checkOrSetAccessToken(shareCode).finally(() => {
setIsFetchingAccessToken(false)
})
}
else {
setIsFetchingAccessToken(false)
}
}
}, [accessModeResult, updateWebAppAccessMode, shareCode])
if (isGlobalPending || isFetching || isFetchingAccessToken) {
if (isGlobalPending || isFetching) {
return <div className='flex h-full w-full items-center justify-center'>
<Loading />
</div>

View File

@ -2,63 +2,6 @@ import type { AliyunConfig, ArizeConfig, LangFuseConfig, LangSmithConfig, OpikCo
import type { App, AppMode, AppTemplate, SiteConfig } from '@/types/app'
import type { Dependency } from '@/app/components/plugins/types'
/* export type App = {
id: string
name: string
description: string
mode: AppMode
enable_site: boolean
enable_api: boolean
api_rpm: number
api_rph: number
is_demo: boolean
model_config: AppModelConfig
providers: Array<{ provider: string; token_is_set: boolean }>
site: SiteConfig
created_at: string
}
export type AppModelConfig = {
provider: string
model_id: string
configs: {
prompt_template: string
prompt_variables: Array<PromptVariable>
completion_params: CompletionParam
}
}
export type PromptVariable = {
key: string
name: string
description: string
type: string | number
default: string
options: string[]
}
export type CompletionParam = {
max_tokens: number
temperature: number
top_p: number
echo: boolean
stop: string[]
presence_penalty: number
frequency_penalty: number
}
export type SiteConfig = {
access_token: string
title: string
author: string
support_email: string
default_language: string
customize_domain: string
theme: string
customize_token_strategy: 'must' | 'allow' | 'not_allow'
prompt_public: boolean
} */
export enum DSLImportMode {
YAML_CONTENT = 'yaml-content',
YAML_URL = 'yaml-url',

View File

@ -54,7 +54,7 @@
"@lexical/link": "^0.36.2",
"@lexical/list": "^0.36.2",
"@lexical/react": "^0.36.2",
"@lexical/selection": "^0.36.2",
"@lexical/selection": "^0.37.0",
"@lexical/text": "^0.36.2",
"@lexical/utils": "^0.37.0",
"@monaco-editor/react": "^4.6.0",
@ -81,7 +81,7 @@
"elkjs": "^0.9.3",
"emoji-mart": "^5.5.2",
"fast-deep-equal": "^3.1.3",
"html-to-image": "1.11.11",
"html-to-image": "1.11.13",
"i18next": "^23.16.4",
"i18next-resources-to-backend": "^1.2.1",
"immer": "^9.0.19",

14
web/pnpm-lock.yaml generated
View File

@ -80,8 +80,8 @@ importers:
specifier: ^0.36.2
version: 0.36.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(yjs@13.6.27)
'@lexical/selection':
specifier: ^0.36.2
version: 0.36.2
specifier: ^0.37.0
version: 0.37.0
'@lexical/text':
specifier: ^0.36.2
version: 0.36.2
@ -161,8 +161,8 @@ importers:
specifier: ^3.1.3
version: 3.1.3
html-to-image:
specifier: 1.11.11
version: 1.11.11
specifier: 1.11.13
version: 1.11.13
i18next:
specifier: ^23.16.4
version: 23.16.8
@ -5640,8 +5640,8 @@ packages:
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
html-to-image@1.11.11:
resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==}
html-to-image@1.11.13:
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
@ -14926,7 +14926,7 @@ snapshots:
dependencies:
void-elements: 3.1.0
html-to-image@1.11.11: {}
html-to-image@1.11.13: {}
html-url-attributes@3.0.1: {}

View File

@ -1,4 +1,4 @@
import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config'
import { API_PREFIX, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, IS_CE_EDITION, PASSPORT_HEADER_NAME, PUBLIC_API_PREFIX, WEB_APP_SHARE_CODE_HEADER_NAME } from '@/config'
import { refreshAccessTokenOrRelogin } from './refresh-token'
import Toast from '@/app/components/base/toast'
import { basePath } from '@/utils/var'
@ -21,15 +21,16 @@ import type {
WorkflowFinishedResponse,
WorkflowStartedResponse,
} from '@/types/workflow'
import { removeAccessToken } from '@/app/components/share/utils'
import type { FetchOptionType, ResponseError } from './fetch'
import { ContentType, base, getAccessToken, getBaseOptions } from './fetch'
import { ContentType, base, getBaseOptions } from './fetch'
import { asyncRunSafe } from '@/utils'
import type {
DataSourceNodeCompletedResponse,
DataSourceNodeErrorResponse,
DataSourceNodeProcessingResponse,
} from '@/types/pipeline'
import Cookies from 'js-cookie'
import { getWebAppPassport } from './webapp-auth'
const TIME_OUT = 100000
export type IOnDataMoreInfo = {
@ -122,14 +123,19 @@ function unicodeToChar(text: string) {
})
}
const WBB_APP_LOGIN_PATH = '/webapp-signin'
function requiredWebSSOLogin(message?: string, code?: number) {
const params = new URLSearchParams()
// prevent redirect loop
if(globalThis.location.pathname === WBB_APP_LOGIN_PATH)
return
params.append('redirect_url', encodeURIComponent(`${globalThis.location.pathname}${globalThis.location.search}`))
if (message)
params.append('message', message)
if (code)
params.append('code', String(code))
globalThis.location.href = `${globalThis.location.origin}${basePath}/webapp-signin?${params.toString()}`
globalThis.location.href = `${globalThis.location.origin}${basePath}/${WBB_APP_LOGIN_PATH}?${params.toString()}`
}
export function format(text: string) {
@ -338,12 +344,14 @@ type UploadResponse = {
export const upload = async (options: UploadOptions, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise<UploadResponse> => {
const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
const token = await getAccessToken(isPublicAPI)
const shareCode = globalThis.location.pathname.split('/').slice(-1)[0]
const defaultOptions = {
method: 'POST',
url: (url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`) + (searchParams || ''),
headers: {
Authorization: `Bearer ${token}`,
[CSRF_HEADER_NAME]: Cookies.get(CSRF_COOKIE_NAME()) || '',
[PASSPORT_HEADER_NAME]: getWebAppPassport(shareCode),
[WEB_APP_SHARE_CODE_HEADER_NAME]: shareCode,
},
}
const mergedOptions = {
@ -413,14 +421,17 @@ export const ssePost = async (
} = otherOptions
const abortController = new AbortController()
const token = localStorage.getItem('console_token')
// No need to get token from localStorage, cookies will be sent automatically
const baseOptions = getBaseOptions()
const shareCode = globalThis.location.pathname.split('/').slice(-1)[0]
const options = Object.assign({}, baseOptions, {
method: 'POST',
signal: abortController.signal,
headers: new Headers({
Authorization: `Bearer ${token}`,
[CSRF_HEADER_NAME]: Cookies.get(CSRF_COOKIE_NAME()) || '',
[WEB_APP_SHARE_CODE_HEADER_NAME]: shareCode,
[PASSPORT_HEADER_NAME]: getWebAppPassport(shareCode),
}),
} as RequestInit, fetchOptions)
@ -439,9 +450,6 @@ export const ssePost = async (
if (body)
options.body = JSON.stringify(body)
const accessToken = await getAccessToken(isPublicAPI)
; (options.headers as Headers).set('Authorization', `Bearer ${accessToken}`)
globalThis.fetch(urlWithPrefix, options as RequestInit)
.then((res) => {
if (!/^[23]\d{2}$/.test(String(res.status))) {
@ -452,15 +460,11 @@ export const ssePost = async (
if (data.code === 'web_app_access_denied')
requiredWebSSOLogin(data.message, 403)
if (data.code === 'web_sso_auth_required') {
removeAccessToken()
if (data.code === 'web_sso_auth_required')
requiredWebSSOLogin()
}
if (data.code === 'unauthorized') {
removeAccessToken()
if (data.code === 'unauthorized')
requiredWebSSOLogin()
}
}
})
}
@ -551,13 +555,11 @@ export const request = async<T>(url: string, options = {}, otherOptions?: IOther
return Promise.reject(err)
}
if (code === 'web_sso_auth_required') {
removeAccessToken()
requiredWebSSOLogin()
return Promise.reject(err)
}
if (code === 'unauthorized_and_force_logout') {
localStorage.removeItem('console_token')
localStorage.removeItem('refresh_token')
// Cookies will be cleared by the backend
globalThis.location.reload()
return Promise.reject(err)
}
@ -566,7 +568,6 @@ export const request = async<T>(url: string, options = {}, otherOptions?: IOther
silent,
} = otherOptionsForBaseFetch
if (isPublicAPI && code === 'unauthorized') {
removeAccessToken()
requiredWebSSOLogin()
return Promise.reject(err)
}

View File

@ -40,7 +40,7 @@ import type { SystemFeatures } from '@/types/feature'
type LoginSuccess = {
result: 'success'
data: { access_token: string; refresh_token: string }
data: { access_token: string }
}
type LoginFail = {
result: 'fail'
@ -56,10 +56,6 @@ export const webAppLogin: Fetcher<LoginResponse, { url: string; body: Record<str
return post(url, { body }, { isPublicAPI: true }) as Promise<LoginResponse>
}
export const fetchNewToken: Fetcher<CommonResponse & { data: { access_token: string; refresh_token: string } }, { body: Record<string, any> }> = ({ body }) => {
return post('/refresh-token', { body }) as Promise<CommonResponse & { data: { access_token: string; refresh_token: string } }>
}
export const setup: Fetcher<CommonResponse, { body: Record<string, any> }> = ({ body }) => {
return post<CommonResponse>('/setup', { body })
}
@ -84,10 +80,6 @@ export const updateUserProfile: Fetcher<CommonResponse, { url: string; body: Rec
return post<CommonResponse>(url, { body })
}
export const logout: Fetcher<CommonResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<CommonResponse>(url, params)
}
export const fetchLangGeniusVersion: Fetcher<LangGeniusVersionResponse, { url: string; params: Record<string, any> }> = ({ url, params }) => {
return get<LangGeniusVersionResponse>(url, { params })
}

View File

@ -2,9 +2,9 @@ import type { AfterResponseHook, BeforeErrorHook, BeforeRequestHook, Hooks } fro
import ky from 'ky'
import type { IOtherOptions } from './base'
import Toast from '@/app/components/base/toast'
import { API_PREFIX, APP_VERSION, MARKETPLACE_API_PREFIX, PUBLIC_API_PREFIX } from '@/config'
import { getInitialTokenV2, isTokenV1 } from '@/app/components/share/utils'
import { getProcessedSystemVariablesFromUrlParams } from '@/app/components/base/chat/utils'
import { API_PREFIX, APP_VERSION, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, MARKETPLACE_API_PREFIX, PASSPORT_HEADER_NAME, PUBLIC_API_PREFIX, WEB_APP_SHARE_CODE_HEADER_NAME } from '@/config'
import Cookies from 'js-cookie'
import { getWebAppAccessToken, getWebAppPassport } from './webapp-auth'
const TIME_OUT = 100000
@ -69,35 +69,15 @@ const beforeErrorToast = (otherOptions: IOtherOptions): BeforeErrorHook => {
}
}
export async function getAccessToken(isPublicAPI?: boolean) {
if (isPublicAPI) {
const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id
const accessToken = localStorage.getItem('token') || JSON.stringify({ version: 2 })
let accessTokenJson: Record<string, any> = { version: 2 }
try {
accessTokenJson = JSON.parse(accessToken)
if (isTokenV1(accessTokenJson))
accessTokenJson = getInitialTokenV2()
}
catch {
}
return accessTokenJson[sharedToken]?.[userId || 'DEFAULT']
}
else {
return localStorage.getItem('console_token') || ''
}
}
const beforeRequestPublicAuthorization: BeforeRequestHook = async (request) => {
const token = await getAccessToken(true)
request.headers.set('Authorization', `Bearer ${token}`)
}
const beforeRequestAuthorization: BeforeRequestHook = async (request) => {
const accessToken = await getAccessToken()
request.headers.set('Authorization', `Bearer ${accessToken}`)
const beforeRequestPublicWithCode = (request: Request) => {
request.headers.set('Authorization', `Bearer ${getWebAppAccessToken()}`)
const shareCode = globalThis.location.pathname.split('/').filter(Boolean).pop() || ''
// some pages does not end with share code, so we need to check it
// TODO: maybe find a better way to access app code?
if (shareCode === 'webapp-signin' || shareCode === 'check-code')
return
request.headers.set(WEB_APP_SHARE_CODE_HEADER_NAME, shareCode)
request.headers.set(PASSPORT_HEADER_NAME, getWebAppPassport(shareCode))
}
const baseHooks: Hooks = {
@ -148,6 +128,8 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
}
const fetchPathname = base + (url.startsWith('/') ? url : `/${url}`)
if (!isMarketplaceAPI)
(headers as any).set(CSRF_HEADER_NAME, Cookies.get(CSRF_COOKIE_NAME()) || '')
if (deleteContentType)
(headers as any).delete('Content-Type')
@ -165,8 +147,7 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
],
beforeRequest: [
...baseHooks.beforeRequest || [],
isPublicAPI && beforeRequestPublicAuthorization,
!isPublicAPI && !isMarketplaceAPI && beforeRequestAuthorization,
isPublicAPI && beforeRequestPublicWithCode,
].filter((h): h is BeforeRequestHook => Boolean(h)),
afterResponse: [
...baseHooks.afterResponse || [],

View File

@ -39,7 +39,6 @@ async function getNewAccessToken(timeout: number): Promise<void> {
globalThis.localStorage.setItem(LOCAL_STORAGE_KEY, '1')
globalThis.localStorage.setItem('last_refresh_time', new Date().getTime().toString())
globalThis.addEventListener('beforeunload', releaseRefreshLock)
const refresh_token = globalThis.localStorage.getItem('refresh_token')
// Do not use baseFetch to refresh tokens.
// If a 401 response occurs and baseFetch itself attempts to refresh the token,
@ -48,10 +47,11 @@ async function getNewAccessToken(timeout: number): Promise<void> {
// that does not call baseFetch and uses a single retry mechanism.
const [error, ret] = await fetchWithRetry(globalThis.fetch(`${API_PREFIX}/refresh-token`, {
method: 'POST',
credentials: 'include', // Important: include cookies in the request
headers: {
'Content-Type': 'application/json;utf-8',
},
body: JSON.stringify({ refresh_token }),
// No body needed - refresh token is in cookie
}))
if (error) {
return Promise.reject(error)
@ -59,10 +59,6 @@ async function getNewAccessToken(timeout: number): Promise<void> {
else {
if (ret.status === 401)
return Promise.reject(ret)
const { data } = await ret.json()
globalThis.localStorage.setItem('console_token', data.access_token)
globalThis.localStorage.setItem('refresh_token', data.refresh_token)
}
}
}

View File

@ -34,6 +34,8 @@ import type {
} from '@/models/share'
import type { ChatConfig } from '@/app/components/base/chat/types'
import type { AccessMode } from '@/models/access-control'
import { WEB_APP_SHARE_CODE_HEADER_NAME } from '@/config'
import { getWebAppAccessToken } from './webapp-auth'
function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) {
switch (action) {
@ -286,16 +288,14 @@ export const textToAudioStream = (url: string, isPublicAPI: boolean, header: { c
return (getAction('post', !isPublicAPI))(url, { body, header }, { needAllResponseContent: true })
}
export const fetchAccessToken = async ({ appCode, userId, webAppAccessToken }: { appCode: string, userId?: string, webAppAccessToken?: string | null }) => {
export const fetchAccessToken = async ({ userId, appCode }: { userId?: string, appCode: string }) => {
const headers = new Headers()
headers.append('X-App-Code', appCode)
headers.append(WEB_APP_SHARE_CODE_HEADER_NAME, appCode)
headers.append('Authorization', `Bearer ${getWebAppAccessToken()}`)
const params = new URLSearchParams()
if (webAppAccessToken)
params.append('web_app_access_token', webAppAccessToken)
if (userId)
params.append('user_id', userId)
userId && params.append('user_id', userId)
const url = `/passport?${params.toString()}`
return get(url, { headers }) as Promise<{ access_token: string }>
return get<{ access_token: string }>(url, { headers }) as Promise<{ access_token: string }>
}
export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => {

View File

@ -50,7 +50,7 @@ export const useMailValidity = () => {
})
}
export type MailRegisterResponse = { result: string, data: { access_token: string, refresh_token: string } }
export type MailRegisterResponse = { result: string, data: {} }
export const useMailRegister = () => {
return useMutation({
@ -106,3 +106,23 @@ export const useSchemaTypeDefinitions = () => {
queryFn: () => get<SchemaTypeDefinition[]>('/spec/schema-definitions'),
})
}
type isLogin = {
logged_in: boolean
}
export const useIsLogin = () => {
return useQuery<isLogin>({
queryKey: [NAME_SPACE, 'is-login'],
staleTime: 0,
gcTime: 0,
queryFn: () => get<isLogin>('/login/status'),
})
}
export const useLogout = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'logout'],
mutationFn: () => post('/logout'),
})
}

View File

@ -8,6 +8,8 @@ export const useGetWebAppAccessModeByCode = (code: string | null) => {
queryKey: [NAME_SPACE, 'appAccessMode', code],
queryFn: () => getAppAccessModeByAppCode(code!),
enabled: !!code,
staleTime: 0, // backend change the access mode may cause the logic error. Because /permission API is no cached.
gcTime: 0,
})
}

View File

@ -0,0 +1,53 @@
import { ACCESS_TOKEN_LOCAL_STORAGE_NAME, PASSPORT_LOCAL_STORAGE_NAME } from '@/config'
import { getPublic, postPublic } from './base'
export function setWebAppAccessToken(token: string) {
localStorage.setItem(ACCESS_TOKEN_LOCAL_STORAGE_NAME, token)
}
export function setWebAppPassport(shareCode: string, token: string) {
localStorage.setItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode), token)
}
export function getWebAppAccessToken() {
return localStorage.getItem(ACCESS_TOKEN_LOCAL_STORAGE_NAME) || ''
}
export function getWebAppPassport(shareCode: string) {
return localStorage.getItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode)) || ''
}
export function clearWebAppAccessToken() {
localStorage.removeItem(ACCESS_TOKEN_LOCAL_STORAGE_NAME)
}
export function clearWebAppPassport(shareCode: string) {
localStorage.removeItem(PASSPORT_LOCAL_STORAGE_NAME(shareCode))
}
type isWebAppLogin = {
logged_in: boolean
app_logged_in: boolean
}
export async function webAppLoginStatus(enabled: boolean, shareCode: string) {
if (!enabled) {
return {
userLoggedIn: true,
appLoggedIn: true,
}
}
// check remotely, the access token could be in cookie (enterprise SSO redirected with https)
const { logged_in, app_logged_in } = await getPublic<isWebAppLogin>(`/login/status?app_code=${shareCode}`)
return {
userLoggedIn: logged_in,
appLoggedIn: app_logged_in,
}
}
export async function webAppLogout(shareCode: string) {
clearWebAppAccessToken()
clearWebAppPassport(shareCode)
await postPublic('/logout')
}