feat: Phase 3 - AES encryption, SQL audit engine, audit logs (85 tests)
Phase 3.1: Instance password AES-256-GCM encryption Phase 3.2: SQL audit rule engine (10 rules) Phase 3.3: Auto-audit on ticket creation Phase 3.4: Operation audit logs + GET /audit/logs Tests: 44 -> 85 (41 new), Frontend: AuditLogsPage
This commit is contained in:
1 parent
27989274f5
commit
66b09e4d85
16 files changed
+1146
-6
No files matched your search
@@ -15,6 +15,7 @@ import WorkflowPage from '@/pages/WorkflowPage';
|
||||
import AutoTaskPage from '@/pages/AutoTaskPage';
|
||||
import AnnouncementPage from '@/pages/AnnouncementPage';
|
||||
import ConfigPlatformPage from '@/pages/ConfigPlatformPage';
|
||||
import AuditLogsPage from '@/pages/AuditLogsPage';
|
||||
|
||||
// 路由守卫:未登录跳转登录页,无权限跳转仪表盘
|
||||
function RouteGuard() {
|
||||
@@ -60,6 +61,7 @@ function AppRoutes() {
|
||||
<Route path="users" element={<UsersPage />} />
|
||||
<Route path="data-permission" element={<DataPermissionPage />} />
|
||||
<Route path="audit-rules" element={<AuditRulesPage />} />
|
||||
<Route path="audit-logs" element={<AuditLogsPage />} />
|
||||
<Route path="workflow" element={<WorkflowPage />} />
|
||||
<Route path="auto-task" element={<AutoTaskPage />} />
|
||||
<Route path="announcement" element={<AnnouncementPage />} />
|
||||
|
||||
@@ -46,6 +46,7 @@ const routePermissionMap: Record<string, UserRole[]> = {
|
||||
users: ['admin'],
|
||||
'data-permission': ['admin', 'dba'],
|
||||
'audit-rules': ['admin', 'dba'],
|
||||
'audit-logs': ['admin', 'dba'],
|
||||
workflow: ['admin', 'dba'],
|
||||
'auto-task': ['admin'],
|
||||
announcement: ['admin'],
|
||||
|
||||
@@ -32,6 +32,7 @@ export const menuItems = [
|
||||
{ id: 'users', label: '用户管理', icon: 'Users', section: 'permission', roles: ['admin'] },
|
||||
{ id: 'data-permission', label: '数据权限', icon: 'Lock', section: 'permission', roles: ['admin', 'dba'] },
|
||||
{ id: 'audit-rules', label: '审核规则', icon: 'Shield', section: 'system', roles: ['admin', 'dba'] },
|
||||
{ id: 'audit-logs', label: '审计日志', icon: 'FileText', section: 'system', roles: ['admin', 'dba'] },
|
||||
{ id: 'workflow', label: '审批流程', icon: 'GitBranch', section: 'system', roles: ['admin', 'dba'] },
|
||||
{ id: 'auto-task', label: '自动任务', icon: 'Clock', section: 'system', roles: ['admin'] },
|
||||
{ id: 'announcement', label: '公告管理', icon: 'Megaphone', section: 'system', roles: ['admin'] },
|
||||
@@ -165,8 +166,8 @@ export const notificationChannels: NotificationChannel[] = [
|
||||
];
|
||||
|
||||
export const roles: Role[] = [
|
||||
{ id: 'admin', name: '超级管理员', description: '拥有系统的所有权限,包括用户管理、系统配置等', icon: 'Crown', iconClass: 'text-purple-400 bg-purple-400/10', userCount: 2, permissionCount: 56, permissions: ['dashboard', 'mysql', 'postgres', 'redis', 'mongo', 'tickets', 'queries', 'roles', 'users', 'data-permission', 'audit-rules', 'workflow', 'auto-task', 'announcement', 'config-platform', 'notifications'] },
|
||||
{ id: 'dba', name: '数据库管理员', description: '负责数据库运维、工单审批和 SQL 审核', icon: 'Database', iconClass: 'text-blue-400 bg-blue-400/10', userCount: 5, permissionCount: 32, permissions: ['dashboard', 'mysql', 'postgres', 'redis', 'tickets', 'queries', 'data-permission', 'audit-rules', 'workflow'] },
|
||||
{ id: 'admin', name: '超级管理员', description: '拥有系统的所有权限,包括用户管理、系统配置等', icon: 'Crown', iconClass: 'text-purple-400 bg-purple-400/10', userCount: 2, permissionCount: 56, permissions: ['dashboard', 'mysql', 'postgres', 'redis', 'mongo', 'tickets', 'queries', 'roles', 'users', 'data-permission', 'audit-rules', 'audit-logs', 'workflow', 'auto-task', 'announcement', 'config-platform', 'notifications'] },
|
||||
{ id: 'dba', name: '数据库管理员', description: '负责数据库运维、工单审批和 SQL 审核', icon: 'Database', iconClass: 'text-blue-400 bg-blue-400/10', userCount: 5, permissionCount: 32, permissions: ['dashboard', 'mysql', 'postgres', 'redis', 'tickets', 'queries', 'data-permission', 'audit-rules', 'audit-logs', 'workflow'] },
|
||||
{ id: 'developer', name: '开发人员', description: '可以提交变更工单和执行数据查询', icon: 'Code', iconClass: 'text-green-400 bg-green-400/10', userCount: 28, permissionCount: 12, permissions: ['dashboard', 'tickets', 'queries'] },
|
||||
{ id: 'guest', name: '访客', description: '仅可查看,无法执行任何操作', icon: 'Eye', iconClass: 'text-gray-400 bg-gray-400/10', userCount: 8, permissionCount: 4, permissions: ['dashboard', 'queries'] },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import request from '../api/request';
|
||||
|
||||
interface AuditLog {
|
||||
id: number;
|
||||
user_id: number;
|
||||
action: string;
|
||||
target_type: string;
|
||||
target_id: number | null;
|
||||
detail: string | null;
|
||||
ip_address: string | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
interface AuditLogResponse {
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
items: AuditLog[];
|
||||
}
|
||||
|
||||
const ACTION_COLORS: Record<string, string> = {
|
||||
CREATE_TICKET: 'bg-blue-100 text-blue-800',
|
||||
REVIEW_TICKET: 'bg-yellow-100 text-yellow-800',
|
||||
EXECUTE_TICKET: 'bg-green-100 text-green-800',
|
||||
CREATE_INSTANCE: 'bg-purple-100 text-purple-800',
|
||||
DELETE_INSTANCE: 'bg-red-100 text-red-800',
|
||||
CREATE_USER: 'bg-indigo-100 text-indigo-800',
|
||||
DELETE_USER: 'bg-red-100 text-red-800',
|
||||
QUERY: 'bg-cyan-100 text-cyan-800',
|
||||
};
|
||||
|
||||
const TARGET_ICONS: Record<string, string> = {
|
||||
ticket: '📋',
|
||||
instance: '🗄️',
|
||||
user: '👤',
|
||||
query: '🔍',
|
||||
};
|
||||
|
||||
export default function AuditLogsPage() {
|
||||
const [logs, setLogs] = useState<AuditLog[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filterAction, setFilterAction] = useState('');
|
||||
const pageSize = 20;
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, [page, filterAction]);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
|
||||
if (filterAction) params.set('action', filterAction);
|
||||
const res = await request.get(`/audit/logs?${params}`);
|
||||
const data = res.data as AuditLogResponse;
|
||||
setLogs(data.items || []);
|
||||
setTotal(data.total);
|
||||
} catch (e) {
|
||||
console.error('获取审计日志失败', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
const formatDate = (d: string | null) => {
|
||||
if (!d) return '-';
|
||||
return new Date(d).toLocaleString('zh-CN');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-800">审计日志</h2>
|
||||
<span className="text-sm text-gray-500">共 {total} 条记录</span>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex gap-2">
|
||||
{['', 'CREATE_TICKET', 'REVIEW_TICKET', 'EXECUTE_TICKET', 'CREATE_INSTANCE', 'DELETE_INSTANCE', 'QUERY'].map(action => (
|
||||
<button
|
||||
key={action}
|
||||
onClick={() => { setFilterAction(action); setPage(1); }}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
filterAction === action
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{action || '全部'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 text-gray-600">
|
||||
<th className="px-4 py-3 text-left font-medium">操作</th>
|
||||
<th className="px-4 py-3 text-left font-medium">目标</th>
|
||||
<th className="px-4 py-3 text-left font-medium">详情</th>
|
||||
<th className="px-4 py-3 text-left font-medium">时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{loading ? (
|
||||
<tr><td colSpan={4} className="px-4 py-8 text-center text-gray-400">加载中...</td></tr>
|
||||
) : logs.length === 0 ? (
|
||||
<tr><td colSpan={4} className="px-4 py-8 text-center text-gray-400">暂无审计日志</td></tr>
|
||||
) : (
|
||||
logs.map(log => (
|
||||
<tr key={log.id} className="hover:bg-gray-50 transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-block px-2 py-0.5 rounded text-xs font-medium ${ACTION_COLORS[log.action] || 'bg-gray-100 text-gray-600'}`}>
|
||||
{log.action}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="mr-1">{TARGET_ICONS[log.target_type] || '📌'}</span>
|
||||
<span className="text-gray-600">{log.target_type}</span>
|
||||
{log.target_id && <span className="text-gray-400 ml-1">#{log.target_id}</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-500 max-w-md truncate">{log.detail || '-'}</td>
|
||||
<td className="px-4 py-3 text-gray-400 whitespace-nowrap">{formatDate(log.created_at)}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex justify-center gap-2">
|
||||
<button
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={page <= 1}
|
||||
className="px-3 py-1 rounded bg-gray-100 text-gray-600 disabled:opacity-50"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span className="px-3 py-1 text-gray-500">{page} / {totalPages}</span>
|
||||
<button
|
||||
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages}
|
||||
className="px-3 py-1 rounded bg-gray-100 text-gray-600 disabled:opacity-50"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user