feat: 1. 添加axios全局路由拦截进行自动添加请求jwt。 2.重新整理路由表。 3. 文档列表新增版本差异对比。 4.菜单路由可访问列表通过对接接口返回,添加全局路由检测。
5. 修改统一认证登录和管理员登录是通过接口形式进行,存储返回的accessToken。 6. 修改交叉评查的部分样式
This commit is contained in:
+137
-67
@@ -20,6 +20,8 @@ import { Layout } from "~/components/layout/Layout";
|
||||
import { ErrorBoundary as AppErrorBoundary } from "~/components/error/ErrorBoundary";
|
||||
import { MessageModalProvider } from "~/components/ui/MessageModal";
|
||||
import { ToastProvider } from "~/components/ui/Toast";
|
||||
import { ClientAuthGuard } from "~/components/auth/ClientAuthGuard";
|
||||
import { getAccessToken } from "./utils/auth-storage";
|
||||
import "remixicon/fonts/remixicon.css";
|
||||
// 导入样式
|
||||
import styles from "~/styles/main.css?url";
|
||||
@@ -32,25 +34,66 @@ import RouteChangeLoader from "~/components/ui/RouteChangeLoader";
|
||||
|
||||
// 导入认证相关的服务器端功能(仅在服务器端使用)
|
||||
import {
|
||||
getUserSession,
|
||||
getSession,
|
||||
sessionStorage,
|
||||
// getUserSession,
|
||||
logout,
|
||||
type UserRole
|
||||
} from "~/api/login/auth.server";
|
||||
|
||||
// 定义需要高级权限的路径
|
||||
export const developerOnlyPaths = [
|
||||
'/settings',
|
||||
'/config-lists',
|
||||
'/document-types',
|
||||
'/prompts',
|
||||
];
|
||||
// export const developerOnlyPaths = [
|
||||
// '/settings',
|
||||
// '/config-lists',
|
||||
// '/document-types',
|
||||
// '/prompts',
|
||||
// ];
|
||||
|
||||
// 导出类型供客户端使用
|
||||
export type { UserRole };
|
||||
|
||||
// 辅助函数:从 MenuItem 数组中提取所有路径(包括子路由)
|
||||
interface MenuItem {
|
||||
path: string;
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
function extractAllPaths(menuItems: MenuItem[]): string[] {
|
||||
const paths: string[] = [];
|
||||
|
||||
function traverse(items: MenuItem[]) {
|
||||
for (const item of items) {
|
||||
paths.push(item.path);
|
||||
if (item.children && item.children.length > 0) {
|
||||
traverse(item.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(menuItems);
|
||||
return paths;
|
||||
}
|
||||
|
||||
// 辅助函数:检查路径是否在允许列表中
|
||||
function isPathAllowed(pathname: string, allowedPaths: string[]): boolean {
|
||||
// 精确匹配
|
||||
if (allowedPaths.includes(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 前缀匹配(处理动态路由和子路由)
|
||||
// 例如:allowedPaths 包含 '/documents',则 '/documents/123' 也应该被允许
|
||||
for (const allowedPath of allowedPaths) {
|
||||
if (pathname.startsWith(allowedPath + '/')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 根路径特殊处理
|
||||
if (pathname === '/' || pathname === '/home') {
|
||||
return true; // 首页通常对所有已登录用户开放
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 添加action处理登录/登出请求
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
@@ -70,89 +113,98 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
// 排除不需要登录验证的路径
|
||||
// 排除不需要登录验证的路径(公共路径)
|
||||
const publicPaths = ['/login', '/favicon.ico', '/callback'];
|
||||
const isPublicPath = publicPaths.some(path => pathname.startsWith(path));
|
||||
|
||||
// 获取用户会话(可能包含刷新后的token)
|
||||
const { isAuthenticated, userRole, refreshedSession, frontendJWT } = await getUserSession(request);
|
||||
// console.log("是否公开路径:", isPublicPath, "是否已认证:", isAuthenticated);
|
||||
// 获取用户角色和 JWT(从 Cookie Session)
|
||||
let userRole: UserRole = 'common'; // 默认为普通用户
|
||||
let frontendJWT: string | null = null;
|
||||
let allowedPaths: string[] = []; // 用户允许访问的路由列表
|
||||
|
||||
// 如果访问需要认证的路径但未登录,重定向到登录页
|
||||
if (!isPublicPath && !isAuthenticated) {
|
||||
console.log("未认证,需要重定向到登录页");
|
||||
// 保存请求的URL,以便登录后重定向回来
|
||||
const session = await getSession(request);
|
||||
if (!isPublicPath) {
|
||||
try {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const session = await getUserSession(request);
|
||||
userRole = session.userRole || 'common';
|
||||
frontendJWT = session.frontendJWT || null;
|
||||
// console.log("🔑 [Root Loader] 用户角色:", userRole);
|
||||
|
||||
// 如果路径是/home,则将重定向目标设置为/
|
||||
const redirectTarget = pathname !== "/" ? "/" : pathname;
|
||||
// const redirectTarget = pathname === "home" ? "/" : pathname;
|
||||
// 保存重定向目标
|
||||
session.set("redirectTo", redirectTarget);
|
||||
// 🔒 RBAC 路由权限检查
|
||||
if (frontendJWT) {
|
||||
const { getUserRoutesByRole } = await import("~/api/auth/user-routes");
|
||||
const routesResult = await getUserRoutesByRole(userRole, frontendJWT);
|
||||
|
||||
return redirect("/login", {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.commitSession(session),
|
||||
},
|
||||
});
|
||||
if (routesResult.success && routesResult.data) {
|
||||
// 从菜单数据中提取所有允许的路径
|
||||
allowedPaths = extractAllPaths(routesResult.data);
|
||||
// console.log("🔑 [Root Loader] 用户允许的路由:", allowedPaths);
|
||||
|
||||
// 检查当前路径是否在允许列表中
|
||||
const isAllowedPath = isPathAllowed(pathname, allowedPaths);
|
||||
|
||||
if (!isAllowedPath) {
|
||||
console.warn(`⚠️ [Root Loader] 用户尝试访问未授权路由: ${pathname}`);
|
||||
// 返回 403 错误,而不是 redirect(避免循环)
|
||||
throw new Response("无权访问此页面", { status: 403 });
|
||||
}
|
||||
} else {
|
||||
// 获取路由权限失败,只记录警告,不阻止访问(避免影响正常使用)
|
||||
console.warn("⚠️ [Root Loader] 获取用户路由权限失败,跳过权限检查");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果是 Response 对象(403 错误),直接抛出
|
||||
if (error instanceof Response) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 🔑 检查是否是 AuthenticationError(token 过期)
|
||||
if (error instanceof Error && error.name === 'AuthenticationError') {
|
||||
console.warn("⚠️ [Root Loader] Token 过期,重定向到登录页");
|
||||
// 保存当前路径,登录后可以跳转回来
|
||||
const redirectTo = pathname !== '/login' ? pathname : '/';
|
||||
return redirect(`/login?redirect=${encodeURIComponent(redirectTo)}`);
|
||||
}
|
||||
|
||||
console.warn("⚠️ [Root Loader] 获取用户会话失败:", error);
|
||||
// 保持默认值 'common'
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已登录且访问登录页,重定向到首页
|
||||
if (pathname === "/login" && isAuthenticated) {
|
||||
console.log("已认证,重定向到首页");
|
||||
return redirect("/");
|
||||
}
|
||||
// ⚠️ 重要:现在使用客户端 localStorage 存储 token,服务端不再检查 session
|
||||
// 认证检查改为在客户端进行(通过 ClientAuthGuard 组件)
|
||||
|
||||
// 检查访问权限 - 如果是common用户访问了开发者专属页面,重定向到首页
|
||||
if (userRole === 'common' && developerOnlyPaths.some(path => pathname.startsWith(path))) {
|
||||
console.log("用户没有访问权限,重定向到首页");
|
||||
return redirect("/");
|
||||
if (!frontendJWT) {
|
||||
frontendJWT = getAccessToken();
|
||||
}
|
||||
|
||||
// 检查51707端口访问控制
|
||||
|
||||
// 由于应用直接运行在51707端口,我们需要从环境变量或运行时获取端口
|
||||
const currentPort = process.env.PORT || process.env.API_PORT_CONFIG;
|
||||
// console.log("currentPort-----------",currentPort)
|
||||
|
||||
// 获取运行时端口(从请求URL或环境变量)
|
||||
const runtimePort = url.port || currentPort;
|
||||
// console.log("runtimePort-----------",runtimePort)
|
||||
|
||||
const isPort51707 = currentPort === '51707' || runtimePort === '51707';
|
||||
|
||||
|
||||
if (isPort51707 && !isPublicPath) {
|
||||
// 51707端口(省局)只允许访问交叉评查相关路径和首页
|
||||
const allowedPaths = ['/', '/cross-checking','/chat-with-llm'];
|
||||
const isAllowedPath = allowedPaths.some(path => pathname === path) ||
|
||||
const isAllowedPath = allowedPaths.some(path => pathname === path) ||
|
||||
pathname.startsWith('/cross-checking/') ||
|
||||
pathname.startsWith('/chat-with-llm/');
|
||||
|
||||
|
||||
if (!isAllowedPath) {
|
||||
// console.log("51707端口访问受限,重定向到交叉评查页面");
|
||||
return redirect("/cross-checking");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果token被刷新了,需要在响应中设置更新后的cookie
|
||||
const responseHeaders: Record<string, string> = {};
|
||||
if (refreshedSession) {
|
||||
responseHeaders["Set-Cookie"] = await sessionStorage.commitSession(refreshedSession);
|
||||
}
|
||||
|
||||
// 向组件传递认证状态、当前路径
|
||||
// 注意:不再传递 Dify 相关环境变量,客户端改为调用 Remix API routes
|
||||
// 向组件传递路径信息
|
||||
return Response.json({
|
||||
isAuthenticated,
|
||||
userRole,
|
||||
userRole, // ✅ 返回真实的用户角色
|
||||
pathname,
|
||||
frontendJWT,
|
||||
isPublicPath, // 传递给客户端,用于判断是否需要认证
|
||||
ENV: {
|
||||
// 移除 NEXT_PUBLIC_API_URL, NEXT_PUBLIC_APP_ID, NEXT_PUBLIC_APP_KEY
|
||||
// 客户端不再需要直接调用 Dify API
|
||||
},
|
||||
}, {
|
||||
headers: responseHeaders
|
||||
});
|
||||
}
|
||||
|
||||
@@ -183,7 +235,7 @@ export function links() {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { userRole, ENV, frontendJWT } = useLoaderData<typeof loader>();
|
||||
const { userRole, ENV, frontendJWT, isPublicPath } = useLoaderData<typeof loader>();
|
||||
|
||||
|
||||
return (
|
||||
@@ -198,7 +250,7 @@ export default function App() {
|
||||
--color-primary-hover: #005a3f;
|
||||
--color-primary-light: rgba(0, 104, 74, 0.1);
|
||||
--primary-color: #00684a;
|
||||
|
||||
|
||||
/* 成功、警告、错误颜色 */
|
||||
--color-success: #52c41a;
|
||||
--color-warning: #faad14;
|
||||
@@ -216,9 +268,17 @@ export default function App() {
|
||||
<body className="font-sans">
|
||||
<MessageModalProvider>
|
||||
<ToastProvider>
|
||||
<Layout userRole={userRole} frontendJWT={frontendJWT}>
|
||||
{/* 客户端认证守卫 - 在客户端检查 localStorage 中的 token */}
|
||||
<ClientAuthGuard isPublicPath={isPublicPath} />
|
||||
|
||||
{/* 🔑 公共路径(登录页、回调页)不显示 Layout,直接渲染内容 */}
|
||||
{isPublicPath ? (
|
||||
<Outlet />
|
||||
</Layout>
|
||||
) : (
|
||||
<Layout userRole={userRole} frontendJWT={frontendJWT}>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
)}
|
||||
<RouteChangeLoader />
|
||||
</ToastProvider>
|
||||
</MessageModalProvider>
|
||||
@@ -236,10 +296,20 @@ export function ErrorBoundary() {
|
||||
// 为错误页面设置标题和描述
|
||||
let title = "发生错误";
|
||||
let message = "发生了一个未知错误,请稍后重试";
|
||||
let statusText = "服务器错误";
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
title = `错误 ${error.status}`;
|
||||
message = error.data?.message || "发生了一个错误,请稍后重试";
|
||||
|
||||
// 特殊处理 403 错误
|
||||
if (error.status === 403) {
|
||||
title = "访问被拒绝";
|
||||
message = typeof error.data === 'string' ? error.data : "您没有权限访问此页面,请联系管理员获取相应权限";
|
||||
statusText = "无权访问";
|
||||
} else {
|
||||
message = error.data?.message || error.data || "发生了一个错误,请稍后重试";
|
||||
statusText = error.statusText || "错误";
|
||||
}
|
||||
} else {
|
||||
title = "意外错误";
|
||||
message = "服务器发生了意外错误,请稍后重试";
|
||||
@@ -257,7 +327,7 @@ export function ErrorBoundary() {
|
||||
<body>
|
||||
<AppErrorBoundary
|
||||
status={isRouteErrorResponse(error) ? error.status : 500}
|
||||
statusText={isRouteErrorResponse(error) ? error.statusText : "服务器错误"}
|
||||
statusText={statusText}
|
||||
message={message}
|
||||
/>
|
||||
<Scripts />
|
||||
|
||||
Reference in New Issue
Block a user