feat: 1. 修改完善全局路由检测。 2. 完善统一的token认证管理,token失效自动跳转到登录页。

This commit is contained in:
2025-11-18 20:32:43 +08:00
parent e7b1c2e294
commit adfb84a31d
17 changed files with 270 additions and 294 deletions
+15 -1
View File
@@ -54,7 +54,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 🔑 检查是否是管理员账密登录(直接传递 token 和 userInfo
const token = url.searchParams.get("token");
const userInfo = url.searchParams.get("userInfo");
const redirectTo = url.searchParams.get("redirectTo") || "/";
// const redirectTo = url.searchParams.get("redirectTo") || "/";
// 🔑 如果有 token 和 userInfo,说明是管理员账密登录
// login.tsx action 已经创建了 Cookie Session,这里只需要返回 null
@@ -180,6 +180,19 @@ export async function loader({ request }: LoaderFunctionArgs) {
const frontendJWT = loginResponse.data.access_token;
const savedUserInfo = loginResponse.data.user_info;
// 🔑 提取后端返回的签发时间并转换为时间戳
let tokenIssuedAt = Date.now(); // 默认使用当前时间
if (loginResponse.data.issued_time) {
try {
// 后端返回格式:"2025-11-18 17:41:06"
// 转换为时间戳(毫秒)
tokenIssuedAt = new Date(loginResponse.data.issued_time.replace(' ', 'T')).getTime();
console.log("📅 [Callback] 使用后端返回的签发时间:", loginResponse.data.issued_time, "→", tokenIssuedAt);
} catch (error) {
console.warn("⚠️ [Callback] 无法解析 issued_time,使用当前时间:", error);
}
}
// 更新userInfo以包含数据库ID、JWTuser_role 从后端返回)
const enhancedUserInfo = {
...userInfo.data, // 保留OAuth返回的原字段(包含 nickname 等)
@@ -221,6 +234,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
accessToken: tokenResponse.access_token,
refreshToken: tokenResponse.refresh_token,
tokenExpiresIn: tokenResponse.expires_in,
tokenIssuedAt: tokenIssuedAt, // 🔑 传递后端返回的签发时间
userInfo: enhancedUserInfo,
frontendJWT
});
+15 -15
View File
@@ -44,30 +44,30 @@ function transformCategory(category: ContractCategoryWithCount) {
* @returns 分类数据
*/
export async function loader({ request }: LoaderFunctionArgs) {
// 获取 JWT
const { frontendJWT } = await getUserSession(request);
const jwt = frontendJWT || undefined;
try {
const url = new URL(request.url);
const { handleServerAuth } = await import("~/utils/server-auth-handler");
return handleServerAuth(async () => {
// 获取 JWT
const { frontendJWT } = await getUserSession(request);
const jwt = frontendJWT || undefined;
// 使用聚合查询获取分类及其模板数量
const categoriesResponse = await getContractCategoriesWithCount(jwt);
// 处理分类数据
if (categoriesResponse.error) {
console.error('获取分类失败:', categoriesResponse.error);
return { categories: [] };
}
// 处理分类数据
if (categoriesResponse.error) {
console.error('获取分类失败:', categoriesResponse.error);
return { categories: [] };
}
const categories = categoriesResponse.data || [];
const categories = categoriesResponse.data || [];
// 转换分类数据格式
const categoriesWithCount = categories.map(transformCategory);
return { categories: categoriesWithCount };
} catch (error) {
console.error('加载分类数据失败:', error);
return { categories: [] };
}
}, url.pathname);
}
export default function ContractTemplateSearchIndex() {
+4 -4
View File
@@ -616,10 +616,10 @@ export default function CrossCheckingResult() {
setIsLoading(false);
}
},
onCancel: () => {
// 用户取消时不需要做任何处理
console.log('[完成评查] 用户取消了确认操作');
}
// onCancel: () => {
// // 用户取消时不需要做任何处理
// console.log('[完成评查] 用户取消了确认操作');
// }
});
} catch (error) {
isProcessingRef.current = false;
+4 -5
View File
@@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from "react";
import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/node";
import { Form, useNavigate, useLoaderData } from "@remix-run/react";
import React, { useState, useRef, useEffect } from "react";
import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { useNavigate, useLoaderData } from "@remix-run/react";
import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea";
import { Button } from "~/components/ui/Button";
import { messageService } from "~/components/ui/MessageModal";
@@ -20,7 +20,6 @@ import {
getOrganizationTree,
convertToTreeData
} from "~/api/user";
import React from "react"; // Added for React.useState
import { API_BASE_URL } from '~/config/api-config';
export const meta: MetaFunction = () => {
@@ -133,7 +132,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo, frontendJWT } = await getUserSession(request);
return json({
return Response.json({
userInfo,
frontendJWT
});
+2 -2
View File
@@ -3,7 +3,7 @@ import { useSearchParams, useLoaderData, useFetcher, useNavigate,Link } from "@r
import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { Table } from "~/components/ui/Table";
// import { Table } from "~/components/ui/Table";
import { Pagination } from "~/components/ui/Pagination";
import { FileTypeTag } from "~/components/ui/FileTypeTag";
import { FileTag } from "~/components/ui/FileTag";
@@ -19,7 +19,7 @@ import { appendContractAttachments, uploadContractTemplate } from "~/api/files/f
import { toastService } from "~/components/ui/Toast";
import { messageService } from "~/components/ui/MessageModal";
import { loadingBarService } from "~/components/ui/LoadingBar";
import { DOCUMENT_URL } from "~/api/axios-client";
// import { DOCUMENT_URL } from "~/api/axios-client";
// 导入样式
export function links() {
+17 -2
View File
@@ -48,9 +48,15 @@ export const meta: MetaFunction = () => {
export async function loader({ request }: LoaderFunctionArgs) {
try {
// 从根loader获取用户角色
const { userRole, userInfo, frontendJWT } = await getUserSession(request);
const { userRole, userInfo, frontendJWT, isAuthenticated } = await getUserSession(request);
// 🔑 检查用户是否已登录且有用户信息
if (!isAuthenticated || !userInfo) {
console.warn("⚠️ [Home Loader] 用户未登录或缺少用户信息,重定向到登录页");
const url = new URL(request.url);
return Response.redirect(`/login?redirect=${encodeURIComponent(url.pathname)}`, 302);
}
// 返回默认值,实际数据将在客户端根据 sessionStorage 加载
return Response.json({
homeData: {
@@ -101,6 +107,15 @@ export default function Home() {
});
const [isLoading, setIsLoading] = useState(true);
// const userRole = serverUserRole as UserRole;
// 🔑 防御性检查:如果 userInfo 不存在,重定向到登录页(理论上不应该发生,因为 loader 已经检查了)
if (!userInfo) {
console.error("❌ [Home] userInfo 不存在,重定向到登录页");
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
return null;
}
// 打印服务器端传递的用户角色
useEffect(() => {
+40 -5
View File
@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { useLoaderData, useNavigate, useFetcher } from "@remix-run/react";
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
import { useLoaderData, useFetcher } from "@remix-run/react";
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { OAuthClient } from "~/api/login/oauth-client";
import { CLIENT_OAUTH_CONFIG } from "~/config/api-config";
import { getUserSession, getSession, createUserSession } from "~/api/login/auth.server";
import { getSession, createUserSession } from "~/api/login/auth.server";
import { loginWithPassword } from "~/api/login/login-client";
import styles from "~/styles/pages/login.css?url";
import { toastService } from "~/components/ui";
@@ -88,7 +88,7 @@ export async function action({ request }: ActionFunctionArgs) {
}, { status: 401 });
}
const { access_token, user_info } = response.data;
const { access_token, expires_in, issued_time, user_info } = response.data;
// 验证返回数据
if (!access_token) {
@@ -107,8 +107,22 @@ export async function action({ request }: ActionFunctionArgs) {
}, { status: 500 });
}
// 🔑 将后端返回的 issued_time 转换为时间戳(毫秒)
let tokenIssuedAt = Date.now(); // 默认使用当前时间
if (issued_time) {
try {
// 后端返回格式:"2025-11-18 17:41:06"
// 转换为时间戳(毫秒)
tokenIssuedAt = new Date(issued_time.replace(' ', 'T')).getTime();
console.log("📅 [Login Action] 使用后端返回的签发时间:", issued_time, "→", tokenIssuedAt);
} catch (error) {
console.warn("⚠️ [Login Action] 无法解析 issued_time,使用当前时间:", error);
}
}
console.log("✅ [Login Action] 登录成功,准备创建 session");
console.log("👤 [Login Action] 用户角色:", user_info.user_role); // 应该是 "admin"
console.log("⏰ [Login Action] Token 有效期:", expires_in, "秒 (", expires_in / 3600, "小时)");
// 获取当前 URL 用于构建 callback URL
const url = new URL(request.url);
@@ -137,6 +151,8 @@ export async function action({ request }: ActionFunctionArgs) {
userRole: user_info.user_role,
redirectTo: callbackUrl.toString(), // 先跳转到 callback 页面保存 token
frontendJWT: access_token, // 保存到 Cookie Session
tokenExpiresIn: expires_in,
tokenIssuedAt: tokenIssuedAt, // 🔑 传递后端返回的签发时间
userInfo: {
user_id: user_info.user_id,
username: user_info.username,
@@ -161,7 +177,7 @@ export async function action({ request }: ActionFunctionArgs) {
}
export default function Login() {
const navigate = useNavigate();
// const navigate = useNavigate();
const loaderData = useLoaderData<typeof loader>();
const fetcher = useFetcher<{ success: boolean; error?: string }>();
const [isFlipped, setIsFlipped] = useState(false);
@@ -268,6 +284,25 @@ export default function Login() {
}, [fetcher.data]);
useEffect(() => {
// 🔑 只在 token 过期时清理客户端存储
// 检查 URL 参数中是否有 expired=true 标识
if (typeof window !== 'undefined') {
const urlParams = new URLSearchParams(window.location.search);
const isExpired = urlParams.get('expired') === 'true';
if (isExpired) {
// 只有在因为过期被重定向时才清除 localStorage
localStorage.removeItem('access_token');
localStorage.removeItem('user_info');
console.log("🧹 [Login] Token 已过期,已清除客户端 token 数据");
// 清除 URL 中的 expired 参数,避免刷新页面时重复清除
urlParams.delete('expired');
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
window.history.replaceState({}, '', newUrl);
}
}
// 检查OAuth配置是否完整(客户端不需要检查 clientSecret
if (!CLIENT_OAUTH_CONFIG.serverUrl || !CLIENT_OAUTH_CONFIG.clientId) {
console.error("OAuth2.0配置不完整:", CLIENT_OAUTH_CONFIG);
+1 -1
View File
@@ -499,7 +499,7 @@ export default function RuleGroupsIndex() {
key: "ruleCount",
width: "12%",
render: (_: unknown, record: RuleGroup & { isParent?: boolean, parentId?: string }) => (
<button onClick={() => navigate(`/rules?${!record.isParent ? `ruleType=${record.parentId}&groupId=${record.id}` : `ruleType=${record.id}`}`)} className="badge bg-primary text-white">
<button onClick={() => navigate(`/rules/list?${!record.isParent ? `ruleType=${record.parentId}&groupId=${record.id}` : `ruleType=${record.id}`}`)} className="badge bg-primary text-white">
<span className="text-xs hover:underline">{calculateTotalRuleCount(record)}</span>
</button>
)
+4 -4
View File
@@ -504,7 +504,7 @@ export default function RulesIndex() {
// 复制评查点
const handleCopy = (rule: Rule) => {
navigate(`/rules-new?id=${rule.id}&mode=copy`);
navigate(`/rules/new?id=${rule.id}&mode=copy`);
};
const handlePageChange = (page: number) => {
@@ -618,7 +618,7 @@ export default function RulesIndex() {
{isDeveloper ? (
// 开发者可以看到编辑、复制、删除
<>
<Link to={`/rules-new?id=${record.id}`} className="operation-btn">
<Link to={`/rules/new?id=${record.id}`} className="operation-btn">
<i className="ri-edit-line"></i>
</Link>
<button className="operation-btn" onClick={() => handleCopy(record)}>
@@ -630,7 +630,7 @@ export default function RulesIndex() {
</>
) : (
// 普通用户只能查看
<Link to={`/rules-new?id=${record.id}&mode=view`} className="operation-btn">
<Link to={`/rules/new?id=${record.id}&mode=view`} className="operation-btn">
<i className="ri-eye-line"></i>
</Link>
)}
@@ -658,7 +658,7 @@ export default function RulesIndex() {
)}
</div>
{isDeveloper && (
<Button type="primary" icon="ri-add-line" to="/rules-new" className="btn-add-rule">
<Button type="primary" icon="ri-add-line" to="/rules/new" className="btn-add-rule">
</Button>
)}
@@ -68,21 +68,7 @@ export function links() {
}
export const handle = {
breadcrumb: "评查点管理",
previousRoute: () => {
if (typeof window !== 'undefined') {
const searchParams = new URLSearchParams(window.location.search);
const mode = searchParams.get('mode');
const id = searchParams.get('id');
if (mode || id) {
return {
title: "评查点列表",
to: `/rules`
};
}
}
return undefined;
}
breadcrumb: "评查点管理"
};
// 添加规则配置接口
@@ -795,7 +781,7 @@ export default function RuleNew() {
toastService.success(`评查点${isEditMode ? '更新' : '创建'}成功!`);
// 保存成功后跳转到编辑页面并重新加载数据
navigate(`/rules-new?id=${savedPointId}`, { replace: true });
navigate(`/rules/new?id=${savedPointId}`, { replace: true });
// 重新获取评查点数据
await fetchEvaluationPoint(savedPointId);
} else {
+2 -1
View File
@@ -13,7 +13,8 @@ export const meta: MetaFunction = () => {
};
export const handle = {
breadcrumb: "评查点列表"
breadcrumb: "评查点列表",
to: "/rules/list" // 指定面包屑点击后跳转的路径
};
/**