Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions frontend/src/components/AuthGuard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useState, useEffect, useCallback } from "react";
import { message } from "antd";
import { LoginDialog } from "@/pages/Layout/LoginDialog";
import { SignupDialog } from "@/pages/Layout/SignupDialog";
import { post, get } from "@/utils/request";
import { useTranslation } from "react-i18next";

function loginUsingPost(data: { username: string; password: string }) {
return post("/api/user/login", data);
}

function signupUsingPost(data: { username: string; email: string; password: string }) {
return post("/api/user/signup", data);
}

export function AuthGuard() {
const { t } = useTranslation();
const [loginOpen, setLoginOpen] = useState(false);
const [signupOpen, setSignupOpen] = useState(false);
const [loading, setLoading] = useState(false);

const openLoginDialog = useCallback(() => {
console.log('[AuthGuard] openLoginDialog called, setting loginOpen to true');
setLoginOpen(true);
}, []);

const openSignupDialog = useCallback(() => {
console.log('[AuthGuard] openSignupDialog called');
setSignupOpen(true);
}, []);

useEffect(() => {
console.log('[AuthGuard] Registering show-login event listener');
window.addEventListener("show-login", openLoginDialog);

return () => {
console.log('[AuthGuard] Removing show-login event listener');
window.removeEventListener("show-login", openLoginDialog);
};
}, [openLoginDialog]);

useEffect(() => {
const session = localStorage.getItem("session");
if (!session) {
get("/api/sys-param/sys.home.page.url").catch(() => {});
}
}, []);

const handleLogin = async (values: { username: string; password: string }) => {
try {
setLoading(true);
const response = await loginUsingPost(values);
localStorage.setItem("session", JSON.stringify(response.data));
message.success(t("user.messages.loginSuccess"));
setLoginOpen(false);
window.location.reload();
} catch (error) {
console.error("Login error:", error);
message.error(t("user.messages.loginFailed"));
} finally {
setLoading(false);
}
};

const handleSignup = async (values: {
username: string;
email: string;
password: string;
confirmPassword: string;
}) => {
if (values.password !== values.confirmPassword) {
message.error(t("user.messages.passwordMismatch"));
return;
}

try {
setLoading(true);
const { username, email, password } = values;
const response = await signupUsingPost({ username, email, password });
message.success(t("user.messages.signupSuccess"));
localStorage.setItem("session", JSON.stringify(response.data));
setSignupOpen(false);
window.location.reload();
} catch (error) {
console.error("Registration error:", error);
message.error(t("user.messages.signupFailed"));
} finally {
setLoading(false);
}
};

return (
<>
<LoginDialog
open={loginOpen}
onOpenChange={setLoginOpen}
onLogin={handleLogin}
loading={loading}
onSignupClick={openSignupDialog}
/>
<SignupDialog
open={signupOpen}
onOpenChange={setSignupOpen}
onSignup={handleSignup}
loading={loading}
onLoginClick={openLoginDialog}
/>
</>
);
}

export default AuthGuard;
2 changes: 2 additions & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import router from "./routes/routes";
import { App as AntdApp, Spin, ConfigProvider } from "antd";
import "./index.css";
import TopLoadingBar from "./components/TopLoadingBar";
import AuthGuard from "./components/AuthGuard";
import { store } from "./store";
import { Provider } from "react-redux";
import theme from "./theme";
Expand Down Expand Up @@ -94,6 +95,7 @@ async function bootstrap() {
<AntdApp>
<Suspense fallback={<Spin />}>
<TopLoadingBar />
<AuthGuard />
<RouterProvider router={router} />
</Suspense>
</AntdApp>
Expand Down
35 changes: 20 additions & 15 deletions frontend/src/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ import {message} from "antd";
import Loading from "./loading";
import {errorConfigStore} from "@/utils/errorConfigStore.ts";
import i18n from "@/i18n";
import i18n from "@/i18n";
import i18n from "@/i18n";
import i18n from "@/i18n";

/**
* 通用请求工具类
Expand Down Expand Up @@ -526,11 +523,9 @@ request.addRequestInterceptor((config) => {
try {
const sessionData = JSON.parse(session);
if (sessionData.token) {
// 后端使用 "User" 请求头而不是 "Authorization"
// 可以直接发送 token 或 username
config.headers = {
...config.headers,
'User': sessionData.token, // 使用 User 请求头
'Authorization': `Bearer ${sessionData.token}`,
};
}
} catch (e) {
Expand All @@ -550,24 +545,29 @@ request.addRequestInterceptor((config) => {
// --- 常量配置 ---
const DEFAULT_ERROR_MSG = '系统繁忙,请稍后重试';
// 需要触发重新登录的 Code 集合 (包含 HTTP 401 和 业务 Token 过期码)
const AUTH_ERR_CODES = [401, '401', 'common.401'];
// 注意:后端返回的是 "common.0401"(有前导零)
const AUTH_ERR_CODES = [401, '401', 'common.401', 'common.0401'];

// --- 辅助函数:防抖处理登录失效 ---
let isRelogging = false;

const handleLoginRedirect = () => {
if (isRelogging) return;
console.log('[Auth] handleLoginRedirect called, isRelogging:', isRelogging);

if (isRelogging) {
console.log('[Auth] Skipping - already relogging');
return;
}
isRelogging = true;

// 1. 清除 Session / Token
localStorage.removeItem('session');

// 2. 触发登录弹窗事件 (根据你的架构,这里可以是 dispatch event 或 router 跳转)
const loginEvent = new CustomEvent('show-login');
window.dispatchEvent(loginEvent);
console.log('[Auth] Dispatching show-login event');
window.dispatchEvent(new CustomEvent('show-login'));

// 3. 重置标志位 (3秒后才允许再次触发)
setTimeout(() => {
isRelogging = false;
console.log('[Auth] Reset isRelogging flag');
}, 3000);
};

Expand All @@ -578,6 +578,7 @@ request.addResponseInterceptor(async (response, config) => {
}

const { status } = response;
console.log('[API Interceptor] Response status:', status, 'URL:', config?.url);

// ------------------ 修改重点开始 ------------------

Expand All @@ -588,17 +589,19 @@ request.addResponseInterceptor(async (response, config) => {
// 关键点 2: 必须用 .clone(),因为流只能读一次。读了克隆的,原版 response 还能留给外面用
// 关键点 3: 必须 await,因为读取流是异步的
resData = await response.clone().json();
console.log('[API Interceptor] Response data:', resData);
} catch (e) {
// 如果后端返回的不是 JSON (比如 404 HTML 页面,或者空字符串),json() 会报错
// 这里捕获异常,保证 resData 至少是个空对象,不会导致后面取值 crash
console.warn('响应体不是有效的JSON:', e);
console.warn('[API Interceptor] 响应体不是有效的JSON:', e);
resData = {};
}

// 2. 获取统一的错误码 (转为字符串以匹配 JSON 配置的 Key)
// 优先取后端 body 里的 business code,没有则取 HTTP status
const code = resData.code ?? status;
const codeStr = String(code);
console.log('[API Interceptor] Extracted code:', code, 'codeStr:', codeStr);

// 3. 判断成功 (根据你的后端约定:200/0 为成功)
// 如果是成功状态,直接返回 response,不拦截
Expand All @@ -623,7 +626,9 @@ request.addResponseInterceptor(async (response, config) => {
}

// 7. 处理 Token 过期 / 未登录
if (AUTH_ERR_CODES.includes(code) || AUTH_ERR_CODES.includes(codeStr)) {
const isAuthError = AUTH_ERR_CODES.includes(code) || AUTH_ERR_CODES.includes(codeStr);
console.log('[API Interceptor] Is auth error?', isAuthError, 'AUTH_ERR_CODES:', AUTH_ERR_CODES);
if (isAuthError) {
handleLoginRedirect();
}

Expand Down
Loading