Merge pull request #759 from iflytek/fix/auth-login-redirect-loop

fix(auth): prevent login page redirect loop
This commit is contained in:
XiaoSeS 2026-08-26 15:57:30 +08:00 committed by GitHub
commit 4344ec6b22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 30 additions and 19 deletions

View file

@ -123,7 +123,6 @@ export function Layout() {
) : (
<Link
to="/login"
search={{ returnTo: '' }}
className="hover:opacity-80 transition-opacity"
>
{t('nav.login')}

View file

@ -183,8 +183,8 @@ const skillsRoute = createRoute({
const loginRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'login',
validateSearch: (search: Record<string, unknown>): { returnTo: string; reason?: string } => ({
returnTo: typeof search.returnTo === 'string' ? search.returnTo : '',
validateSearch: (search: Record<string, unknown>): { returnTo?: string; reason?: string } => ({
returnTo: typeof search.returnTo === 'string' && search.returnTo ? search.returnTo : undefined,
reason: typeof search.reason === 'string' ? search.reason : undefined,
}),
component: LoginPage,

View file

@ -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')
})
})

View file

@ -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<AuthMethod[]>({
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<AuthMethod[]>(getAuthMethodsQueryOptions(returnTo))
}

View file

@ -100,7 +100,7 @@ export function ResetPasswordPage() {
<p className="rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-700">
{t('resetPassword.successMessage')}
</p>
<Link to="/login" search={{ returnTo: '' }} className="block text-center font-medium text-primary hover:underline">
<Link to="/login" className="block text-center font-medium text-primary hover:underline">
{t('resetPassword.backToLogin')}
</Link>
</div>

View file

@ -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'))