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
@@ -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