merge: SQL平台代码v1 优化包合并 — 193条审核规则 + 16页面UI优化
后端新增 (Step 1): - mysql_audit_rules.py: 63条 MySQL审核规则 (DDL 36 + DML 9 + 命名 5 + 性能 5 + 安全 5) - redis_audit_rules.py: 43条 Redis审核规则 (KEY管理 10 + 命令安全 12 + 数据结构 8 + 性能 8 + 安全 5) - mongodb_audit_rules.py: 42条 MongoDB审核规则 (集合管理 + 查询安全 + 聚合管道 + 写入安全 + 性能) - postgres_audit_rules.py: 45条 PostgreSQL审核规则 (DDL 18 + DML 8 + DQL 6 + 安全 6 + 性能 7) 后端合并 (Step 2): - audit_engine_v2.py: 新增4个DB规则模块import (100+→200+规则) - audit_rule_seeds.py: 新增30条MongoDB种子规则 + 别名映射 - audit_rule.py: 新增数据库类型别名 (postgresql→postgres, pg→postgres, mongo→mongodb) 前端优化 (Step 4) — 16页面全覆盖: - InstancePage, WorkflowPage, TicketsPage, TicketCreatePage, TicketDetailPage - UsersPage, SlowQueryPage, DataDictionaryPage, Dashboard - InstanceGroupPage, AuditLogsPage, QueriesPage, AuditRulesPage, RolesPage - useAuth.tsx, SqlEditor.tsx 后端测试 895/895 通过, 前端 tsc --noEmit 通过
This commit is contained in:
1 parent
e1ba8b3327
commit
58eb78c987
24 files changed
+16333
-2225
No files matched your search
+368
-23
@@ -1,9 +1,28 @@
|
||||
import { createContext, useContext, useState, useCallback, useMemo } from 'react';
|
||||
import { createContext, useContext, useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
||||
import { menuItems, currentUser as defaultUser } from '@/lib/mock';
|
||||
import { authApi } from '@/api/auth';
|
||||
|
||||
export type UserRole = 'admin' | 'dba' | 'developer' | 'guest';
|
||||
|
||||
/** 细粒度权限类型 */
|
||||
export type PermissionAction =
|
||||
| 'ticket.create' | 'ticket.approve' | 'ticket.execute' | 'ticket.rollback' | 'ticket.delete'
|
||||
| 'query.create' | 'query.export'
|
||||
| 'instance.add' | 'instance.edit' | 'instance.delete'
|
||||
| 'user.manage' | 'role.manage' | 'rule.manage'
|
||||
| 'workflow.manage' | 'config.manage'
|
||||
| 'announcement.manage' | 'auto-task.manage' | 'instance-group.manage';
|
||||
|
||||
/** 数据源级权限 */
|
||||
export interface DataSourcePermission {
|
||||
instanceId: number;
|
||||
database?: string;
|
||||
tables?: string[];
|
||||
columns?: Record<string, string[]>;
|
||||
actions: ('read' | 'write' | 'delete' | 'export')[];
|
||||
}
|
||||
|
||||
/** 扩展的用户信息 */
|
||||
export interface AuthUser {
|
||||
id: number;
|
||||
username: string;
|
||||
@@ -11,6 +30,22 @@ export interface AuthUser {
|
||||
role: UserRole;
|
||||
roleName: string;
|
||||
avatar: string;
|
||||
/** 用户组ID列表 */
|
||||
groupIds?: number[];
|
||||
/** 细粒度数据源权限 */
|
||||
dataSourcePermissions?: DataSourcePermission[];
|
||||
/** 自定义权限(覆盖角色默认权限) */
|
||||
customPermissions?: Record<string, boolean>;
|
||||
/** 权限最后更新时间 */
|
||||
permissionsUpdatedAt?: number;
|
||||
}
|
||||
|
||||
/** 权限缓存结构 */
|
||||
interface PermissionCache {
|
||||
menu: Map<string, boolean>;
|
||||
action: Map<string, boolean>;
|
||||
dataSource: Map<string, boolean>;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
@@ -19,12 +54,35 @@ interface AuthContextType {
|
||||
login: (username: string, password: string) => Promise<boolean>;
|
||||
logout: () => void;
|
||||
switchRole: (role: UserRole) => void;
|
||||
/** 菜单级权限检查(带缓存) */
|
||||
hasPermission: (menuId: string) => boolean;
|
||||
/** 操作级权限检查(带缓存) */
|
||||
hasActionPermission: (action: PermissionAction | string) => boolean;
|
||||
/** 数据源级权限检查 */
|
||||
hasDataSourcePermission: (
|
||||
instanceId: number,
|
||||
action: 'read' | 'write' | 'delete' | 'export',
|
||||
database?: string,
|
||||
table?: string
|
||||
) => boolean;
|
||||
/** 批量检查数据源权限 */
|
||||
checkDataSourcePermissions: (
|
||||
instanceId: number,
|
||||
actions: ('read' | 'write' | 'delete' | 'export')[],
|
||||
database?: string,
|
||||
table?: string
|
||||
) => Record<string, boolean>;
|
||||
/** 刷新用户权限 */
|
||||
refreshPermissions: () => Promise<void>;
|
||||
/** 清除权限缓存 */
|
||||
clearPermissionCache: () => void;
|
||||
visibleMenuItems: typeof menuItems;
|
||||
isAdmin: boolean;
|
||||
isDBA: boolean;
|
||||
isDeveloper: boolean;
|
||||
isGuest: boolean;
|
||||
/** 是否拥有指定角色或以上级别权限 */
|
||||
hasRoleLevel: (minRole: UserRole) => boolean;
|
||||
}
|
||||
|
||||
const roleNameMap: Record<UserRole, string> = {
|
||||
@@ -34,6 +92,14 @@ const roleNameMap: Record<UserRole, string> = {
|
||||
guest: '访客',
|
||||
};
|
||||
|
||||
/** 角色权重映射,用于权限级别比较 */
|
||||
const roleWeightMap: Record<UserRole, number> = {
|
||||
admin: 100,
|
||||
dba: 80,
|
||||
developer: 60,
|
||||
guest: 20,
|
||||
};
|
||||
|
||||
const routePermissionMap: Record<string, UserRole[]> = {
|
||||
dashboard: ['admin', 'dba', 'developer', 'guest'],
|
||||
instances: ['admin', 'dba'],
|
||||
@@ -55,7 +121,7 @@ export const actionPermissionMap: Record<string, UserRole[]> = {
|
||||
'ticket.execute': ['admin', 'dba'],
|
||||
'ticket.rollback': ['admin', 'dba'],
|
||||
'ticket.delete': ['admin'],
|
||||
'query.create': ['admin', 'dba', 'developer'],
|
||||
'query.create': ['admin', 'dba', 'developer', 'guest'],
|
||||
'query.export': ['admin', 'dba', 'developer'],
|
||||
'instance.add': ['admin', 'dba'],
|
||||
'instance.edit': ['admin', 'dba'],
|
||||
@@ -70,6 +136,11 @@ export const actionPermissionMap: Record<string, UserRole[]> = {
|
||||
'instance-group.manage': ['admin'],
|
||||
};
|
||||
|
||||
/** 权限变更事件名称 */
|
||||
const PERMISSION_CHANGE_EVENT = 'auth:permissions_changed';
|
||||
/** 缓存有效期(毫秒) */
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5分钟
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
@@ -80,56 +151,284 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
role: defaultUser.role,
|
||||
roleName: defaultUser.roleName,
|
||||
avatar: defaultUser.avatar,
|
||||
groupIds: [],
|
||||
dataSourcePermissions: [],
|
||||
customPermissions: {},
|
||||
});
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(() => !!localStorage.getItem('token'));
|
||||
|
||||
/** 权限缓存引用 */
|
||||
const permissionCacheRef = useRef<PermissionCache>({
|
||||
menu: new Map(),
|
||||
action: new Map(),
|
||||
dataSource: new Map(),
|
||||
timestamp: 0,
|
||||
});
|
||||
|
||||
/** 权限变更监听器引用 */
|
||||
const listenersRef = useRef<Set<(user: AuthUser) => void>>(new Set());
|
||||
|
||||
/** 检查缓存是否有效 */
|
||||
const isCacheValid = useCallback((): boolean => {
|
||||
return Date.now() - permissionCacheRef.current.timestamp < CACHE_TTL;
|
||||
}, []);
|
||||
|
||||
/** 清除权限缓存 */
|
||||
const clearPermissionCache = useCallback(() => {
|
||||
permissionCacheRef.current.menu.clear();
|
||||
permissionCacheRef.current.action.clear();
|
||||
permissionCacheRef.current.dataSource.clear();
|
||||
permissionCacheRef.current.timestamp = 0;
|
||||
}, []);
|
||||
|
||||
/** 触发权限变更事件 */
|
||||
const emitPermissionChange = useCallback((updatedUser: AuthUser) => {
|
||||
listenersRef.current.forEach(listener => listener(updatedUser));
|
||||
// 触发自定义事件,供其他组件监听
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent(PERMISSION_CHANGE_EVENT, { detail: updatedUser }));
|
||||
}
|
||||
}, []);
|
||||
|
||||
/** 注册权限变更监听器 */
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === 'token') {
|
||||
if (!e.newValue) {
|
||||
setIsLoggedIn(false);
|
||||
clearPermissionCache();
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
return () => window.removeEventListener('storage', handleStorageChange);
|
||||
}, [clearPermissionCache]);
|
||||
|
||||
const login = useCallback(async (username: string, password: string): Promise<boolean> => {
|
||||
try {
|
||||
const res = await authApi.login(username, password);
|
||||
localStorage.setItem('token', res.access_token);
|
||||
|
||||
const u = res.user;
|
||||
const role = u.role as UserRole;
|
||||
setUser({
|
||||
|
||||
// 解析后端返回的细粒度权限
|
||||
const dataSourcePermissions: DataSourcePermission[] = u.data_source_permissions
|
||||
? JSON.parse(u.data_source_permissions)
|
||||
: [];
|
||||
const customPermissions: Record<string, boolean> = u.custom_permissions
|
||||
? JSON.parse(u.custom_permissions)
|
||||
: {};
|
||||
|
||||
const newUser: AuthUser = {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
email: u.email || '',
|
||||
role,
|
||||
roleName: roleNameMap[role] || role,
|
||||
avatar: u.display_name?.charAt(0) || username.charAt(0).toUpperCase(),
|
||||
});
|
||||
groupIds: u.group_ids || [],
|
||||
dataSourcePermissions,
|
||||
customPermissions,
|
||||
permissionsUpdatedAt: Date.now(),
|
||||
};
|
||||
|
||||
setUser(newUser);
|
||||
setIsLoggedIn(true);
|
||||
clearPermissionCache();
|
||||
emitPermissionChange(newUser);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
}, [clearPermissionCache, emitPermissionChange]);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
localStorage.removeItem('token');
|
||||
setIsLoggedIn(false);
|
||||
}, []);
|
||||
clearPermissionCache();
|
||||
setUser({
|
||||
id: defaultUser.id,
|
||||
username: defaultUser.username,
|
||||
email: defaultUser.email,
|
||||
role: defaultUser.role,
|
||||
roleName: defaultUser.roleName,
|
||||
avatar: defaultUser.avatar,
|
||||
groupIds: [],
|
||||
dataSourcePermissions: [],
|
||||
customPermissions: {},
|
||||
});
|
||||
emitPermissionChange({} as AuthUser);
|
||||
}, [clearPermissionCache, emitPermissionChange]);
|
||||
|
||||
const switchRole = useCallback((role: UserRole) => {
|
||||
setUser(prev => ({
|
||||
...prev,
|
||||
role,
|
||||
roleName: roleNameMap[role],
|
||||
username: role === 'admin' ? 'admin' : role === 'dba' ? 'zhangsan' : role === 'developer' ? 'wangwu' : 'visitor',
|
||||
email: `${role}@company.com`,
|
||||
avatar: role.charAt(0).toUpperCase(),
|
||||
}));
|
||||
}, []);
|
||||
setUser(prev => {
|
||||
const newUser: AuthUser = {
|
||||
...prev,
|
||||
role,
|
||||
roleName: roleNameMap[role],
|
||||
username: role === 'admin' ? 'admin' : role === 'dba' ? 'zhangsan' : role === 'developer' ? 'wangwu' : 'visitor',
|
||||
email: `${role}@company.com`,
|
||||
avatar: role.charAt(0).toUpperCase(),
|
||||
};
|
||||
clearPermissionCache();
|
||||
emitPermissionChange(newUser);
|
||||
return newUser;
|
||||
});
|
||||
}, [clearPermissionCache, emitPermissionChange]);
|
||||
|
||||
/** 菜单权限检查(带缓存) */
|
||||
const hasPermission = useCallback((menuId: string): boolean => {
|
||||
if (!isLoggedIn) return false;
|
||||
|
||||
const cache = permissionCacheRef.current;
|
||||
if (isCacheValid() && cache.menu.has(menuId)) {
|
||||
return cache.menu.get(menuId)!;
|
||||
}
|
||||
|
||||
const allowedRoles = routePermissionMap[menuId];
|
||||
if (!allowedRoles) return false;
|
||||
return allowedRoles.includes(user.role);
|
||||
}, [user.role]);
|
||||
if (!allowedRoles) {
|
||||
cache.menu.set(menuId, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = allowedRoles.includes(user.role);
|
||||
cache.menu.set(menuId, result);
|
||||
cache.timestamp = Date.now();
|
||||
return result;
|
||||
}, [user.role, isLoggedIn, isCacheValid]);
|
||||
|
||||
/** 操作权限检查(带缓存,支持自定义权限覆盖) */
|
||||
const hasActionPermission = useCallback((action: PermissionAction | string): boolean => {
|
||||
if (!isLoggedIn) return false;
|
||||
|
||||
const cache = permissionCacheRef.current;
|
||||
if (isCacheValid() && cache.action.has(action)) {
|
||||
return cache.action.get(action)!;
|
||||
}
|
||||
|
||||
// 优先检查自定义权限
|
||||
if (user.customPermissions && action in user.customPermissions) {
|
||||
const customResult = user.customPermissions[action];
|
||||
cache.action.set(action, customResult);
|
||||
cache.timestamp = Date.now();
|
||||
return customResult;
|
||||
}
|
||||
|
||||
const allowedRoles = actionPermissionMap[action];
|
||||
if (!allowedRoles) {
|
||||
cache.action.set(action, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = allowedRoles.includes(user.role);
|
||||
cache.action.set(action, result);
|
||||
cache.timestamp = Date.now();
|
||||
return result;
|
||||
}, [user.role, user.customPermissions, isLoggedIn, isCacheValid]);
|
||||
|
||||
/** 数据源级权限检查 */
|
||||
const hasDataSourcePermission = useCallback((
|
||||
instanceId: number,
|
||||
action: 'read' | 'write' | 'delete' | 'export',
|
||||
database?: string,
|
||||
table?: string
|
||||
): boolean => {
|
||||
if (!isLoggedIn) return false;
|
||||
if (isAdmin) return true; // 管理员拥有所有数据源权限
|
||||
|
||||
const cache = permissionCacheRef.current;
|
||||
const cacheKey = `${instanceId}:${action}:${database || '*'}:${table || '*'}`;
|
||||
if (isCacheValid() && cache.dataSource.has(cacheKey)) {
|
||||
return cache.dataSource.get(cacheKey)!;
|
||||
}
|
||||
|
||||
const perms = user.dataSourcePermissions || [];
|
||||
if (perms.length === 0) {
|
||||
cache.dataSource.set(cacheKey, false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasPerm = perms.some((ds: DataSourcePermission) => {
|
||||
// 检查实例ID
|
||||
if (ds.instanceId !== instanceId) return false;
|
||||
|
||||
// 检查操作权限
|
||||
if (!ds.actions.includes(action)) return false;
|
||||
|
||||
// 检查数据库级别
|
||||
if (database && ds.database && ds.database !== database) return false;
|
||||
|
||||
// 检查表级别
|
||||
if (table && ds.tables && ds.tables.length > 0 && !ds.tables.includes(table)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
cache.dataSource.set(cacheKey, hasPerm);
|
||||
cache.timestamp = Date.now();
|
||||
return hasPerm;
|
||||
}, [user.dataSourcePermissions, user.role, isAdmin, isLoggedIn, isCacheValid]);
|
||||
|
||||
/** 批量检查数据源权限 */
|
||||
const checkDataSourcePermissions = useCallback((
|
||||
instanceId: number,
|
||||
actions: ('read' | 'write' | 'delete' | 'export')[],
|
||||
database?: string,
|
||||
table?: string
|
||||
): Record<string, boolean> => {
|
||||
const result: Record<string, boolean> = {};
|
||||
for (const action of actions) {
|
||||
result[action] = hasDataSourcePermission(instanceId, action, database, table);
|
||||
}
|
||||
return result;
|
||||
}, [hasDataSourcePermission]);
|
||||
|
||||
/** 从服务器刷新用户权限 */
|
||||
const refreshPermissions = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const res = await authApi.getCurrentUser();
|
||||
if (res.user) {
|
||||
const u = res.user;
|
||||
const role = u.role as UserRole;
|
||||
const dataSourcePermissions: DataSourcePermission[] = u.data_source_permissions
|
||||
? JSON.parse(u.data_source_permissions)
|
||||
: [];
|
||||
const customPermissions: Record<string, boolean> = u.custom_permissions
|
||||
? JSON.parse(u.custom_permissions)
|
||||
: {};
|
||||
|
||||
const newUser: AuthUser = {
|
||||
...user,
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
email: u.email || '',
|
||||
role,
|
||||
roleName: roleNameMap[role] || role,
|
||||
groupIds: u.group_ids || [],
|
||||
dataSourcePermissions,
|
||||
customPermissions,
|
||||
permissionsUpdatedAt: Date.now(),
|
||||
};
|
||||
|
||||
setUser(newUser);
|
||||
clearPermissionCache();
|
||||
emitPermissionChange(newUser);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新权限失败:', error);
|
||||
}
|
||||
}, [user, clearPermissionCache, emitPermissionChange]);
|
||||
|
||||
const visibleMenuItems = useMemo(() => {
|
||||
return menuItems.filter(item => item.roles.includes(user.role));
|
||||
}, [user.role]);
|
||||
|
||||
/** 检查是否拥有指定角色或以上级别权限 */
|
||||
const hasRoleLevel = useCallback((minRole: UserRole): boolean => {
|
||||
return roleWeightMap[user.role] >= roleWeightMap[minRole];
|
||||
}, [user.role]);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
user,
|
||||
isLoggedIn,
|
||||
@@ -137,12 +436,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
logout,
|
||||
switchRole,
|
||||
hasPermission,
|
||||
hasActionPermission,
|
||||
hasDataSourcePermission,
|
||||
checkDataSourcePermissions,
|
||||
refreshPermissions,
|
||||
clearPermissionCache,
|
||||
visibleMenuItems,
|
||||
isAdmin: user.role === 'admin',
|
||||
isDBA: user.role === 'dba',
|
||||
isDeveloper: user.role === 'developer',
|
||||
isGuest: user.role === 'guest',
|
||||
}), [user, isLoggedIn, login, logout, switchRole, hasPermission, visibleMenuItems]);
|
||||
hasRoleLevel,
|
||||
}), [user, isLoggedIn, login, logout, switchRole, hasPermission, hasActionPermission,
|
||||
hasDataSourcePermission, checkDataSourcePermissions, refreshPermissions,
|
||||
clearPermissionCache, visibleMenuItems, hasRoleLevel]);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
@@ -153,9 +460,47 @@ export function useAuth(): AuthContextType {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function useActionPermission(action: string): boolean {
|
||||
/** Hook: 检查操作权限(带缓存) */
|
||||
export function useActionPermission(action: PermissionAction | string): boolean {
|
||||
const { hasActionPermission } = useAuth();
|
||||
return hasActionPermission(action);
|
||||
}
|
||||
|
||||
/** Hook: 检查数据源权限 */
|
||||
export function useDataSourcePermission(
|
||||
instanceId: number,
|
||||
action: 'read' | 'write' | 'delete' | 'export',
|
||||
database?: string,
|
||||
table?: string
|
||||
): boolean {
|
||||
const { hasDataSourcePermission } = useAuth();
|
||||
return hasDataSourcePermission(instanceId, action, database, table);
|
||||
}
|
||||
|
||||
/** Hook: 监听权限变更 */
|
||||
export function usePermissionChange(callback: (user: AuthUser) => void): void {
|
||||
const { user } = useAuth();
|
||||
const allowedRoles = actionPermissionMap[action];
|
||||
if (!allowedRoles) return false;
|
||||
return allowedRoles.includes(user.role);
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = (e: Event) => {
|
||||
const customEvent = e as CustomEvent<AuthUser>;
|
||||
callback(customEvent.detail);
|
||||
};
|
||||
|
||||
window.addEventListener(PERMISSION_CHANGE_EVENT, handleChange);
|
||||
return () => window.removeEventListener(PERMISSION_CHANGE_EVENT, handleChange);
|
||||
}, [callback]);
|
||||
|
||||
// 当user变化时也触发回调
|
||||
useEffect(() => {
|
||||
if (user && user.id) {
|
||||
callback(user);
|
||||
}
|
||||
}, [user.id, user.permissionsUpdatedAt, callback]);
|
||||
}
|
||||
|
||||
/** Hook: 检查角色级别 */
|
||||
export function useRoleLevel(minRole: UserRole): boolean {
|
||||
const { hasRoleLevel } = useAuth();
|
||||
return hasRoleLevel(minRole);
|
||||
}
|
||||
Reference in new issue
Block a user