)
+
+ render()
+
+ expect(screen.getByRole('button', { name: 'login.join Acme' })).toBeInTheDocument()
+ expect(mockReplace).not.toHaveBeenCalled()
+ })
+ })
})
diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx
index 41de03fa7b4..33eeea9e6c2 100644
--- a/web/app/signin/invite-settings/page.tsx
+++ b/web/app/signin/invite-settings/page.tsx
@@ -12,13 +12,14 @@ import {
} from '@langgenius/dify-ui/select'
import { toast } from '@langgenius/dify-ui/toast'
import { RiAccountCircleLine } from '@remixicon/react'
-import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
+import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
import { noop } from 'es-toolkit/function'
-import { useCallback, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { LICENSE_LINK } from '@/constants/link'
import { useLocale } from '@/context/i18n'
+import { isLegacyBase401, userProfileQueryOptions } from '@/features/account-profile/client'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { i18n, setLocaleOnClient } from '@/i18n-config'
import { languages } from '@/i18n-config/language'
@@ -30,6 +31,7 @@ import { useInvitationCheck } from '@/service/use-common'
import { replaceLoginRedirect } from '@/utils/login-redirect.client'
import { getBrowserTimezone, timezones } from '@/utils/timezone'
import { basePath } from '@/utils/var'
+import { isInvitationForAccount } from '../utils/invitation-account'
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
type LanguageSelectOption = {
@@ -67,6 +69,15 @@ export default function InviteSettingsPage() {
const queryClient = useQueryClient()
const searchParams = useSearchParams()
const token = decodeURIComponent(searchParams.get('invite_token') as string)
+ const {
+ data: userResp,
+ isPending: isProfilePending,
+ error: profileError,
+ } = useQuery({
+ ...userProfileQueryOptions(),
+ throwOnError: (err) => !isLegacyBase401(err),
+ refetchOnWindowFocus: false,
+ })
const locale = useLocale()
const [name, setName] = useState('')
const [isActivating, setIsActivating] = useState(false)
@@ -92,11 +103,28 @@ export default function InviteSettingsPage() {
},
}
const { data: checkRes, refetch: recheck } = useInvitationCheck(checkParams.params, !!token)
+ const isInvitationForCurrentAccount = isInvitationForAccount(
+ checkRes?.data?.email,
+ userResp?.profile.email,
+ )
+ const shouldReturnToSignIn =
+ !isProfilePending &&
+ Boolean(
+ checkRes?.is_valid &&
+ (isLegacyBase401(profileError) || (userResp && !isInvitationForCurrentAccount)),
+ )
const requiresAccountSetup =
checkRes?.data?.requires_setup ?? checkRes?.data?.account_status === 'pending'
+ useEffect(() => {
+ if (!shouldReturnToSignIn) return
+
+ router.replace(`/signin?${searchParams.toString()}`)
+ }, [router, searchParams, shouldReturnToSignIn])
+
const handleActivate = useCallback(async () => {
try {
+ if (!isInvitationForCurrentAccount) return
if (requiresAccountSetup && !name) {
toast.error(t(($) => $.enterYourName, { ns: 'login' }))
return
@@ -127,7 +155,7 @@ export default function InviteSettingsPage() {
setIsActivating(false)
}
}, [
- isActivating,
+ isInvitationForCurrentAccount,
language,
name,
queryClient,
@@ -140,7 +168,7 @@ export default function InviteSettingsPage() {
t,
])
- if (!checkRes) return
+ if (isProfilePending || shouldReturnToSignIn || !checkRes) return
if (!checkRes.is_valid) {
return (
diff --git a/web/app/signin/normal-form.tsx b/web/app/signin/normal-form.tsx
index 008f781d7b8..6604d1d8786 100644
--- a/web/app/signin/normal-form.tsx
+++ b/web/app/signin/normal-form.tsx
@@ -19,6 +19,7 @@ import MailAndPasswordAuth from './components/mail-and-password-auth'
import SocialAuth from './components/social-auth'
import SSOAuth from './components/sso-auth'
import Split from './split'
+import { isInvitationForAccount } from './utils/invitation-account'
import { resolvePostLoginRedirect } from './utils/post-login-redirect'
type AuthType = 'code' | 'password'
@@ -67,6 +68,10 @@ function NormalForm() {
})
const workspaceName = invitationCheckResp?.data?.workspace_name || ''
+ const isInvitationForCurrentAccount = isInvitationForAccount(
+ invitationCheckResp?.data?.email,
+ userResp?.profile.email,
+ )
const hasSocialLogin = systemFeatures.enable_social_oauth_login
const hasSsoLogin = Boolean(systemFeatures.sso_enforced_for_signin)
const hasEmailCodeLogin = systemFeatures.enable_email_code_login
@@ -83,18 +88,21 @@ function NormalForm() {
const noLoginMethodsConfigured =
!hasSocialLogin && !hasEmailCodeLogin && !hasEmailPasswordLogin && !hasSsoLogin
const allMethodsAreDisabled = noLoginMethodsConfigured || isInviteCheckError
- const isLoading = isCheckLoading || isLoggedIn || (isInviteLink && isInviteCheckLoading)
+ const shouldRedirectLoggedInUser = isLoggedIn && (!isInviteLink || isInvitationForCurrentAccount)
+ const isLoading =
+ isCheckLoading || shouldRedirectLoggedInUser || (isInviteLink && isInviteCheckLoading)
useEffect(() => {
if (!isLoggedIn) return
if (isInviteLink) {
+ if (!isInvitationForCurrentAccount) return
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
return
}
replaceLoginRedirect(resolvePostLoginRedirect(searchParams), router.replace, basePath)
- }, [isInviteLink, isLoggedIn, router, searchParams])
+ }, [isInvitationForCurrentAccount, isInviteLink, isLoggedIn, router, searchParams])
useEffect(() => {
if (message) toast.error(message)
diff --git a/web/app/signin/utils/invitation-account.ts b/web/app/signin/utils/invitation-account.ts
new file mode 100644
index 00000000000..a5ae0a0ff95
--- /dev/null
+++ b/web/app/signin/utils/invitation-account.ts
@@ -0,0 +1,9 @@
+export const isInvitationForAccount = (
+ invitationEmail?: string | null,
+ accountEmail?: string | null,
+) =>
+ Boolean(
+ invitationEmail &&
+ accountEmail &&
+ invitationEmail.trim().toLowerCase() === accountEmail.trim().toLowerCase(),
+ )