feat(rbac): F3 前端 RBAC 按钮级控制
后端: - 新增 GET /user/permissions 端点返回当前用户权限码 - Login 响应附带 permissions 数组 - collectUserPermissionCodes 用 raw SQL JOIN 避免 GORM many2many 表名问题 - admin 用户返回 ['*'] 通配符 前端: - usePermission hook (hasPermission/hasAnyPermission/hasAllPermissions) - <HasPermission> 组件 (permission/anyOf/allOf + fallback) - zustand store 新增 permissions 字段 - Sidebar 菜单递归过滤 (42 菜单项全部标注 permission) - ServerManager: 添加/导入/批量删除按钮按权限显隐 - ServerTableRow: 终端/编辑/文件/删除按权限显隐 - SystemUserTab: 新建/导入/编辑/删除按钮按权限显隐 - MainLayout: 页面刷新自动拉取权限 - Login: 登录时存储 permissions 验证: - admin: permissions=['*'] 通配符 - viewer: 7 个只读权限 (dashboard:view/server:view/...) - viewer 无 server:create/delete - 10/10 回归测试通过 - npm run build 零错误
This commit is contained in:
1 parent
77c234b060
commit
be2a377f01
30 files changed
+2328
-462
No files matched your search
@@ -1,7 +1,8 @@
|
||||
import { useEffect} from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useThemeStore, useMenuStore } from '@/store';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import {
|
||||
|
||||
LayoutDashboard,
|
||||
@@ -50,113 +51,116 @@ interface MenuItem {
|
||||
icon: React.ElementType;
|
||||
path?: string;
|
||||
children?: MenuItem[];
|
||||
/** RBAC 权限码 — 用户需拥有此权限才显示该菜单项;不填=所有人可见 */
|
||||
permission?: string;
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{ id: 'dashboard', name: '仪表盘', icon: LayoutDashboard, path: '/' },
|
||||
{ id: 'dashboard', name: '仪表盘', icon: LayoutDashboard, path: '/', permission: 'dashboard:view' },
|
||||
{
|
||||
id: 'assets', name: '资产管理', icon: Server,
|
||||
id: 'assets', name: '资产管理', icon: Server, permission: 'server:view',
|
||||
children: [
|
||||
{ id: 'servers', name: '服务器管理', icon: Database, path: '/servers' },
|
||||
{ id: 'cloud-accounts', name: '云账号管理', icon: Key, path: '/cloud-accounts' },
|
||||
{ id: 'domains', name: '域名管理', icon: Globe, path: '/domains' },
|
||||
{ id: 'regions', name: '区域管理', icon: MapPin, path: '/regions' },
|
||||
{ id: 'platforms', name: '云平台', icon: CloudCog, path: '/platforms' },
|
||||
{ id: 'cloud-billing', name: '云账单', icon: BarChart3, path: '/cloud-billing' },
|
||||
{ id: 'servers', name: '服务器管理', icon: Database, path: '/servers', permission: 'server:view' },
|
||||
{ id: 'cloud-accounts', name: '云账号管理', icon: Key, path: '/cloud-accounts', permission: 'cloud:account' },
|
||||
{ id: 'domains', name: '域名管理', icon: Globe, path: '/domains', permission: 'cloud:domain' },
|
||||
{ id: 'regions', name: '区域管理', icon: MapPin, path: '/regions', permission: 'cloud:account' },
|
||||
{ id: 'platforms', name: '云平台', icon: CloudCog, path: '/platforms', permission: 'cloud:account' },
|
||||
{ id: 'cloud-billing', name: '云账单', icon: BarChart3, path: '/cloud-billing', permission: 'cloud:billing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'kubernetes', name: 'Kubernetes', icon: Cloud,
|
||||
id: 'kubernetes', name: 'Kubernetes', icon: Cloud, permission: 'k8s:cluster',
|
||||
children: [
|
||||
{ id: 'k8s-clusters', name: '集群列表', icon: Cloud, path: '/kubernetes' },
|
||||
{ id: 'k8s-nodes', name: '节点管理', icon: Cpu, path: '/nodes' },
|
||||
{ id: 'k8s-clusters', name: '集群列表', icon: Cloud, path: '/kubernetes', permission: 'k8s:cluster' },
|
||||
{ id: 'k8s-nodes', name: '节点管理', icon: Cpu, path: '/nodes', permission: 'k8s:nodes' },
|
||||
{
|
||||
id: 'k8s-workload', name: '工作负载', icon: Boxes,
|
||||
id: 'k8s-workload', name: '工作负载', icon: Boxes, permission: 'k8s:workloads',
|
||||
children: [
|
||||
{ id: 'k8s-deployments', name: 'Deployments', icon: Boxes, path: '/workloads/deployments' },
|
||||
{ id: 'k8s-statefulsets', name: 'StatefulSets', icon: Boxes, path: '/workloads/statefulsets' },
|
||||
{ id: 'k8s-daemonsets', name: 'DaemonSets', icon: Boxes, path: '/workloads/daemonsets' },
|
||||
{ id: 'k8s-cronjobs', name: 'CronJobs', icon: Clock, path: '/workloads/cronjobs' },
|
||||
{ id: 'k8s-pods', name: 'Pods', icon: Container, path: '/workloads/pods' },
|
||||
{ id: 'k8s-deployments', name: 'Deployments', icon: Boxes, path: '/workloads/deployments', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-statefulsets', name: 'StatefulSets', icon: Boxes, path: '/workloads/statefulsets', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-daemonsets', name: 'DaemonSets', icon: Boxes, path: '/workloads/daemonsets', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-cronjobs', name: 'CronJobs', icon: Clock, path: '/workloads/cronjobs', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-pods', name: 'Pods', icon: Container, path: '/workloads/pods', permission: 'k8s:workloads' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'k8s-network', name: '网络', icon: Network,
|
||||
id: 'k8s-network', name: '网络', icon: Network, permission: 'k8s:workloads',
|
||||
children: [
|
||||
{ id: 'k8s-services', name: 'Services', icon: Network, path: '/workloads/services' },
|
||||
{ id: 'k8s-ingresses', name: 'Ingresses', icon: Globe, path: '/workloads/ingresses' },
|
||||
{ id: 'k8s-services', name: 'Services', icon: Network, path: '/workloads/services', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-ingresses', name: 'Ingresses', icon: Globe, path: '/workloads/ingresses', permission: 'k8s:workloads' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'k8s-config', name: '配置中心', icon: Settings,
|
||||
id: 'k8s-config', name: '配置中心', icon: Settings, permission: 'k8s:workloads',
|
||||
children: [
|
||||
{ id: 'k8s-configmaps', name: 'ConfigMaps', icon: FileCode, path: '/workloads/configmaps' },
|
||||
{ id: 'k8s-secrets', name: 'Secrets', icon: Shield, path: '/workloads/secrets' },
|
||||
{ id: 'k8s-configmaps', name: 'ConfigMaps', icon: FileCode, path: '/workloads/configmaps', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-secrets', name: 'Secrets', icon: Shield, path: '/workloads/secrets', permission: 'k8s:workloads' },
|
||||
],
|
||||
},
|
||||
{ id: 'k8s-storage', name: '存储', icon: Database, path: '/workloads/pvcs' },
|
||||
{ id: 'k8s-metrics', name: '资源用量', icon: Activity, path: '/workloads/metrics' },
|
||||
{ id: 'k8s-events', name: '事件', icon: Bell, path: '/workloads/events' },
|
||||
{ id: 'k8s-capacity', name: '容量分析', icon: BarChart3, path: '/workloads/capacity' },
|
||||
{ id: 'k8s-topology', name: '架构拓扑', icon: Network, path: '/topology' },
|
||||
{ id: 'k8s-storage', name: '存储', icon: Database, path: '/workloads/pvcs', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-metrics', name: '资源用量', icon: Activity, path: '/workloads/metrics', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-events', name: '事件', icon: Bell, path: '/workloads/events', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-capacity', name: '容量分析', icon: BarChart3, path: '/workloads/capacity', permission: 'k8s:workloads' },
|
||||
{ id: 'k8s-topology', name: '架构拓扑', icon: Network, path: '/topology', permission: 'k8s:topology' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'deploy', name: '应用发布', icon: Rocket,
|
||||
id: 'deploy', name: '应用发布', icon: Rocket, permission: 'deploy:pipeline',
|
||||
children: [
|
||||
{ id: 'pipelines', name: '流水线', icon: GitBranch, path: '/pipelines' },
|
||||
{ id: 'deploy-credentials', name: '发布配置', icon: Key, path: '/deploy/configureTools' },
|
||||
{ id: 'deploy-rbac', name: '发布权限', icon: Shield, path: '/deploy/rbac' },
|
||||
{ id: 'pipelines', name: '流水线', icon: GitBranch, path: '/pipelines', permission: 'deploy:pipeline' },
|
||||
{ id: 'deploy-credentials', name: '发布配置', icon: Key, path: '/deploy/configureTools', permission: 'deploy:release' },
|
||||
{ id: 'deploy-rbac', name: '发布权限', icon: Shield, path: '/deploy/rbac', permission: 'deploy:release' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'monitor', name: '监控中心', icon: Bell,
|
||||
id: 'monitor', name: '监控中心', icon: Bell, permission: 'monitor:alert',
|
||||
children: [
|
||||
{ id: 'grafana', name: '监控大屏', icon: Monitor, path: '/dashboards' },
|
||||
{ id: 'monitor-center', name: '资源监控', icon: Activity, path: '/monitor-center' },
|
||||
{ id: 'trace', name: '链路追踪', icon: GitBranch, path: '/trace' },
|
||||
{ id: 'alert-center', name: '告警中心', icon: Bell, path: '/alert-center' },
|
||||
{ id: 'datasources', name: '数据源', icon: Database, path: '/datasources' },
|
||||
{ id: 'grafana', name: '监控大屏', icon: Monitor, path: '/dashboards', permission: 'monitor:alert' },
|
||||
{ id: 'monitor-center', name: '资源监控', icon: Activity, path: '/monitor-center', permission: 'agent:monitor' },
|
||||
{ id: 'trace', name: '链路追踪', icon: GitBranch, path: '/trace', permission: 'monitor:alert' },
|
||||
{ id: 'alert-center', name: '告警中心', icon: Bell, path: '/alert-center', permission: 'monitor:alert' },
|
||||
{ id: 'datasources', name: '数据源', icon: Database, path: '/datasources', permission: 'monitor:alert' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'automation', name: '自动化运维', icon: PlayCircle,
|
||||
id: 'automation', name: '自动化运维', icon: PlayCircle, permission: 'auto:cron',
|
||||
children: [
|
||||
{ id: 'cronjobs', name: '定时任务', icon: PlayCircle, path: '/cronjobs' },
|
||||
{ id: 'scripts', name: '脚本库', icon: FileCode, path: '/scripts' },
|
||||
{ id: 'ansible', name: '远程执行', icon: Terminal, path: '/ansible' },
|
||||
{ id: 'cronjobs', name: '定时任务', icon: PlayCircle, path: '/cronjobs', permission: 'auto:cron' },
|
||||
{ id: 'scripts', name: '脚本库', icon: FileCode, path: '/scripts', permission: 'auto:script' },
|
||||
{ id: 'ansible', name: '远程执行', icon: Terminal, path: '/ansible', permission: 'auto:ansible' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'aiops', name: 'AIOps智能', icon: Brain,
|
||||
id: 'aiops', name: 'AIOps智能', icon: Brain, permission: 'aiops:assistant',
|
||||
children: [
|
||||
{ id: 'ai-assistant', name: '智能助手', icon: Bot, path: '/ai-assistant' },
|
||||
{ id: 'ai-alert-analyzer', name: '告警分析', icon: AlertTriangle, path: '/ai-alert-analyzer' },
|
||||
{ id: 'ai-predictive', name: '容量预测', icon: BarChart3, path: '/ai-predictive' },
|
||||
{ id: 'ai-assistant', name: '智能助手', icon: Bot, path: '/ai-assistant', permission: 'aiops:assistant' },
|
||||
{ id: 'ai-alert-analyzer', name: '告警分析', icon: AlertTriangle, path: '/ai-alert-analyzer', permission: 'aiops:analysis' },
|
||||
{ id: 'ai-predictive', name: '容量预测', icon: BarChart3, path: '/ai-predictive', permission: 'aiops:forecast' },
|
||||
],
|
||||
},
|
||||
{ id: 'approval', name: '资源申请', icon: CheckSquare,
|
||||
{ id: 'approval', name: '资源申请', icon: CheckSquare, permission: 'approval:apply',
|
||||
children: [
|
||||
{ id: 'approval-resource', name: '资源申请', icon: Server, path: '/approval/resource' },
|
||||
{ id: 'approval-app', name: '应用申请', icon: Rocket, path: '/approval/app' },
|
||||
{ id: 'approval-permission', name: '权限审批', icon: ShieldCheck, path: '/approval/permission' },
|
||||
{ id: 'approval-resource', name: '资源申请', icon: Server, path: '/approval/resource', permission: 'approval:apply' },
|
||||
{ id: 'approval-app', name: '应用申请', icon: Rocket, path: '/approval/app', permission: 'approval:apply' },
|
||||
{ id: 'approval-permission', name: '权限审批', icon: ShieldCheck, path: '/approval/permission', permission: 'approval:apply' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'audit', name: '安全审计', icon: FileSearch,
|
||||
id: 'audit', name: '安全审计', icon: FileSearch, permission: 'audit:terminal',
|
||||
children: [
|
||||
{ id: 'audit-terminal', name: '终端审计', icon: Terminal, path: '/audit/terminal' },
|
||||
{ id: 'audit-operations', name: '操作审计', icon: FileCode, path: '/audit/operations' },
|
||||
{ id: 'audit-login', name: '登录日志', icon: LogIn, path: '/audit/login-logs' },
|
||||
{ id: 'audit-terminal', name: '终端审计', icon: Terminal, path: '/audit/terminal', permission: 'audit:terminal' },
|
||||
{ id: 'audit-operations', name: '操作审计', icon: FileCode, path: '/audit/operations', permission: 'audit:operations' },
|
||||
{ id: 'audit-login', name: '登录日志', icon: LogIn, path: '/audit/login-logs', permission: 'audit:login-logs' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'system', name: '系统管理', icon: Settings,
|
||||
id: 'system', name: '系统管理', icon: Settings, permission: 'system:user',
|
||||
children: [
|
||||
{ id: 'users', name: '用户管理', icon: Users, path: '/users' },
|
||||
{ id: 'roles', name: '角色权限', icon: Shield, path: '/roles' },
|
||||
{ id: 'credentials', name: '凭证配置', icon: Shield, path: '/credentials' },
|
||||
{ id: 'settings', name: '系统设置', icon: Settings, path: '/settings' },
|
||||
{ id: 'users', name: '用户管理', icon: Users, path: '/users', permission: 'system:user' },
|
||||
{ id: 'roles', name: '角色权限', icon: Shield, path: '/roles', permission: 'system:role' },
|
||||
{ id: 'credentials', name: '凭证配置', icon: Shield, path: '/credentials', permission: 'system:user' },
|
||||
{ id: 'approval-workflow', name: '审批流程', icon: GitBranch, path: '/approval-workflow', permission: 'system:role' },
|
||||
{ id: 'settings', name: '系统设置', icon: Settings, path: '/settings', permission: 'system:user' },
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -321,6 +325,7 @@ function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number
|
||||
*/
|
||||
export function Sidebar() {
|
||||
const { sidebarCollapsed, toggleSidebar } = useThemeStore();
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
// Listen for keyboard shortcut
|
||||
useEffect(() => {
|
||||
@@ -329,6 +334,20 @@ export function Sidebar() {
|
||||
return () => window.removeEventListener('toggle-sidebar' as any, handler);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// F3: 按权限过滤菜单(递归)
|
||||
// 父级菜单:自身权限通过 且 至少有一个子菜单可见
|
||||
const filterMenu = (items: MenuItem[]): MenuItem[] => {
|
||||
return items
|
||||
.filter((item) => !item.permission || hasPermission(item.permission))
|
||||
.map((item) => {
|
||||
if (!item.children) return item;
|
||||
const visibleChildren = filterMenu(item.children);
|
||||
return { ...item, children: visibleChildren };
|
||||
})
|
||||
.filter((item) => !item.children || item.children.length > 0);
|
||||
};
|
||||
const visibleMenuItems = filterMenu(menuItems);
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
@@ -365,7 +384,7 @@ export function Sidebar() {
|
||||
{/* Menu Items */}
|
||||
<ScrollArea className="flex-1 py-2 px-1.5">
|
||||
<nav className="space-y-0.5">
|
||||
{menuItems.map((item) => (
|
||||
{visibleMenuItems.map((item) => (
|
||||
<MenuItemComponent key={item.id} item={item} />
|
||||
))}
|
||||
</nav>
|
||||
|
||||
Reference in new issue
Block a user