chore: initialize insar management system v2

This commit is contained in:
2026-04-14 13:16:01 +08:00
commit ecc72ec9cd
361 changed files with 2142522 additions and 0 deletions
+270
View File
@@ -0,0 +1,270 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
const LIMIT_OPTIONS = [100, 200, 500, 1000];
const ACTION_LABELS = {
write_access_granted: '写操作放行',
write_blocked_readonly: '只读拦截',
write_auth_required: '未登录拦截',
login_success: '登录成功',
login_failed: '登录失败',
logout: '退出登录',
user_created: '创建用户',
user_updated: '更新用户',
license_uploaded: '上传授权文件',
license_upload_failed: '上传授权失败',
license_refreshed: '刷新授权状态',
task_queued: '任务入队',
batch_created: '创建任务批次',
batch_marked_complete: '批次标记完成',
batch_item_updated: '更新批次条目',
dinsar_label_updated: '更新D-InSAR标签',
};
const getActionLabel = (action) => ACTION_LABELS[action] || action || '未知动作';
const getOutcome = (action) => {
const normalized = (action || '').toLowerCase();
if (normalized.includes('failed') || normalized.includes('blocked') || normalized.includes('required')) {
return 'blocked';
}
return 'ok';
};
const formatTime = (value) => {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '-';
return date.toLocaleString();
};
const formatDetail = (detail) => {
if (detail === null || detail === undefined || detail === '') {
return '-';
}
if (typeof detail === 'string') {
return detail;
}
try {
return JSON.stringify(detail);
} catch {
return '[detail 解析失败]';
}
};
const AuditLogPanel = ({ apiClient }) => {
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [limit, setLimit] = useState(200);
const [keyword, setKeyword] = useState('');
const [actionFilter, setActionFilter] = useState('all');
const [outcomeFilter, setOutcomeFilter] = useState('all');
const [autoRefresh, setAutoRefresh] = useState(false);
const [lastUpdated, setLastUpdated] = useState(null);
const loadLogs = useCallback(async ({ silent = false } = {}) => {
if (!silent) {
setLoading(true);
setMessage('');
}
try {
const response = await apiClient.get('/auth/audit-logs', { params: { limit } });
const data = Array.isArray(response.data) ? response.data : [];
setLogs(data);
setLastUpdated(new Date());
} catch (error) {
setMessage(error.response?.data?.detail || '加载审计日志失败。');
} finally {
if (!silent) {
setLoading(false);
}
}
}, [apiClient, limit]);
useEffect(() => {
loadLogs();
}, [loadLogs]);
useEffect(() => {
if (!autoRefresh) return undefined;
const timer = setInterval(() => {
loadLogs({ silent: true });
}, 15000);
return () => clearInterval(timer);
}, [autoRefresh, loadLogs]);
const actionOptions = useMemo(() => {
return Array.from(new Set(logs.map((item) => item.action).filter(Boolean))).sort();
}, [logs]);
const filteredLogs = useMemo(() => {
const normalizedKeyword = keyword.trim().toLowerCase();
return logs.filter((item) => {
if (actionFilter !== 'all' && item.action !== actionFilter) {
return false;
}
const outcome = getOutcome(item.action);
if (outcomeFilter !== 'all' && outcome !== outcomeFilter) {
return false;
}
if (!normalizedKeyword) {
return true;
}
const detailText = formatDetail(item.detail).toLowerCase();
const searchText = [
item.username || '',
item.action || '',
item.resource || '',
item.ip_address || '',
detailText,
].join(' ').toLowerCase();
return searchText.includes(normalizedKeyword);
});
}, [logs, actionFilter, outcomeFilter, keyword]);
const summary = useMemo(() => {
const total = filteredLogs.length;
const blocked = filteredLogs.filter((item) => getOutcome(item.action) === 'blocked').length;
const passed = total - blocked;
return { total, blocked, passed };
}, [filteredLogs]);
return (
<div className="audit-panel">
<div className="panel-card audit-panel-header">
<div className="audit-header-title">
<div className="panel-card-title">审计日志</div>
<p className="panel-card-desc">记录关键操作权限拦截和鉴权行为便于排查问题</p>
</div>
<div className="audit-header-actions">
<label className="audit-switch">
<input
type="checkbox"
checked={autoRefresh}
onChange={(event) => setAutoRefresh(event.target.checked)}
/>
自动刷新15s
</label>
<button
type="button"
className="secondary-btn"
onClick={() => loadLogs()}
disabled={loading}
>
{loading ? '刷新中...' : '立即刷新'}
</button>
</div>
</div>
<div className="panel-card audit-summary-card">
<div className="audit-summary-grid">
<div className="audit-summary-item">
<span>当前显示</span>
<strong>{summary.total}</strong>
</div>
<div className="audit-summary-item ok">
<span>通过</span>
<strong>{summary.passed}</strong>
</div>
<div className="audit-summary-item blocked">
<span>拦截/失败</span>
<strong>{summary.blocked}</strong>
</div>
<div className="audit-summary-item">
<span>更新时间</span>
<strong>{lastUpdated ? lastUpdated.toLocaleTimeString() : '-'}</strong>
</div>
</div>
</div>
<div className="panel-card audit-filter-card">
<div className="audit-filter-grid">
<label className="audit-filter-field">
<span>关键字</span>
<input
type="text"
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
placeholder="用户名 / 动作 / 资源 / 详情"
/>
</label>
<label className="audit-filter-field">
<span>动作类型</span>
<select value={actionFilter} onChange={(event) => setActionFilter(event.target.value)}>
<option value="all">全部动作</option>
{actionOptions.map((action) => (
<option key={action} value={action}>
{getActionLabel(action)}
</option>
))}
</select>
</label>
<label className="audit-filter-field">
<span>结果</span>
<select value={outcomeFilter} onChange={(event) => setOutcomeFilter(event.target.value)}>
<option value="all">全部结果</option>
<option value="ok">通过</option>
<option value="blocked">拦截/失败</option>
</select>
</label>
<label className="audit-filter-field">
<span>返回条数</span>
<select value={limit} onChange={(event) => setLimit(Number(event.target.value) || 200)}>
{LIMIT_OPTIONS.map((value) => (
<option key={value} value={value}>
最近 {value}
</option>
))}
</select>
</label>
</div>
</div>
<div className="panel-card audit-list-card">
<div className="audit-list">
{filteredLogs.map((item) => {
const outcome = getOutcome(item.action);
return (
<div key={item.id} className="audit-log-item">
<div className="audit-log-top">
<div className="audit-log-main">
<span className={`audit-outcome ${outcome}`}>{outcome === 'ok' ? '通过' : '拦截/失败'}</span>
<strong>{getActionLabel(item.action)}</strong>
<span className="audit-operator">{item.username || '匿名/未登录'}</span>
</div>
<span className="audit-time">{formatTime(item.created_at)}</span>
</div>
<div className="audit-log-meta">
<span>资源{item.resource || '-'}</span>
<span>IP{item.ip_address || '-'}</span>
</div>
<div className="audit-log-detail">详情{formatDetail(item.detail)}</div>
</div>
);
})}
{!loading && filteredLogs.length === 0 && (
<div className="empty-state">当前筛选条件下暂无日志</div>
)}
</div>
</div>
{message && (
<div className="panel-inline-message error">
{message}
</div>
)}
</div>
);
};
export default AuditLogPanel;