Commit 1bdbabbfe00aefae2ccec20eb23a1fa23c9ce6dd

Authored by zichun
1 parent 88bba451

feat(fe-shell): 已登录访问登录页重定向守卫 REQ-USR-004

frontend/src/router/RedirectIfAuthed.tsx 0 → 100644
  1 +// REQ-USR-004: /login 守卫(BR2)。已登录访问登录页 → 回 from 或 /。
  2 +import type { ReactNode } from 'react';
  3 +import { Navigate, useLocation } from 'react-router-dom';
  4 +import { useAppSelector } from '../store/hooks';
  5 +
  6 +interface RedirectIfAuthedProps {
  7 + children: ReactNode;
  8 +}
  9 +
  10 +/**
  11 + * 已登录态判定(D:仅 token 即视为已登录,与 RequireAuth 的「token 存在即非未登录」语义对齐;
  12 + * 避免 token 已持久化、user 尚在拉取时登录页与守卫之间互相弹跳)。
  13 + * 已登录 → 重定向到 location.state.from(来源)或 /;否则渲染 children(LoginPage)。
  14 + */
  15 +export default function RedirectIfAuthed({ children }: RedirectIfAuthedProps) {
  16 + const token = useAppSelector((s) => s.auth.token);
  17 + const location = useLocation();
  18 +
  19 + if (token) {
  20 + const from = (location.state as { from?: string } | null)?.from;
  21 + return <Navigate to={from ?? '/'} replace />;
  22 + }
  23 +
  24 + return <>{children}</>;
  25 +}
... ...
frontend/tests/unit/RedirectIfAuthed.test.tsx 0 → 100644
  1 +// REQ-USR-004: RedirectIfAuthed —— 已登录访问 /login 回主页(BR2)
  2 +import { describe, it, expect } from 'vitest';
  3 +import { screen } from '@testing-library/react';
  4 +import { Routes, Route } from 'react-router-dom';
  5 +import { renderShell } from './renderShell';
  6 +import RedirectIfAuthed from '../../src/router/RedirectIfAuthed';
  7 +
  8 +function LoginScreen() {
  9 + return <div data-testid="login-screen">login-screen</div>;
  10 +}
  11 +function HomeSentinel() {
  12 + return <div data-testid="home-sentinel">home-sentinel</div>;
  13 +}
  14 +function UsersSentinel() {
  15 + return <div data-testid="users-sentinel">users-sentinel</div>;
  16 +}
  17 +
  18 +function renderTree(
  19 + initialEntries: { pathname: string; state?: unknown }[] | string[],
  20 + preloadedAuth?: Parameters<typeof renderShell>[1]['preloadedAuth'],
  21 +) {
  22 + return renderShell(
  23 + <Routes>
  24 + <Route
  25 + path="/login"
  26 + element={
  27 + <RedirectIfAuthed>
  28 + <LoginScreen />
  29 + </RedirectIfAuthed>
  30 + }
  31 + />
  32 + <Route path="/" element={<HomeSentinel />} />
  33 + <Route path="/usr/users" element={<UsersSentinel />} />
  34 + </Routes>,
  35 + { initialEntries: initialEntries as never, preloadedAuth },
  36 + );
  37 +}
  38 +
  39 +const READY_USER = {
  40 + token: 't',
  41 + user: { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' },
  42 +};
  43 +
  44 +describe('RedirectIfAuthed', () => {
  45 + it('renders children when unauthenticated', () => {
  46 + renderTree(['/login'], { token: null, user: null });
  47 + expect(screen.getByTestId('login-screen')).toBeInTheDocument();
  48 + });
  49 +
  50 + it('redirects to / when already authenticated', () => {
  51 + renderTree(['/login'], READY_USER);
  52 + expect(screen.getByTestId('home-sentinel')).toBeInTheDocument();
  53 + expect(screen.queryByTestId('login-screen')).not.toBeInTheDocument();
  54 + });
  55 +
  56 + it('redirects to from when present', () => {
  57 + renderTree([{ pathname: '/login', state: { from: '/usr/users' } }], READY_USER);
  58 + expect(screen.getByTestId('users-sentinel')).toBeInTheDocument();
  59 + });
  60 +});
... ...