From 1ac0a347170e8187bd6f57165e994083895d7a00 Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Mon, 14 Sep 2026 15:27:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20401=20redirect=20=E5=9B=9E?= =?UTF-8?q?=E8=B7=B3=E4=B8=8E=E5=90=AF=E5=8A=A8=E6=9C=9F=E8=AF=8A=E6=96=AD?= =?UTF-8?q?=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 401 统一携带 redirect 跳登录,登录成功后回到原页面(防开放跳转校验) - 并发 401 只清理会话并跳转一次 - 开发环境输出 [HTTP] 请求日志,新增 bootLog 启动横幅(API Base/Domain/Mode) --- frontend/src/components/auth/Login.tsx | 8 +++-- .../src/components/auth/ProtectedRoute.tsx | 7 +++-- frontend/src/lib/bootLog.ts | 17 +++++++++++ frontend/src/main.tsx | 4 +++ frontend/src/utils/request.ts | 29 +++++++++++++++++-- 5 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 frontend/src/lib/bootLog.ts diff --git a/frontend/src/components/auth/Login.tsx b/frontend/src/components/auth/Login.tsx index 9022711..6a581a1 100644 --- a/frontend/src/components/auth/Login.tsx +++ b/frontend/src/components/auth/Login.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { User, Lock, Palette, Check, Loader2, AlertCircle, Eye, EyeOff, Languages, Activity } from 'lucide-react'; import { useUserStore, useLanguageStore } from '@/store'; @@ -22,6 +22,7 @@ import { export function Login() { const { t } = useTranslation(); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const { theme, setTheme } = useTheme(); const { language, setLanguage } = useLanguageStore(); const [username, setUsername] = useState('admin'); @@ -38,7 +39,10 @@ export function Login() { const result = await authApi.login(username, password) as { user: { id: number; username: string; email: string; status: number }; token: string; permissions?: string[] }; useUserStore.getState().login(result.user, result.token, result.permissions ?? null); localStorage.setItem('token', result.token); - navigate('/', { replace: true }); + // 会话过期被踢到登录页时,登录成功后回到原页面;只接受站内相对路径,防开放跳转。 // Logan.Li + const redirect = searchParams.get('redirect'); + const safeRedirect = redirect && redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : '/'; + navigate(safeRedirect, { replace: true }); } catch (err) { const message = err instanceof Error ? err.message : t('login.loginFailed'); setError(message); diff --git a/frontend/src/components/auth/ProtectedRoute.tsx b/frontend/src/components/auth/ProtectedRoute.tsx index 7a05c38..8e6c2da 100644 --- a/frontend/src/components/auth/ProtectedRoute.tsx +++ b/frontend/src/components/auth/ProtectedRoute.tsx @@ -1,10 +1,13 @@ -import { Navigate } from 'react-router-dom'; +import { Navigate, useLocation } from 'react-router-dom'; import { useUserStore } from '@/store'; export function ProtectedRoute({ children }: { children: React.ReactNode }) { const isAuthenticated = useUserStore((s) => s.isAuthenticated); + const location = useLocation(); if (!isAuthenticated) { - return ; + // 携带当前路径,登录成功后回到原页面(借鉴 opsx-web-lite 的 redirect 回跳)。 // Logan.Li + const current = location.pathname + location.search; + return ; } return <>{children}; } diff --git a/frontend/src/lib/bootLog.ts b/frontend/src/lib/bootLog.ts new file mode 100644 index 0000000..adb4e12 --- /dev/null +++ b/frontend/src/lib/bootLog.ts @@ -0,0 +1,17 @@ +// 启动期控制台输出:打印 API 基础路径与运行环境信息,便于联调排查(参考 Vue 项目的启动横幅)。 // Logan.Li +import { getApiBase, getPublicApiBase } from '@/utils/request'; + +export function logBootInfo(): void { + const base = getApiBase(); + const badgeStyle = 'background-color:#f4516c;color:#fff;padding:3px;border-radius:3px'; + const textStyle = 'color:inherit'; + + // API 基础路径(baseURL 已包含 /api/v1,接口路径使用相对路径即可) // Logan.Li + console.log(`%cAIOps API Base:%c ${base}`, badgeStyle, textStyle); + + // 当前应用运行环境信息 // Logan.Li + const fullUrl = window.location.href; + const hostname = window.location.hostname; + console.log('Domain:', hostname, 'Page:', fullUrl); + console.log('Mode:', import.meta.env.MODE, ' Public API:', getPublicApiBase()); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index a94b6d2..e9bb545 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -4,11 +4,15 @@ import './index.css' // U3: i18n — 必须在 App 之前导入以初始化翻译实例 import './i18n' import { getInitialTheme, applyThemeToDocument } from '@/utils/theme' +import { logBootInfo } from '@/lib/bootLog' import App from './App.tsx' // 首屏前应用主题,避免闪烁 // Logan.Li applyThemeToDocument(getInitialTheme()) +// 启动期打印 API 基础路径与环境信息,便于联调排查 // Logan.Li +logBootInfo() + window.addEventListener('vite:preloadError', () => { // 任一左侧子菜单懒加载资源在发版后失效时,统一整页刷新获取最新 index/chunk。 // Logan.Li window.location.reload() diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index d0a2f74..e9d7f46 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -19,6 +19,22 @@ export interface ListResponse { type Json = unknown; +// 并发 401 只清理会话并跳转一次,避免多个请求同时过期时重复登出/提示(借鉴 opsx-web-lite)。 // Logan.Li +let handling401 = false; + +/** 401 统一处理:清理本地会话并携带 redirect 跳转登录页 // Logan.Li */ +function handleUnauthorized(): void { + if (handling401) return; + handling401 = true; + clearToken(); + useUserStore.getState().logout(); + const current = window.location.pathname + window.location.search; + const target = current && current !== '/login' + ? `/login?redirect=${encodeURIComponent(current)}` + : '/login'; + window.location.href = target; +} + /** 统一 API 根路径,避免 get/post 与 download/upload 行为不一致 // Logan.Li */ export function getApiBase(): string { const envBase = import.meta.env.VITE_API_BASE_URL; @@ -93,16 +109,23 @@ async function request( headers.Authorization = 'Bearer ' + token; } + if (import.meta.env.DEV) { + // 开发环境输出请求日志,便于排查 404/代理类问题(借鉴 opsx-web-lite) // Logan.Li + console.log(`[HTTP] ${method} ${url}`); + } + const response = await fetch(fullUrl, { method, headers, body: body ? JSON.stringify(body) : undefined, }); + if (import.meta.env.DEV) { + console.log(`[HTTP] ${method} ${url} → HTTP ${response.status}`); + } + if (response.status === 401) { - clearToken(); - useUserStore.getState().logout(); - window.location.href = '/login'; + handleUnauthorized(); throw new Error(i18n.t('common.requestts.sd3ab821c')); }