You've already forked DataMate
feat(auth): 完善API网关JWT认证和权限控制功能
- 实现网关侧JWT工具类和权限规则匹配器 - 集成JWT认证流程,支持Bearer Token验证 - 添加基于路径和HTTP方法的权限控制机制 - 配置白名单路由规则,优化认证性能 - 更新前端受保护路由组件,实现权限验证 - 添加403禁止访问页面和权限检查逻辑 - 重构登录页面,集成实际认证API调用 - 实现用户信息获取和权限加载功能 - 优化全局异常处理器中的认证错误状态码 - 集成FastJSON2和JJWT依赖库支持
This commit is contained in:
24
frontend/src/pages/Forbidden/ForbiddenPage.tsx
Normal file
24
frontend/src/pages/Forbidden/ForbiddenPage.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { Button, Result } from "antd";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
const ForbiddenPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="h-screen w-full flex items-center justify-center bg-[#050b14]">
|
||||
<Result
|
||||
status="403"
|
||||
title="403"
|
||||
subTitle="你当前账号没有访问该页面的权限。"
|
||||
extra={
|
||||
<Button type="primary" onClick={() => navigate("/data/management")}>
|
||||
返回首页
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForbiddenPage;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useEffect, useState } from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button, Drawer, Menu, Popover } from "antd";
|
||||
import {
|
||||
CloseOutlined,
|
||||
@@ -14,6 +14,7 @@ import SettingsPage from "../SettingsPage/SettingsPage";
|
||||
import { useAppSelector, useAppDispatch } from "@/store/hooks";
|
||||
import { showSettings, hideSettings } from "@/store/slices/settingsSlice";
|
||||
import { logout } from "@/store/slices/authSlice";
|
||||
import { hasPermission } from "@/auth/permissions";
|
||||
|
||||
const isPathMatch = (currentPath: string, targetPath: string) =>
|
||||
currentPath === targetPath || currentPath.startsWith(`${targetPath}/`);
|
||||
@@ -25,13 +26,36 @@ const AsiderAndHeaderLayout = () => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [taskCenterVisible, setTaskCenterVisible] = useState(false);
|
||||
const settingVisible = useAppSelector((state) => state.settings.visible);
|
||||
const permissions = useAppSelector((state) => state.auth.permissions);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const visibleMenuItems = useMemo(
|
||||
() =>
|
||||
menuItems
|
||||
.map((item) => ({
|
||||
...item,
|
||||
children: item.children?.filter((subItem) =>
|
||||
hasPermission(permissions, (subItem as { permissionCode?: string }).permissionCode)
|
||||
),
|
||||
}))
|
||||
.filter((item) => {
|
||||
const selfVisible = hasPermission(
|
||||
permissions,
|
||||
(item as { permissionCode?: string }).permissionCode
|
||||
);
|
||||
if (item.children && item.children.length > 0) {
|
||||
return selfVisible;
|
||||
}
|
||||
return selfVisible;
|
||||
}),
|
||||
[permissions]
|
||||
);
|
||||
|
||||
// Initialize active item based on current pathname
|
||||
const initActiveItem = useCallback(() => {
|
||||
const dataPath = pathname.startsWith("/data/") ? pathname.slice(6) : pathname;
|
||||
for (let index = 0; index < menuItems.length; index++) {
|
||||
const element = menuItems[index];
|
||||
for (let index = 0; index < visibleMenuItems.length; index++) {
|
||||
const element = visibleMenuItems[index];
|
||||
if (element.children) {
|
||||
for (const subItem of element.children) {
|
||||
if (isPathMatch(dataPath, subItem.id)) {
|
||||
@@ -44,7 +68,8 @@ const AsiderAndHeaderLayout = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
setActiveItem(visibleMenuItems[0]?.id ?? "");
|
||||
}, [pathname, visibleMenuItems]);
|
||||
|
||||
useEffect(() => {
|
||||
initActiveItem();
|
||||
@@ -100,7 +125,7 @@ const AsiderAndHeaderLayout = () => {
|
||||
<Menu
|
||||
mode="inline"
|
||||
inlineCollapsed={!sidebarOpen}
|
||||
items={menuItems.map((item) => ({
|
||||
items={visibleMenuItems.map((item) => ({
|
||||
key: item.id,
|
||||
label: item.title,
|
||||
icon: item.icon ? <item.icon className="w-4 h-4" /> : null,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
// Store,
|
||||
// Merge,
|
||||
} from "lucide-react";
|
||||
import { PermissionCodes } from "@/auth/permissions";
|
||||
|
||||
export const menuItems = [
|
||||
// {
|
||||
@@ -26,6 +27,7 @@ export const menuItems = [
|
||||
id: "management",
|
||||
title: "数集管理",
|
||||
icon: FolderOpen,
|
||||
permissionCode: PermissionCodes.dataManagementRead,
|
||||
description: "创建、导入和管理数据集",
|
||||
color: "bg-blue-500",
|
||||
},
|
||||
@@ -33,6 +35,7 @@ export const menuItems = [
|
||||
id: "annotation",
|
||||
title: "数据标注",
|
||||
icon: Tag,
|
||||
permissionCode: PermissionCodes.dataAnnotationRead,
|
||||
description: "对数据进行标注和标记",
|
||||
color: "bg-green-500",
|
||||
},
|
||||
@@ -40,6 +43,7 @@ export const menuItems = [
|
||||
id: "content-generation",
|
||||
title: "内容生成",
|
||||
icon: Sparkles,
|
||||
permissionCode: PermissionCodes.contentGenerationUse,
|
||||
description: "智能内容生成与创作",
|
||||
color: "bg-purple-500",
|
||||
},
|
||||
@@ -47,6 +51,7 @@ export const menuItems = [
|
||||
id: "knowledge-management",
|
||||
title: "知识管理",
|
||||
icon: Shield,
|
||||
permissionCode: PermissionCodes.knowledgeManagementRead,
|
||||
description: "管理知识集与知识条目",
|
||||
color: "bg-indigo-500",
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router';
|
||||
import { Form, Input, Button, Typography, message, Card } from 'antd';
|
||||
import { Form, Input, Button, Typography, message } from 'antd';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { loginLocal } from '@/store/slices/authSlice';
|
||||
import { loginUser } from '@/store/slices/authSlice';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -11,19 +11,20 @@ const LoginPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { loading, error } = useAppSelector((state) => state.auth);
|
||||
const { loading } = useAppSelector((state) => state.auth);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const from = location.state?.from?.pathname || '/data';
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
dispatch(loginLocal(values));
|
||||
// The reducer updates state synchronously.
|
||||
if (values.username === 'admin' && values.password === '123456') {
|
||||
messageApi.success('登录成功');
|
||||
navigate(from, { replace: true });
|
||||
} else {
|
||||
messageApi.error('账号或密码错误');
|
||||
const onFinish = async (values: { username: string; password: string }) => {
|
||||
try {
|
||||
await dispatch(loginUser(values)).unwrap();
|
||||
messageApi.success('登录成功');
|
||||
navigate(from, { replace: true });
|
||||
} catch (loginError) {
|
||||
const messageText =
|
||||
typeof loginError === 'string' ? loginError : '账号或密码错误';
|
||||
messageApi.error(messageText);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,9 +60,9 @@ const LoginPage: React.FC = () => {
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
<Form<{ username: string; password: string }>
|
||||
name="login"
|
||||
initialValues={{ remember: true, username: 'admin', password: '123456' }}
|
||||
initialValues={{ username: 'admin', password: '123456' }}
|
||||
onFinish={onFinish}
|
||||
layout="vertical"
|
||||
size="large"
|
||||
|
||||
Reference in New Issue
Block a user