mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-07 02:57:51 +00:00
fix(auth): redirect signed-in visitors away from login
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
5be60043d5
commit
0e9d2e2d3e
4 changed files with 61 additions and 2 deletions
|
|
@ -112,6 +112,13 @@ test.describe('Auth Entry', () => {
|
|||
await page.getByRole('button', { name: 'Register & Login' }).click()
|
||||
await expect(page).toHaveURL('/')
|
||||
|
||||
await page.goto('/login')
|
||||
await expect(page).toHaveURL('/')
|
||||
await page.goto('/login?returnTo=%2Fdashboard%2Ftokens%3Ftab%3Dactive%23latest')
|
||||
await expect(page).toHaveURL('/dashboard/tokens?tab=active#latest')
|
||||
await page.goto('/login?returnTo=%2Flogin')
|
||||
await expect(page).toHaveURL('/')
|
||||
|
||||
const loginPage = await browser.newPage()
|
||||
await setEnglishLocale(loginPage)
|
||||
await loginPage.goto('/login?returnTo=%2Fdashboard%2Ftokens')
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Layout } from './layout'
|
|||
import { getCurrentUser } from '@/api/client'
|
||||
import { RoleGuard } from '@/shared/components/role-guard'
|
||||
import { RouteError } from '@/shared/components/route-error'
|
||||
import { createRequireAuth, isSafeAuthReturnTo } from '@/shared/lib/auth-route'
|
||||
import { createRedirectAuthenticated, createRequireAuth, isSafeAuthReturnTo } from '@/shared/lib/auth-route'
|
||||
import { clearDynamicImportReloadGuard, recoverFromDynamicImportError } from '@/shared/lib/dynamic-import-recovery'
|
||||
import { normalizeSearchQuery } from '@/shared/lib/search-query'
|
||||
|
||||
|
|
@ -228,6 +228,7 @@ const rootRoute = createRootRoute({
|
|||
})
|
||||
|
||||
const requireAuth = createRequireAuth(getCurrentUser)
|
||||
const redirectAuthenticated = createRedirectAuthenticated(getCurrentUser)
|
||||
|
||||
const landingRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
|
|
@ -244,6 +245,7 @@ const skillsRoute = createRoute({
|
|||
const loginRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: 'login',
|
||||
beforeLoad: redirectAuthenticated,
|
||||
validateSearch: (search: Record<string, unknown>): { returnTo?: string; reason?: string } => ({
|
||||
returnTo: isSafeAuthReturnTo(search.returnTo) ? search.returnTo : undefined,
|
||||
reason: typeof search.reason === 'string' ? search.reason : undefined,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { isRedirect } from '@tanstack/react-router'
|
||||
import { buildReturnTo, createRequireAuth, resolveAuthReturnTo } from './auth-route'
|
||||
import { buildReturnTo, createRedirectAuthenticated, createRequireAuth, resolveAuthReturnTo } from './auth-route'
|
||||
|
||||
describe('auth-route', () => {
|
||||
it('returns to the original local page or the home page, never an external URL', () => {
|
||||
|
|
@ -55,4 +55,33 @@ describe('auth-route', () => {
|
|||
})).resolves.toEqual({ user })
|
||||
expect(getCurrentUser).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps unauthenticated visitors on the login page', async () => {
|
||||
const redirectAuthenticated = createRedirectAuthenticated(async () => null)
|
||||
|
||||
await expect(redirectAuthenticated({ search: { returnTo: '/dashboard/tokens' } })).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('redirects authenticated visitors to the requested local page', async () => {
|
||||
const redirectAuthenticated = createRedirectAuthenticated(async () => ({ userId: 'user-1' }))
|
||||
|
||||
await expect(redirectAuthenticated({ search: { returnTo: '/dashboard/tokens?tab=active#latest' } })).rejects.toSatisfy((error: unknown) => {
|
||||
expect(isRedirect(error)).toBe(true)
|
||||
if (!isRedirect(error)) return false
|
||||
expect(error.options.to).toBe('/dashboard/tokens?tab=active#latest')
|
||||
expect(error.options.replace).toBe(true)
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
it.each([undefined, '//example.com', '/login', '/login?returnTo=%2Flogin', '/register'])('redirects authenticated visitors to home for unusable destinations (%s)', async (returnTo) => {
|
||||
const redirectAuthenticated = createRedirectAuthenticated(async () => ({ userId: 'user-1' }))
|
||||
|
||||
await expect(redirectAuthenticated({ search: { returnTo } })).rejects.toSatisfy((error: unknown) => {
|
||||
expect(isRedirect(error)).toBe(true)
|
||||
if (!isRedirect(error)) return false
|
||||
expect(error.options.to).toBe('/')
|
||||
return true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -25,6 +25,27 @@ export function resolveAuthReturnTo(value: unknown) {
|
|||
return isSafeAuthReturnTo(value) ? value : '/'
|
||||
}
|
||||
|
||||
function resolveAuthenticatedReturnTo(value: unknown) {
|
||||
const target = resolveAuthReturnTo(value)
|
||||
try {
|
||||
const pathname = decodeURIComponent(new URL(target, 'http://localhost').pathname)
|
||||
if (/^\/(?:login|register)\/?$/i.test(pathname) || pathname.startsWith('//') || pathname.includes('\\')) {
|
||||
return '/'
|
||||
}
|
||||
} catch {
|
||||
return '/'
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
export function createRedirectAuthenticated(getCurrentUser: () => Promise<unknown>) {
|
||||
return async function redirectAuthenticated({ search }: { search: { returnTo?: string } }) {
|
||||
if (await getCurrentUser()) {
|
||||
throw redirect({ to: resolveAuthenticatedReturnTo(search.returnTo), replace: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createRequireAuth(getCurrentUser: () => Promise<unknown>) {
|
||||
return async function requireAuth({ location }: { location: RouteLocationLike }) {
|
||||
const user = await getCurrentUser()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue