From 243e9b68f4a25df21e5a53890fe6362e7e3c6f35 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Wed, 26 Aug 2026 15:06:27 +0800 Subject: [PATCH] fix(auth): prevent login page redirect loop Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- web/src/app/layout.tsx | 1 - web/src/app/router.tsx | 4 +-- .../features/auth/use-auth-methods.test.ts | 26 +++++++++++-------- web/src/features/auth/use-auth-methods.ts | 14 +++++++--- web/src/pages/reset-password.tsx | 2 +- web/src/pages/settings/security.tsx | 2 +- 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index dc5a3b80..7a52720b 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -123,7 +123,6 @@ export function Layout() { ) : ( {t('nav.login')} diff --git a/web/src/app/router.tsx b/web/src/app/router.tsx index bb1aac30..71d91116 100644 --- a/web/src/app/router.tsx +++ b/web/src/app/router.tsx @@ -183,8 +183,8 @@ const skillsRoute = createRoute({ const loginRoute = createRoute({ getParentRoute: () => rootRoute, path: 'login', - validateSearch: (search: Record): { returnTo: string; reason?: string } => ({ - returnTo: typeof search.returnTo === 'string' ? search.returnTo : '', + validateSearch: (search: Record): { returnTo?: string; reason?: string } => ({ + returnTo: typeof search.returnTo === 'string' && search.returnTo ? search.returnTo : undefined, reason: typeof search.reason === 'string' ? search.reason : undefined, }), component: LoginPage, diff --git a/web/src/features/auth/use-auth-methods.test.ts b/web/src/features/auth/use-auth-methods.test.ts index 36fdad2e..f353b95c 100644 --- a/web/src/features/auth/use-auth-methods.test.ts +++ b/web/src/features/auth/use-auth-methods.test.ts @@ -1,15 +1,19 @@ import { describe, expect, it } from 'vitest' -import * as authMethods from './use-auth-methods' +import { getAuthMethodsQueryOptions, useAuthMethods } from './use-auth-methods' -/** - * use-auth-methods is a thin useQuery wrapper around authApi.getMethods. - * The query key includes the returnTo parameter for proper cache isolation. - * There are no exported pure functions or data transformations to unit-test. - * - * This file verifies the public API surface so that accidental export removals are caught. - */ -describe('use-auth-methods module exports', () => { - it('exports useAuthMethods hook', () => { - expect(authMethods.useAuthMethods).toBeTypeOf('function') +describe('getAuthMethodsQueryOptions', () => { + it('keeps login auth-method lookup local to the page and bypasses the global 401 redirect', () => { + const options = getAuthMethodsQueryOptions('/dashboard') + + expect(options.queryKey).toEqual(['auth', 'methods', '/dashboard']) + expect(options.retry).toBe(false) + expect(options.meta).toEqual({ skipGlobalErrorHandler: true }) + expect(options.queryFn).toBeTypeOf('function') + }) +}) + +describe('use-auth-methods module exports', () => { + it('exports useAuthMethods hook', () => { + expect(useAuthMethods).toBeTypeOf('function') }) }) diff --git a/web/src/features/auth/use-auth-methods.ts b/web/src/features/auth/use-auth-methods.ts index 864d054a..cfe1c907 100644 --- a/web/src/features/auth/use-auth-methods.ts +++ b/web/src/features/auth/use-auth-methods.ts @@ -5,9 +5,17 @@ import type { AuthMethod } from '@/api/types' /** * Loads the backend-advertised authentication methods for the current entry point. */ -export function useAuthMethods(returnTo?: string) { - return useQuery({ +export function getAuthMethodsQueryOptions(returnTo?: string) { + return { queryKey: ['auth', 'methods', returnTo ?? ''], queryFn: () => authApi.getMethods(returnTo), - }) + retry: false, + meta: { + skipGlobalErrorHandler: true, + }, + } +} + +export function useAuthMethods(returnTo?: string) { + return useQuery(getAuthMethodsQueryOptions(returnTo)) } diff --git a/web/src/pages/reset-password.tsx b/web/src/pages/reset-password.tsx index ef4777de..229bf7bd 100644 --- a/web/src/pages/reset-password.tsx +++ b/web/src/pages/reset-password.tsx @@ -100,7 +100,7 @@ export function ResetPasswordPage() {

{t('resetPassword.successMessage')}

- + {t('resetPassword.backToLogin')} diff --git a/web/src/pages/settings/security.tsx b/web/src/pages/settings/security.tsx index 3726ad7d..8e4dc005 100644 --- a/web/src/pages/settings/security.tsx +++ b/web/src/pages/settings/security.tsx @@ -72,7 +72,7 @@ export function SecuritySettingsPage() { clearSessionScopedQueries(queryClient) queryClient.setQueryData(['auth', 'me'], null) } - await navigate({ to: '/login', search: { returnTo: '' } }) + await navigate({ to: '/login' }) } catch (error) { if (error instanceof ApiError && error.status === 401) { setErrorMessage(t('security.invalidCurrentPassword'))