From e5b7281eb946e28114dfc3199d6efe54f5486529 Mon Sep 17 00:00:00 2001 From: Coding On Star <447357187@qq.com> Date: Wed, 15 Jul 2026 17:48:58 +0800 Subject: [PATCH] fix(auth): keep login redirects on current deployment (#39013) Co-authored-by: CodingOnStar --- web/app/(shareLayout)/components/splash.tsx | 5 +- .../webapp-signin/check-code/page.tsx | 7 ++- .../components/external-member-sso-auth.tsx | 3 +- .../components/mail-and-code-auth.tsx | 6 +-- .../components/mail-and-password-auth.tsx | 6 +-- .../webapp-signin/components/sso-auth.tsx | 5 +- web/app/(shareLayout)/webapp-signin/page.tsx | 3 +- web/app/auth/refresh/__tests__/route.spec.ts | 18 ++++--- web/app/auth/refresh/route.ts | 3 +- .../signin/check-code/__tests__/page.spec.tsx | 48 +++++++++++++++++++ web/app/signin/utils/post-login-redirect.ts | 3 +- web/utils/__tests__/login-redirect.spec.ts | 23 +++------ web/utils/login-redirect.ts | 11 +---- 13 files changed, 81 insertions(+), 60 deletions(-) diff --git a/web/app/(shareLayout)/components/splash.tsx b/web/app/(shareLayout)/components/splash.tsx index fe3360e72e0..9f1967cb39f 100644 --- a/web/app/(shareLayout)/components/splash.tsx +++ b/web/app/(shareLayout)/components/splash.tsx @@ -8,7 +8,6 @@ import { } from '@/app/(shareLayout)/webapp-signin/login-redirect' import AppUnavailable from '@/app/components/base/app-unavailable' import Loading from '@/app/components/base/loading' -import { IS_CLOUD_EDITION } from '@/config' import { useWebAppStore } from '@/context/web-app-context' import { usePathname, useRouter, useSearchParams } from '@/next/navigation' import { fetchAccessToken } from '@/service/share' @@ -48,7 +47,7 @@ function Splash({ children }: PropsWithChildren) { const loginRedirect = resolveWebAppLoginRedirect(redirectUrl, window.location.origin) const effectiveShareCode = loginRedirect?.appCode || shareCode if (!effectiveShareCode || (isWebAppSigninPath(pathname) && !loginRedirect)) { - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) return } @@ -62,7 +61,7 @@ function Splash({ children }: PropsWithChildren) { const loginRedirect = resolveWebAppLoginRedirect(redirectUrl, window.location.origin) const isSigninRoute = isWebAppSigninPath(pathname) if ((redirectUrl !== null && !loginRedirect) || (isSigninRoute && !loginRedirect)) { - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) return } diff --git a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx index 764838a8923..004d025197f 100644 --- a/web/app/(shareLayout)/webapp-signin/check-code/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/check-code/page.tsx @@ -8,7 +8,6 @@ import { useTranslation } from 'react-i18next' import { resolveWebAppLoginRedirect } from '@/app/(shareLayout)/webapp-signin/login-redirect' import Input from '@/app/components/base/input' import Countdown from '@/app/components/signin/countdown' -import { IS_CLOUD_EDITION } from '@/config' import { useLocale } from '@/context/i18n' import { useWebAppStore } from '@/context/web-app-context' import { useRouter, useSearchParams } from '@/next/navigation' @@ -35,13 +34,13 @@ export default function CheckCode() { useEffect(() => { if (!resolveWebAppLoginRedirect(redirectUrl, window.location.origin)) - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) }, [redirectUrl, router]) const verify = async () => { const loginRedirect = resolveWebAppLoginRedirect(redirectUrl, window.location.origin) if (!loginRedirect) { - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) return } try { @@ -89,7 +88,7 @@ export default function CheckCode() { const resendCode = async () => { const loginRedirect = resolveWebAppLoginRedirect(redirectUrl, window.location.origin) if (!loginRedirect) { - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) return } try { diff --git a/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx index 6e3a8bae7f9..c05e471a8d0 100644 --- a/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/external-member-sso-auth.tsx @@ -6,7 +6,6 @@ import { useCallback, useEffect } from 'react' import { resolveWebAppLoginRedirect } from '@/app/(shareLayout)/webapp-signin/login-redirect' import AppUnavailable from '@/app/components/base/app-unavailable' import Loading from '@/app/components/base/loading' -import { IS_CLOUD_EDITION } from '@/config' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { SSOProtocol } from '@/features/system-features/constants' import { useRouter, useSearchParams } from '@/next/navigation' @@ -29,7 +28,7 @@ const ExternalMemberSSOAuth = () => { const handleSSOLogin = useCallback(async () => { const loginRedirect = resolveWebAppLoginRedirect(redirectUrl, window.location.origin) if (!loginRedirect) { - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) return } diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx index 109c2f31463..5d984dfac41 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import { resolveWebAppLoginRedirect } from '@/app/(shareLayout)/webapp-signin/login-redirect' import Input from '@/app/components/base/input' import { COUNT_DOWN_TIME_MS, useSetCountdownLeftTime } from '@/app/components/signin/storage' -import { emailRegex, IS_CLOUD_EDITION } from '@/config' +import { emailRegex } from '@/config' import { useLocale } from '@/context/i18n' import { useRouter, useSearchParams } from '@/next/navigation' import { sendWebAppEMailLoginCode } from '@/service/common' @@ -27,13 +27,13 @@ export default function MailAndCodeAuth() { useEffect(() => { if (!resolveWebAppLoginRedirect(redirectUrl, window.location.origin)) - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) }, [redirectUrl, router]) const handleGetEMailVerificationCode = async () => { const loginRedirect = resolveWebAppLoginRedirect(redirectUrl, window.location.origin) if (!loginRedirect) { - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) return } try { diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx index 879314c7f91..343643374d9 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { resolveWebAppLoginRedirect } from '@/app/(shareLayout)/webapp-signin/login-redirect' import Input from '@/app/components/base/input' -import { emailRegex, IS_CLOUD_EDITION } from '@/config' +import { emailRegex } from '@/config' import { useLocale } from '@/context/i18n' import { useWebAppStore } from '@/context/web-app-context' import Link from '@/next/link' @@ -39,13 +39,13 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut useEffect(() => { if (!resolveWebAppLoginRedirect(redirectUrl, window.location.origin)) - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) }, [redirectUrl, router]) const handleEmailPasswordLogin = async () => { const loginRedirect = resolveWebAppLoginRedirect(redirectUrl, window.location.origin) if (!loginRedirect) { - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) return } if (!email) { diff --git a/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx index 6792ab98d91..f46da750252 100644 --- a/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/sso-auth.tsx @@ -5,7 +5,6 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { resolveWebAppLoginRedirect } from '@/app/(shareLayout)/webapp-signin/login-redirect' import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' -import { IS_CLOUD_EDITION } from '@/config' import { SSOProtocol } from '@/features/system-features/constants' import { useRouter, useSearchParams } from '@/next/navigation' import { @@ -32,13 +31,13 @@ function SSOAuth({ protocol }: SSOAuthProps) { useEffect(() => { if (!resolveWebAppLoginRedirect(redirectUrl, window.location.origin)) - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) }, [redirectUrl, router]) const handleSSOLogin = () => { const loginRedirect = resolveWebAppLoginRedirect(redirectUrl, window.location.origin) if (!loginRedirect) { - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) return } setIsLoading(true) diff --git a/web/app/(shareLayout)/webapp-signin/page.tsx b/web/app/(shareLayout)/webapp-signin/page.tsx index 6c6eb382b05..ca1c96f78b0 100644 --- a/web/app/(shareLayout)/webapp-signin/page.tsx +++ b/web/app/(shareLayout)/webapp-signin/page.tsx @@ -5,7 +5,6 @@ import { useCallback, useEffect, useSyncExternalStore } from 'react' import { useTranslation } from 'react-i18next' import AppUnavailable from '@/app/components/base/app-unavailable' import Loading from '@/app/components/base/loading' -import { IS_CLOUD_EDITION } from '@/config' import { useWebAppStore } from '@/context/web-app-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { AccessMode } from '@/models/access-control' @@ -35,7 +34,7 @@ function WebSSOForm() { useEffect(() => { if (!resolveWebAppLoginRedirect(redirectUrl, window.location.origin)) - replaceLoginRedirect(getClientLoginFallback(IS_CLOUD_EDITION), router.replace, basePath) + replaceLoginRedirect(getClientLoginFallback(), router.replace, basePath) }, [redirectUrl, router]) const getSigninUrl = useCallback(() => { diff --git a/web/app/auth/refresh/__tests__/route.spec.ts b/web/app/auth/refresh/__tests__/route.spec.ts index 68622e05320..ce205e7bb0e 100644 --- a/web/app/auth/refresh/__tests__/route.spec.ts +++ b/web/app/auth/refresh/__tests__/route.spec.ts @@ -244,41 +244,39 @@ describe('auth refresh route', () => { expect(response.headers.get('location')).toBe('/signin?redirect_url=%2F') }) - it('should use the Cloud home as the fallback after a successful refresh', async () => { + it('should keep a Cloud staging fallback on the current deployment after refresh', async () => { mocks.isCloudEdition = true vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 200 }))) const { GET } = await import('../route') const response = await GET( createRequest( - 'https://cloud.dify.ai/auth/refresh?redirect_url=https%3A%2F%2Fevil.example', + 'https://saas.dify.dev/auth/refresh?redirect_url=https%3A%2F%2Fevil.example', 'refresh_token=old-refresh', ), ) expect(response.status).toBe(303) - expect(response.headers.get('location')).toBe('https://cloud.dify.ai/') + expect(response.headers.get('location')).toBe('/') }) - it('should carry the Cloud fallback through signin when refresh fails', async () => { + it('should carry the current Cloud deployment fallback through signin when refresh fails', async () => { mocks.isCloudEdition = true vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 401 }))) const { GET } = await import('../route') const response = await GET( createRequest( - 'https://cloud.dify.ai/auth/refresh?redirect_url=https%3A%2F%2Fevil.example', + 'https://saas.dify.dev/auth/refresh?redirect_url=https%3A%2F%2Fevil.example', 'refresh_token=expired', ), ) expect(response.status).toBe(303) - expect(response.headers.get('location')).toBe( - '/signin?redirect_url=https%3A%2F%2Fcloud.dify.ai%2F', - ) + expect(response.headers.get('location')).toBe('/signin?redirect_url=%2F') }) - it('should use the Cloud home when a trusted absolute target loops back to auth refresh', async () => { + it('should use the current deployment home when a trusted target loops back to auth refresh', async () => { mocks.isCloudEdition = true vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 200 }))) const { GET } = await import('../route') @@ -291,7 +289,7 @@ describe('auth refresh route', () => { ) expect(response.status).toBe(303) - expect(response.headers.get('location')).toBe('https://cloud.dify.ai/') + expect(response.headers.get('location')).toBe('/') }) it.each(['/auth/refresh/', '/auth/%72efresh'])( diff --git a/web/app/auth/refresh/route.ts b/web/app/auth/refresh/route.ts index b231840a244..698ca6df24c 100644 --- a/web/app/auth/refresh/route.ts +++ b/web/app/auth/refresh/route.ts @@ -1,5 +1,4 @@ import type { LoginRedirectTarget } from '@/utils/login-redirect' -import { IS_CLOUD_EDITION } from '@/config' import { resolveServerConsoleApiUrl } from '@/service/server' import { getServerLoginFallback, resolveLoginRedirectTarget } from '@/utils/login-redirect' import { basePath } from '@/utils/var' @@ -38,7 +37,7 @@ const addBasePathToInternalTarget = (target: LoginRedirectTarget): LoginRedirect const resolveSafeRedirectTarget = (request: Request): LoginRedirectTarget => { const requestUrl = new URL(request.url) const redirectUrl = requestUrl.searchParams.get('redirect_url') - const fallback = getServerLoginFallback(IS_CLOUD_EDITION, basePath) + const fallback = getServerLoginFallback(basePath) if (!redirectUrl) return fallback diff --git a/web/app/signin/check-code/__tests__/page.spec.tsx b/web/app/signin/check-code/__tests__/page.spec.tsx index 1ae3f8a63b3..de69fee5f04 100644 --- a/web/app/signin/check-code/__tests__/page.spec.tsx +++ b/web/app/signin/check-code/__tests__/page.spec.tsx @@ -20,6 +20,11 @@ vi.mock('@/app/components/base/amplitude', () => ({ trackEvent: vi.fn(), })) +vi.mock('@/config', async (importOriginal) => ({ + ...(await importOriginal()), + IS_CLOUD_EDITION: true, +})) + vi.mock('@/next/navigation', () => ({ useRouter: () => ({ back: navigationMocks.back, @@ -79,6 +84,10 @@ describe('CheckCode', () => { vi.mocked(emailLoginWithCode).mockResolvedValue({ result: 'success' }) }) + afterEach(() => { + vi.unstubAllGlobals() + }) + describe('Post-login profile bootstrap', () => { it('should resolve an inactive profile query before navigating to the console home', async () => { const user = userEvent.setup() @@ -129,5 +138,44 @@ describe('CheckCode', () => { }) expect(queryClient.getQueryState(profileQueryKey)?.status).toBe('success') }) + + it('should keep a Cloud verification-code login on the current deployment', async () => { + const user = userEvent.setup() + const queryClient = createQueryClient() + const locationReplace = vi.fn() + navigationMocks.searchParams = new URLSearchParams({ + email: 'hanxujiang%2B4%40dify.ai', + token: 'email-login-token', + }) + serviceBaseMocks.get.mockResolvedValue( + new Response(JSON.stringify(accountProfile), { + headers: { + 'content-type': 'application/json', + 'x-env': 'DEVELOPMENT', + 'x-version': '1.0.0', + }, + status: 200, + }), + ) + vi.stubGlobal('location', { + ...window.location, + origin: 'https://saas.dify.dev', + replace: locationReplace, + } as unknown as Location) + + render( + + + , + ) + + await user.type(screen.getByLabelText('login.checkCode.verificationCode'), '123456') + await user.click(screen.getByRole('button', { name: 'login.checkCode.verify' })) + + await waitFor(() => { + expect(navigationMocks.replace).toHaveBeenCalledWith('/') + }) + expect(locationReplace).not.toHaveBeenCalled() + }) }) }) diff --git a/web/app/signin/utils/post-login-redirect.ts b/web/app/signin/utils/post-login-redirect.ts index 8bffa75e4eb..8d9991a51f0 100644 --- a/web/app/signin/utils/post-login-redirect.ts +++ b/web/app/signin/utils/post-login-redirect.ts @@ -1,5 +1,4 @@ import type { ReadonlyURLSearchParams } from '@/next/navigation' -import { IS_CLOUD_EDITION } from '@/config' import { getClientLoginFallback, resolveLoginRedirectTarget } from '@/utils/login-redirect' const REDIRECT_URL_KEY = 'redirect_url' @@ -75,7 +74,7 @@ function getDeviceRedirect(): string | null { export function resolvePostLoginRedirect(searchParams?: ReadonlyURLSearchParams) { const currentOrigin = typeof window === 'undefined' ? undefined : window.location.origin - const fallback = getClientLoginFallback(IS_CLOUD_EDITION) + const fallback = getClientLoginFallback() if (searchParams?.has(REDIRECT_URL_KEY)) { return ( diff --git a/web/utils/__tests__/login-redirect.spec.ts b/web/utils/__tests__/login-redirect.spec.ts index 56d41cbc16a..4674e4f1c2b 100644 --- a/web/utils/__tests__/login-redirect.spec.ts +++ b/web/utils/__tests__/login-redirect.spec.ts @@ -143,30 +143,19 @@ describe('resolveLoginRedirectTarget', () => { }) describe('login redirect fallbacks', () => { - it('should use the Cloud console home for the client Cloud fallback', () => { - expect(getClientLoginFallback(true)).toEqual({ - kind: 'absolute', - href: 'https://cloud.dify.ai/', + it('should keep the client fallback on the current deployment', () => { + expect(getClientLoginFallback()).toEqual({ + kind: 'internal', + href: '/', }) }) - it('should use an internal root for the client self-hosted fallback', () => { - expect(getClientLoginFallback(false)).toEqual({ kind: 'internal', href: '/' }) - }) - it.each([ ['', '/'], ['/', '/'], ['/console', '/console/'], ['/console/', '/console/'], - ])('should include basePath %s in the server self-hosted fallback', (basePath, href) => { - expect(getServerLoginFallback(false, basePath)).toEqual({ kind: 'internal', href }) - }) - - it('should use the Cloud console home for the server Cloud fallback', () => { - expect(getServerLoginFallback(true, '/console')).toEqual({ - kind: 'absolute', - href: 'https://cloud.dify.ai/', - }) + ])('should include basePath %s in the server deployment fallback', (basePath, href) => { + expect(getServerLoginFallback(basePath)).toEqual({ kind: 'internal', href }) }) }) diff --git a/web/utils/login-redirect.ts b/web/utils/login-redirect.ts index c9271ec3041..077d2174d80 100644 --- a/web/utils/login-redirect.ts +++ b/web/utils/login-redirect.ts @@ -7,7 +7,6 @@ type ResolveLoginRedirectOptions = { allowSameOriginAbsolute?: boolean } -const CLOUD_CONSOLE_HOME = 'https://cloud.dify.ai/' const INTERNAL_URL_BASE = 'https://login-redirect.invalid' const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001F\u007F]/ const MALFORMED_PERCENT_PATTERN = /%(?![\dA-F]{2})/i @@ -176,17 +175,11 @@ export function resolveLoginRedirectTarget( } } -export function getClientLoginFallback(isCloudEdition: boolean): LoginRedirectTarget { - if (isCloudEdition) return { kind: 'absolute', href: CLOUD_CONSOLE_HOME } +export function getClientLoginFallback(): LoginRedirectTarget { return { kind: 'internal', href: '/' } } -export function getServerLoginFallback( - isCloudEdition: boolean, - basePath: string, -): LoginRedirectTarget { - if (isCloudEdition) return { kind: 'absolute', href: CLOUD_CONSOLE_HOME } - +export function getServerLoginFallback(basePath: string): LoginRedirectTarget { const normalizedBasePath = normalizeBasePath(basePath) return { kind: 'internal', href: normalizedBasePath ? `${normalizedBasePath}/` : '/' } }