mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-21 07:06:53 +02:00
599 lines
25 KiB
JavaScript
599 lines
25 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: ['create', '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();
|
||
}
|
||
|
||
/** 资源已被删除/移除的审计操作,不再提供「打开关联资源」 */
|
||
const AUDIT_ACTIONS_RESOURCE_REMOVED = {
|
||
delete: true,
|
||
item_delete: true,
|
||
connection_delete: true,
|
||
listener_delete: true,
|
||
session_delete: true,
|
||
task_delete: true,
|
||
execution_delete: true,
|
||
execution_delete_batch: true,
|
||
delete_queue: true,
|
||
delete_batch_task: true,
|
||
markdown_delete: true
|
||
};
|
||
|
||
function auditResourceWasRemoved(log) {
|
||
if (!log || !log.action) return false;
|
||
return !!AUDIT_ACTIONS_RESOURCE_REMOVED[log.action];
|
||
}
|
||
|
||
/** 删除类操作,或关联资源已不存在(由详情 API resourceAvailable 判定) */
|
||
function auditResourceUnavailable(log) {
|
||
if (!log) return false;
|
||
if (auditResourceWasRemoved(log)) return true;
|
||
return log.resourceAvailable === false;
|
||
}
|
||
|
||
function auditResourceMeta(log) {
|
||
if (!log || !log.resourceId) return '';
|
||
const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); };
|
||
const id = esc(log.resourceId);
|
||
if (auditResourceUnavailable(log)) {
|
||
const idLabel = esc(auditT('settingsAudit.resourceIdLabel', null, '资源 ID'));
|
||
const removed = esc(auditT('settingsAudit.resourceRemoved', null, '(关联对象已删除)'));
|
||
return '<p class="audit-resource-meta"><strong>' + idLabel + ':</strong> <code>' + id +
|
||
'</code> <span class="audit-resource-removed">' + removed + '</span></p>';
|
||
}
|
||
const link = auditResourceLink(log);
|
||
return link || ('<p><strong>ID:</strong> ' + id + '</p>');
|
||
}
|
||
|
||
async function auditOpenConversationChat(conversationId) {
|
||
const id = String(conversationId || '').trim();
|
||
if (!id) return;
|
||
if (typeof apiFetch === 'function') {
|
||
try {
|
||
const r = await apiFetch('/api/conversations/' + encodeURIComponent(id));
|
||
if (!r.ok) {
|
||
if (typeof showToast === 'function') {
|
||
showToast(auditT('settingsAudit.resourceRemoved', null, '(关联对象已删除)'), 'warning');
|
||
}
|
||
return;
|
||
}
|
||
} catch (_) {
|
||
return;
|
||
}
|
||
}
|
||
closeAuditDetailModal();
|
||
if (typeof switchPage === 'function') {
|
||
switchPage('chat');
|
||
}
|
||
if (typeof loadConversation === 'function') {
|
||
void loadConversation(id);
|
||
}
|
||
}
|
||
window.auditOpenConversationChat = auditOpenConversationChat;
|
||
|
||
function auditResourceLink(log) {
|
||
if (!log || auditResourceUnavailable(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_'))) {
|
||
const chatLabel = esc(auditT('settingsAudit.openResourceChat', null, '打开关联资源(chat)'));
|
||
return '<p><button type="button" class="btn-secondary btn-small audit-open-chat-btn" data-conversation-id="' +
|
||
esc(id) + '">' + chatLabel + '</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 '';
|
||
}
|
||
|
||
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>' : '') +
|
||
auditResourceMeta(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);
|
||
const chatBtn = overlay.querySelector('.audit-open-chat-btn');
|
||
if (chatBtn) {
|
||
chatBtn.addEventListener('click', function () {
|
||
auditOpenConversationChat(chatBtn.getAttribute('data-conversation-id'));
|
||
});
|
||
}
|
||
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);
|
||
}
|