From 1bdbabbfe00aefae2ccec20eb23a1fa23c9ce6dd Mon Sep 17 00:00:00 2001 From: zichun Date: Mon, 1 Jun 2026 16:57:19 +0800 Subject: [PATCH] feat(fe-shell): 已登录访问登录页重定向守卫 REQ-USR-004 --- frontend/src/router/RedirectIfAuthed.tsx | 25 +++++++++++++++++++++++++ frontend/tests/unit/RedirectIfAuthed.test.tsx | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 0 deletions(-) create mode 100644 frontend/src/router/RedirectIfAuthed.tsx create mode 100644 frontend/tests/unit/RedirectIfAuthed.test.tsx diff --git a/frontend/src/router/RedirectIfAuthed.tsx b/frontend/src/router/RedirectIfAuthed.tsx new file mode 100644 index 0000000..29d4823 --- /dev/null +++ b/frontend/src/router/RedirectIfAuthed.tsx @@ -0,0 +1,25 @@ +// REQ-USR-004: /login 守卫(BR2)。已登录访问登录页 → 回 from 或 /。 +import type { ReactNode } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { useAppSelector } from '../store/hooks'; + +interface RedirectIfAuthedProps { + children: ReactNode; +} + +/** + * 已登录态判定(D:仅 token 即视为已登录,与 RequireAuth 的「token 存在即非未登录」语义对齐; + * 避免 token 已持久化、user 尚在拉取时登录页与守卫之间互相弹跳)。 + * 已登录 → 重定向到 location.state.from(来源)或 /;否则渲染 children(LoginPage)。 + */ +export default function RedirectIfAuthed({ children }: RedirectIfAuthedProps) { + const token = useAppSelector((s) => s.auth.token); + const location = useLocation(); + + if (token) { + const from = (location.state as { from?: string } | null)?.from; + return ; + } + + return <>{children}; +} diff --git a/frontend/tests/unit/RedirectIfAuthed.test.tsx b/frontend/tests/unit/RedirectIfAuthed.test.tsx new file mode 100644 index 0000000..f1debfc --- /dev/null +++ b/frontend/tests/unit/RedirectIfAuthed.test.tsx @@ -0,0 +1,60 @@ +// REQ-USR-004: RedirectIfAuthed —— 已登录访问 /login 回主页(BR2) +import { describe, it, expect } from 'vitest'; +import { screen } from '@testing-library/react'; +import { Routes, Route } from 'react-router-dom'; +import { renderShell } from './renderShell'; +import RedirectIfAuthed from '../../src/router/RedirectIfAuthed'; + +function LoginScreen() { + return
login-screen
; +} +function HomeSentinel() { + return
home-sentinel
; +} +function UsersSentinel() { + return
users-sentinel
; +} + +function renderTree( + initialEntries: { pathname: string; state?: unknown }[] | string[], + preloadedAuth?: Parameters[1]['preloadedAuth'], +) { + return renderShell( + + + + + } + /> + } /> + } /> + , + { initialEntries: initialEntries as never, preloadedAuth }, + ); +} + +const READY_USER = { + token: 't', + user: { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' }, +}; + +describe('RedirectIfAuthed', () => { + it('renders children when unauthenticated', () => { + renderTree(['/login'], { token: null, user: null }); + expect(screen.getByTestId('login-screen')).toBeInTheDocument(); + }); + + it('redirects to / when already authenticated', () => { + renderTree(['/login'], READY_USER); + expect(screen.getByTestId('home-sentinel')).toBeInTheDocument(); + expect(screen.queryByTestId('login-screen')).not.toBeInTheDocument(); + }); + + it('redirects to from when present', () => { + renderTree([{ pathname: '/login', state: { from: '/usr/users' } }], READY_USER); + expect(screen.getByTestId('users-sentinel')).toBeInTheDocument(); + }); +}); -- libgit2 0.22.2