feat(frontend): 401 redirect 回跳与启动期诊断输出

- 401 统一携带 redirect 跳登录,登录成功后回到原页面(防开放跳转校验)
- 并发 401 只清理会话并跳转一次
- 开发环境输出 [HTTP] 请求日志,新增 bootLog 启动横幅(API Base/Domain/Mode)
This commit is contained in:
Logan.Li committed 2026-09-14 15:27:54 +08:00
1 parent 373c1c359f
commit 1ac0a34717
5 files changed
+58 -7

No files matched your search

+6 -2
View File
@@ -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);
@@ -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 <Navigate to="/login" replace />;
// 携带当前路径,登录成功后回到原页面(借鉴 opsx-web-lite 的 redirect 回跳)。 // Logan.Li
const current = location.pathname + location.search;
return <Navigate to={current !== '/login' ? `/login?redirect=${encodeURIComponent(current)}` : '/login'} replace />;
}
return <>{children}</>;
}
+17
View File
@@ -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());
}
+4
View File
@@ -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()
+26 -3
View File
@@ -19,6 +19,22 @@ export interface ListResponse<T = unknown> {
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<T = any>(
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'));
}