mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-21 07:06:53 +02:00
524 lines
22 KiB
JavaScript
524 lines
22 KiB
JavaScript
/**
|
|
* 系统设置 - 平台操作审计日志
|
|
*/
|
|
let auditLogsPage = 1;
|
|
let auditLogsPageSize = 20;
|
|
let auditLogsTotal = 0;
|
|
|
|
const AUDIT_PAGE_SIZE_KEY = 'cyberstrike_audit_page_size';
|
|
|
|
/** 按类别列出的操作(用于 datalist 提示,避免超长下拉) */
|
|
const AUDIT_ACTIONS_BY_CATEGORY = {
|
|
auth: ['login', 'logout', 'change_password'],
|
|
config: ['apply', 'update'],
|
|
c2: ['listener_create', 'listener_delete', 'listener_start', 'listener_stop',
|
|
'session_delete', 'task_create', 'task_cancel', 'task_delete'],
|
|
webshell: ['connection_create', 'connection_delete'],
|
|
knowledge: ['item_delete', 'index_rebuild'],
|
|
conversation: ['delete', 'delete_turn'],
|
|
vulnerability: ['create', 'update', 'delete'],
|
|
external_mcp: ['upsert', 'delete'],
|
|
task: ['create_queue', 'start_queue', 'delete_queue', 'pause_queue', 'rerun_queue', 'delete_batch_task'],
|
|
tool: ['execution_delete', 'execution_delete_batch'],
|
|
file: ['upload', 'delete'],
|
|
hitl: ['decision'],
|
|
role: ['create', 'update', 'delete'],
|
|
skill: ['create', 'update', 'delete'],
|
|
agent: ['markdown_create', 'markdown_update', 'markdown_delete']
|
|
};
|
|
|
|
function auditT(key, opts, fallback) {
|
|
if (typeof t === 'function') {
|
|
const v = t(key, opts);
|
|
if (v && v !== key) return v;
|
|
}
|
|
return fallback != null ? fallback : key;
|
|
}
|
|
|
|
function auditCategoryI18nKey(category) {
|
|
if (!category) return '';
|
|
if (category === 'external_mcp') return 'externalMcp';
|
|
return category;
|
|
}
|
|
|
|
function auditCategoryLabel(category) {
|
|
if (!category) return '';
|
|
const key = 'settingsAudit.cat.' + auditCategoryI18nKey(category);
|
|
return auditT(key, null, category);
|
|
}
|
|
|
|
function auditActionLabel(action) {
|
|
if (!action) return '';
|
|
return auditT('settingsAudit.act.' + action, null, action);
|
|
}
|
|
|
|
function formatAuditTime(iso) {
|
|
if (!iso) return '';
|
|
try {
|
|
const d = new Date(iso);
|
|
if (Number.isNaN(d.getTime())) return iso;
|
|
return d.toLocaleString();
|
|
} catch (_) {
|
|
return iso;
|
|
}
|
|
}
|
|
|
|
function auditDatetimeLocalToRFC3339(value) {
|
|
if (!value || !value.trim()) return '';
|
|
const d = new Date(value);
|
|
if (Number.isNaN(d.getTime())) return '';
|
|
return d.toISOString();
|
|
}
|
|
|
|
function initAuditPageSizeFromStorage() {
|
|
try {
|
|
const saved = parseInt(localStorage.getItem(AUDIT_PAGE_SIZE_KEY), 10);
|
|
if ([10, 20, 50, 100].indexOf(saved) >= 0) {
|
|
auditLogsPageSize = saved;
|
|
}
|
|
} catch (_) { /* ignore */ }
|
|
const sel = document.getElementById('audit-page-size');
|
|
if (sel) sel.value = String(auditLogsPageSize);
|
|
}
|
|
|
|
function onAuditPageSizeChange() {
|
|
const sel = document.getElementById('audit-page-size');
|
|
if (!sel) return;
|
|
const n = parseInt(sel.value, 10);
|
|
if ([10, 20, 50, 100].indexOf(n) < 0) return;
|
|
auditLogsPageSize = n;
|
|
try {
|
|
localStorage.setItem(AUDIT_PAGE_SIZE_KEY, String(n));
|
|
} catch (_) { /* ignore */ }
|
|
auditLogsPage = 1;
|
|
loadAuditLogs(1);
|
|
}
|
|
|
|
function rebuildAuditActionSelect() {
|
|
const catEl = document.getElementById('audit-filter-category');
|
|
const actEl = document.getElementById('audit-filter-action');
|
|
if (!actEl) return;
|
|
|
|
const category = catEl ? catEl.value : '';
|
|
const prev = actEl.value;
|
|
const allLabel = auditT('settingsAudit.filterAllActions', null, '全部操作');
|
|
const hint = auditT('settingsAudit.filterCascadeHint', null, '选择类别后可筛选具体操作');
|
|
actEl.innerHTML = '';
|
|
const allOpt = document.createElement('option');
|
|
allOpt.value = '';
|
|
allOpt.textContent = allLabel;
|
|
actEl.appendChild(allOpt);
|
|
|
|
if (!category) {
|
|
actEl.disabled = true;
|
|
actEl.value = '';
|
|
actEl.title = hint;
|
|
return;
|
|
}
|
|
|
|
actEl.disabled = false;
|
|
actEl.title = '';
|
|
|
|
const actions = AUDIT_ACTIONS_BY_CATEGORY[category] || [];
|
|
actions.forEach(function (action) {
|
|
const opt = document.createElement('option');
|
|
opt.value = action;
|
|
opt.textContent = auditActionLabel(action);
|
|
actEl.appendChild(opt);
|
|
});
|
|
if (prev && Array.prototype.some.call(actEl.options, function (o) { return o.value === prev; })) {
|
|
actEl.value = prev;
|
|
}
|
|
}
|
|
|
|
function onAuditCategoryFilterChange() {
|
|
rebuildAuditActionSelect();
|
|
}
|
|
|
|
function buildAuditQueryParams(forExport) {
|
|
const params = new URLSearchParams();
|
|
if (!forExport) {
|
|
params.set('page', String(auditLogsPage));
|
|
params.set('page_size', String(auditLogsPageSize));
|
|
}
|
|
const cat = document.getElementById('audit-filter-category');
|
|
const act = document.getElementById('audit-filter-action');
|
|
const res = document.getElementById('audit-filter-result');
|
|
const q = document.getElementById('audit-filter-q');
|
|
const since = document.getElementById('audit-filter-since');
|
|
const until = document.getElementById('audit-filter-until');
|
|
if (cat && cat.value) params.set('category', cat.value);
|
|
if (act && !act.disabled && act.value) params.set('action', act.value);
|
|
if (res && res.value) params.set('result', res.value);
|
|
if (q && q.value.trim()) params.set('q', q.value.trim());
|
|
const sinceISO = since ? auditDatetimeLocalToRFC3339(since.value) : '';
|
|
const untilISO = until ? auditDatetimeLocalToRFC3339(until.value) : '';
|
|
if (sinceISO) params.set('since', sinceISO);
|
|
if (untilISO) params.set('until', untilISO);
|
|
return params.toString();
|
|
}
|
|
|
|
async function loadAuditMeta() {
|
|
if (typeof apiFetch !== 'function') return;
|
|
const hint = document.getElementById('audit-retention-hint');
|
|
try {
|
|
const r = await apiFetch('/api/audit/meta');
|
|
if (!r.ok) return;
|
|
const data = await r.json();
|
|
if (!hint) return;
|
|
if (!data.enabled) {
|
|
hint.hidden = false;
|
|
hint.textContent = auditT('settingsAudit.disabledHint', null, '审计功能已关闭,新操作不会写入审计表。');
|
|
return;
|
|
}
|
|
const days = data.retention_days;
|
|
if (days > 0) {
|
|
hint.hidden = false;
|
|
hint.textContent = auditT('settingsAudit.retentionHint', { days: days },
|
|
'审计记录保留 ' + days + ' 天,超期自动清理。');
|
|
} else {
|
|
hint.hidden = true;
|
|
}
|
|
} catch (_) { /* ignore */ }
|
|
}
|
|
|
|
async function loadAuditSummary() {
|
|
if (typeof apiFetch !== 'function') return;
|
|
const wrap = document.getElementById('audit-summary-stats');
|
|
try {
|
|
const r = await apiFetch('/api/audit/summary?' + buildAuditQueryParams(true));
|
|
if (!r.ok) return;
|
|
const data = await r.json();
|
|
if (wrap) wrap.hidden = false;
|
|
const elTotal = document.getElementById('audit-stat-total');
|
|
const elFail = document.getElementById('audit-stat-failures');
|
|
const elRecent = document.getElementById('audit-stat-recent');
|
|
if (elTotal) elTotal.textContent = String(data.total != null ? data.total : 0);
|
|
if (elFail) elFail.textContent = String(data.failures != null ? data.failures : 0);
|
|
if (elRecent) elRecent.textContent = String(data.recent_7d != null ? data.recent_7d : 0);
|
|
} catch (_) { /* ignore */ }
|
|
}
|
|
|
|
async function loadAuditLogs(page) {
|
|
if (typeof apiFetch !== 'function') return;
|
|
auditLogsPage = page != null ? page : auditLogsPage;
|
|
const listEl = document.getElementById('audit-log-list');
|
|
if (listEl) {
|
|
listEl.innerHTML = '<div class="loading-spinner">' + (typeof escapeHtml === 'function' ? escapeHtml(auditT('settingsAudit.loading', null, '加载中...')) : '加载中...') + '</div>';
|
|
}
|
|
try {
|
|
const qs = buildAuditQueryParams(false);
|
|
const r = await apiFetch('/api/audit/logs?' + qs);
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(function () { return {}; });
|
|
throw new Error(err.error || r.statusText);
|
|
}
|
|
const data = await r.json();
|
|
renderAuditLogs(data.logs || []);
|
|
auditLogsTotal = typeof data.total === 'number' ? data.total : 0;
|
|
const maxPage = Math.max(1, Math.ceil(auditLogsTotal / auditLogsPageSize));
|
|
if (auditLogsPage > maxPage) {
|
|
loadAuditLogs(maxPage);
|
|
return;
|
|
}
|
|
renderAuditLogsPagination();
|
|
loadAuditSummary();
|
|
} catch (e) {
|
|
if (listEl) {
|
|
const msg = typeof escapeHtml === 'function' ? escapeHtml(e.message || String(e)) : (e.message || String(e));
|
|
listEl.innerHTML = '<div class="monitor-empty">' + msg + '</div>';
|
|
}
|
|
if (typeof showToast === 'function') {
|
|
showToast(e.message || String(e), 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderAuditLogs(logs) {
|
|
const listEl = document.getElementById('audit-log-list');
|
|
if (!listEl) return;
|
|
const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); };
|
|
if (!logs.length) {
|
|
listEl.innerHTML = '<div class="c2-empty">' + esc(auditT('settingsAudit.empty', null, '暂无审计记录')) + '</div>';
|
|
return;
|
|
}
|
|
listEl.innerHTML = logs.map(function (log) {
|
|
const lvl = log.result === 'failure' ? 'warn' : (log.level || 'info');
|
|
const catLabel = esc(auditCategoryLabel(log.category || ''));
|
|
const actionLabel = esc(auditActionLabel(log.action || ''));
|
|
const msg = esc(log.message || '');
|
|
const ip = esc(log.clientIp || '');
|
|
const when = esc(formatAuditTime(log.createdAt));
|
|
const res = esc(log.result || '');
|
|
const rid = log.resourceId || '';
|
|
const meta = rid ? (' · ' + esc(rid)) : '';
|
|
const eid = esc(log.id || '');
|
|
return (
|
|
'<div class="c2-event-item audit-log-item" role="button" tabindex="0" ' +
|
|
'onclick="showAuditLogDetail(\'' + eid + '\')" ' +
|
|
'onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();showAuditLogDetail(\'' + eid + '\')}">' +
|
|
'<div class="c2-event-level ' + esc(lvl) + '"></div>' +
|
|
'<div class="c2-event-content">' +
|
|
'<div class="c2-event-message">' + msg + '</div>' +
|
|
'<div class="c2-event-meta">' + when + ' · ' + catLabel + '/' + actionLabel + ' · ' + res + meta +
|
|
(ip ? ' · IP ' + ip : '') +
|
|
'</div></div></div>'
|
|
);
|
|
}).join('');
|
|
if (typeof applyTranslations === 'function') {
|
|
applyTranslations(listEl);
|
|
}
|
|
}
|
|
|
|
function renderAuditLogsPagination() {
|
|
const container = document.getElementById('audit-logs-pagination');
|
|
if (!container) return;
|
|
const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); };
|
|
const total = auditLogsTotal || 0;
|
|
const currentPage = auditLogsPage || 1;
|
|
const pageSize = auditLogsPageSize || 20;
|
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1;
|
|
const end = total === 0 ? 0 : Math.min(currentPage * pageSize, total);
|
|
const infoText = auditT('mcpMonitor.paginationInfo', { start: start, end: end, total: total },
|
|
'显示 ' + start + '-' + end + ' / 共 ' + total + ' 条记录');
|
|
const perPageLabel = auditT('mcpMonitor.perPageLabel', null, '每页显示');
|
|
const firstPageLabel = auditT('mcp.firstPage', null, '首页');
|
|
const prevPageLabel = auditT('mcp.prevPage', null, '上一页');
|
|
const pageInfoText = auditT('mcp.pageInfo', { page: currentPage, total: totalPages },
|
|
'第 ' + currentPage + ' / ' + totalPages + ' 页');
|
|
const nextPageLabel = auditT('mcp.nextPage', null, '下一页');
|
|
const lastPageLabel = auditT('mcp.lastPage', null, '末页');
|
|
const disabledFirst = currentPage === 1 || total === 0;
|
|
const disabledLast = currentPage >= totalPages || total === 0;
|
|
let html = '<div class="monitor-pagination">';
|
|
html += '<div class="pagination-info">';
|
|
html += '<span>' + esc(infoText) + '</span>';
|
|
html += '<label class="pagination-page-size">' + esc(perPageLabel);
|
|
html += '<select id="audit-page-size" onchange="onAuditPageSizeChange()">';
|
|
[10, 20, 50, 100].forEach(function (n) {
|
|
html += '<option value="' + n + '"' + (pageSize === n ? ' selected' : '') + '>' + n + '</option>';
|
|
});
|
|
html += '</select></label></div>';
|
|
html += '<div class="pagination-controls">';
|
|
html += '<button type="button" class="btn-secondary" onclick="goAuditLogsPage(1)"' + (disabledFirst ? ' disabled' : '') + '>' + esc(firstPageLabel) + '</button>';
|
|
html += '<button type="button" class="btn-secondary" onclick="goAuditLogsPage(' + (currentPage - 1) + ')"' + (disabledFirst ? ' disabled' : '') + '>' + esc(prevPageLabel) + '</button>';
|
|
html += '<span class="pagination-page">' + esc(pageInfoText) + '</span>';
|
|
html += '<button type="button" class="btn-secondary" onclick="goAuditLogsPage(' + (currentPage + 1) + ')"' + (disabledLast ? ' disabled' : '') + '>' + esc(nextPageLabel) + '</button>';
|
|
html += '<button type="button" class="btn-secondary" onclick="goAuditLogsPage(' + totalPages + ')"' + (disabledLast ? ' disabled' : '') + '>' + esc(lastPageLabel) + '</button>';
|
|
html += '</div></div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function goAuditLogsPage(p) {
|
|
const totalPages = Math.max(1, Math.ceil((auditLogsTotal || 0) / (auditLogsPageSize || 20)));
|
|
if (p < 1 || p > totalPages) return;
|
|
loadAuditLogs(p);
|
|
}
|
|
|
|
function filterAuditLogs() {
|
|
auditLogsPage = 1;
|
|
loadAuditLogs(1);
|
|
}
|
|
|
|
function resetAuditLogFilters() {
|
|
const cat = document.getElementById('audit-filter-category');
|
|
const act = document.getElementById('audit-filter-action');
|
|
const res = document.getElementById('audit-filter-result');
|
|
const q = document.getElementById('audit-filter-q');
|
|
const since = document.getElementById('audit-filter-since');
|
|
const until = document.getElementById('audit-filter-until');
|
|
if (cat) cat.value = '';
|
|
if (res) res.value = '';
|
|
if (q) q.value = '';
|
|
if (since) since.value = '';
|
|
if (until) until.value = '';
|
|
rebuildAuditActionSelect();
|
|
filterAuditLogs();
|
|
}
|
|
|
|
function auditResourceLink(log) {
|
|
if (!log) return '';
|
|
const type = log.resourceType || '';
|
|
const id = log.resourceId || '';
|
|
if (!id) return '';
|
|
const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); };
|
|
const label = esc(auditT('settingsAudit.openResource', null, '打开关联资源'));
|
|
if (type === 'conversation' || (type === '' && id.length > 8 && !id.startsWith('c2_'))) {
|
|
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'chat\');}">' + label + ' (chat)</button></p>';
|
|
}
|
|
if (type === 'vulnerability' || type === 'batch_queue') {
|
|
const page = type === 'batch_queue' ? 'tasks' : 'vulnerabilities';
|
|
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'' + page + '\');}">' + label + '</button></p>';
|
|
}
|
|
if (type === 'c2_listener' || type === 'c2_session' || type === 'c2_task') {
|
|
const page = type === 'c2_listener' ? 'c2-listeners' : (type === 'c2_session' ? 'c2-sessions' : 'c2-tasks');
|
|
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'' + page + '\');}">' + label + '</button></p>';
|
|
}
|
|
if (type === 'webshell_connection') {
|
|
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'webshell\');}">' + label + '</button></p>';
|
|
}
|
|
if (type === 'knowledge_item') {
|
|
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'knowledge-management\');}">' + label + '</button></p>';
|
|
}
|
|
if (type === 'chat_upload') {
|
|
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'chat-files\');}">' + label + '</button></p>';
|
|
}
|
|
if (type === 'tool_execution') {
|
|
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'mcp-monitor\');}">' + label + '</button></p>';
|
|
}
|
|
if (type === 'role' || type === 'skill' || type === 'markdown_agent') {
|
|
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchSettingsSection===\'function\'){switchPage(\'settings\');switchSettingsSection(\'roles\');}">' + label + '</button></p>';
|
|
}
|
|
return id ? '<p><strong>ID:</strong> ' + esc(id) + '</p>' : '';
|
|
}
|
|
|
|
function refreshAuditLogs() {
|
|
loadAuditLogs(auditLogsPage);
|
|
}
|
|
|
|
async function downloadAuditExport(url, filename) {
|
|
const r = await apiFetch(url);
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(function () { return {}; });
|
|
throw new Error(err.error || r.statusText);
|
|
}
|
|
const blob = await r.blob();
|
|
const objectUrl = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = objectUrl;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(objectUrl);
|
|
}
|
|
|
|
function closeAuditExportMenu() {
|
|
const menu = document.getElementById('audit-export-menu');
|
|
const trigger = document.getElementById('audit-export-trigger');
|
|
if (menu) menu.hidden = true;
|
|
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
|
}
|
|
|
|
function toggleAuditExportMenu(ev) {
|
|
if (ev && ev.stopPropagation) ev.stopPropagation();
|
|
const menu = document.getElementById('audit-export-menu');
|
|
const trigger = document.getElementById('audit-export-trigger');
|
|
if (!menu) return;
|
|
const willOpen = menu.hidden;
|
|
if (willOpen) {
|
|
menu.hidden = false;
|
|
if (trigger) trigger.setAttribute('aria-expanded', 'true');
|
|
if (!window._auditExportMenuDocBound) {
|
|
window._auditExportMenuDocBound = true;
|
|
document.addEventListener('click', function () {
|
|
closeAuditExportMenu();
|
|
});
|
|
}
|
|
} else {
|
|
closeAuditExportMenu();
|
|
}
|
|
}
|
|
|
|
async function runAuditExport(format) {
|
|
closeAuditExportMenu();
|
|
if (format === 'csv') {
|
|
await exportAuditLogsCsv();
|
|
} else {
|
|
await exportAuditLogs();
|
|
}
|
|
}
|
|
|
|
async function exportAuditLogs() {
|
|
if (typeof apiFetch !== 'function') return;
|
|
try {
|
|
await downloadAuditExport(
|
|
'/api/audit/logs/export?' + buildAuditQueryParams(true),
|
|
'audit-logs-' + new Date().toISOString().slice(0, 10) + '.json'
|
|
);
|
|
if (typeof showToast === 'function') {
|
|
showToast(auditT('settingsAudit.exportDone', null, '导出完成'), 'success');
|
|
}
|
|
} catch (e) {
|
|
if (typeof showToast === 'function') {
|
|
showToast(e.message || String(e), 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function exportAuditLogsCsv() {
|
|
if (typeof apiFetch !== 'function') return;
|
|
try {
|
|
const qs = buildAuditQueryParams(true);
|
|
await downloadAuditExport(
|
|
'/api/audit/logs/export?' + (qs ? qs + '&' : '') + 'format=csv',
|
|
'audit-logs-' + new Date().toISOString().slice(0, 10) + '.csv'
|
|
);
|
|
if (typeof showToast === 'function') {
|
|
showToast(auditT('settingsAudit.exportDone', null, '导出完成'), 'success');
|
|
}
|
|
} catch (e) {
|
|
if (typeof showToast === 'function') {
|
|
showToast(e.message || String(e), 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
function closeAuditDetailModal() {
|
|
const el = document.getElementById('audit-detail-modal');
|
|
if (el) el.remove();
|
|
}
|
|
|
|
async function showAuditLogDetail(id) {
|
|
if (!id || typeof apiFetch !== 'function') return;
|
|
const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); };
|
|
try {
|
|
const r = await apiFetch('/api/audit/logs/' + encodeURIComponent(id));
|
|
if (!r.ok) throw new Error('not found');
|
|
const data = await r.json();
|
|
const log = data.log || {};
|
|
const detail = log.detail ? JSON.stringify(log.detail, null, 2) : '';
|
|
closeAuditDetailModal();
|
|
const overlay = document.createElement('div');
|
|
overlay.id = 'audit-detail-modal';
|
|
overlay.className = 'modal';
|
|
overlay.style.display = 'block';
|
|
const catAction = esc(auditCategoryLabel(log.category || '')) + ' / ' + esc(auditActionLabel(log.action || ''));
|
|
overlay.innerHTML =
|
|
'<div class="modal-content" style="max-width: 720px;">' +
|
|
'<div class="modal-header">' +
|
|
'<h2>' + esc(auditT('settingsAudit.detailTitle', null, '审计详情')) + '</h2>' +
|
|
'<span class="modal-close" onclick="closeAuditDetailModal()">×</span>' +
|
|
'</div>' +
|
|
'<div class="modal-body audit-detail-body">' +
|
|
'<p><strong>' + esc(auditT('settingsAudit.detailTime', null, '时间')) + ':</strong> ' + esc(formatAuditTime(log.createdAt)) + '</p>' +
|
|
'<p><strong>' + esc(auditT('settingsAudit.detailCategory', null, '类别')) + ':</strong> ' + catAction + '</p>' +
|
|
'<p><strong>' + esc(auditT('settingsAudit.detailResult', null, '结果')) + ':</strong> ' + esc(log.result || '') + '</p>' +
|
|
'<p><strong>' + esc(auditT('settingsAudit.detailMessage', null, '说明')) + ':</strong> ' + esc(log.message || '') + '</p>' +
|
|
(log.clientIp ? '<p><strong>IP:</strong> ' + esc(log.clientIp) + '</p>' : '') +
|
|
(log.sessionHint ? '<p><strong>' + esc(auditT('settingsAudit.detailSession', null, '会话')) + ':</strong> ' + esc(log.sessionHint) + '</p>' : '') +
|
|
(log.userAgent ? '<p><strong>UA:</strong> ' + esc(log.userAgent) + '</p>' : '') +
|
|
auditResourceLink(log) +
|
|
(detail ? '<pre class="audit-detail-pre">' + esc(detail) + '</pre>' : '') +
|
|
'</div>' +
|
|
'<div class="modal-footer"><button type="button" class="btn-secondary" onclick="closeAuditDetailModal()">' +
|
|
esc(auditT('common.close', null, '关闭')) + '</button></div>' +
|
|
'</div>';
|
|
document.body.appendChild(overlay);
|
|
overlay.addEventListener('click', function (ev) {
|
|
if (ev.target === overlay) closeAuditDetailModal();
|
|
});
|
|
} catch (e) {
|
|
if (typeof showToast === 'function') {
|
|
showToast(e.message || String(e), 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
function initAuditLogsSection() {
|
|
if (!document.getElementById('audit-log-list')) return;
|
|
initAuditPageSizeFromStorage();
|
|
rebuildAuditActionSelect();
|
|
loadAuditMeta();
|
|
loadAuditLogs(1);
|
|
}
|