Harden auth, CSRF, and email log UX
This commit is contained in:
@@ -1510,6 +1510,23 @@
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function getCsrfToken() {
|
||||
const match = document.cookie.match(/(?:^|; )csrf_token=([^;]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : '';
|
||||
}
|
||||
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
window.fetch = (input, init = {}) => {
|
||||
const method = String(init.method || 'GET').toUpperCase();
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
const headers = new Headers(init.headers || {});
|
||||
const token = getCsrfToken();
|
||||
if (token) headers.set('X-CSRF-Token', token);
|
||||
init = { ...init, headers };
|
||||
}
|
||||
return originalFetch(input, init);
|
||||
};
|
||||
|
||||
// 页面加载时初始化
|
||||
window.addEventListener('load', () => {
|
||||
loadStats();
|
||||
@@ -1853,11 +1870,11 @@
|
||||
<tr>
|
||||
<td>${user.id}</td>
|
||||
<td>
|
||||
<div><strong>${user.username}</strong> ${getVipBadge(user)}</div>
|
||||
<div><strong>${escapeHtml(user.username)}</strong> ${getVipBadge(user)}</div>
|
||||
${getVipExpire(user)}
|
||||
</td>
|
||||
<td>${user.email || '-'}</td>
|
||||
<td>${user.created_at}</td>
|
||||
<td>${escapeHtml(user.email || '-')}</td>
|
||||
<td>${escapeHtml(user.created_at)}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-small btn-success" onclick="approveUser(${user.id})">通过</button>
|
||||
@@ -1903,9 +1920,9 @@
|
||||
<tr>
|
||||
<td>${user.id}</td>
|
||||
<td>
|
||||
<div><strong>${user.username}</strong> ${getVipBadge(user)}</div>
|
||||
<div><strong>${escapeHtml(user.username)}</strong> ${getVipBadge(user)}</div>
|
||||
${getVipExpire(user)}
|
||||
${user.email ? '<div class="user-info">'+user.email+'</div>' : ''}
|
||||
${user.email ? '<div class="user-info">'+escapeHtml(user.email)+'</div>' : ''}
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge status-${user.status}">
|
||||
@@ -1913,8 +1930,8 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
${user.created_at}
|
||||
${user.approved_at ? '<div class="user-info">审核:'+user.approved_at+'</div>' : ''}
|
||||
${escapeHtml(user.created_at)}
|
||||
${user.approved_at ? '<div class="user-info">审核:'+escapeHtml(user.approved_at)+'</div>' : ''}
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
@@ -2481,7 +2498,8 @@
|
||||
runningList.innerHTML = '<div style="color: #999; text-align: center; padding: 10px;">暂无运行中的任务</div>';
|
||||
} else {
|
||||
runningList.innerHTML = data.running.map(task => {
|
||||
const source = sourceMap[task.source] || {text: task.source, color: '#666'};
|
||||
const sourceKey = String(task.source || '');
|
||||
const source = sourceMap[sourceKey] || {text: escapeHtml(sourceKey), color: '#666'};
|
||||
// 状态颜色映射
|
||||
const statusColorMap = {
|
||||
'初始化': '#6c757d',
|
||||
@@ -2491,6 +2509,11 @@
|
||||
'正在截图': '#17a2b8'
|
||||
};
|
||||
const statusColor = statusColorMap[task.detail_status] || '#666';
|
||||
const safeUser = escapeHtml(task.user_username || '');
|
||||
const safeAccount = escapeHtml(task.username || '');
|
||||
const safeBrowse = escapeHtml(task.browse_type || '');
|
||||
const safeDetail = escapeHtml(task.detail_status || '');
|
||||
const safeElapsed = escapeHtml(task.elapsed_display || '');
|
||||
// 进度显示
|
||||
const progressText = task.progress_items > 0 || task.progress_attachments > 0
|
||||
? `(${task.progress_items}/${task.progress_attachments})`
|
||||
@@ -2499,18 +2522,18 @@
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 5px;">
|
||||
<span style="color: ${source.color}; font-weight: 500; font-size: 12px;">[${source.text}]</span>
|
||||
<span style="color: #333;">${task.user_username}</span>
|
||||
<span style="color: #333;">${safeUser}</span>
|
||||
<span style="color: #666;">→</span>
|
||||
<span style="color: #007bff; font-weight: 500;">${task.username}</span>
|
||||
<span style="background: #e9ecef; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #666;">${task.browse_type}</span>
|
||||
<span style="color: #007bff; font-weight: 500;">${safeAccount}</span>
|
||||
<span style="background: #e9ecef; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #666;">${safeBrowse}</span>
|
||||
</div>
|
||||
<div style="margin-top: 4px; display: flex; align-items: center; gap: 8px;">
|
||||
<span style="color: ${statusColor}; font-weight: 500; font-size: 12px;">● ${task.detail_status}</span>
|
||||
<span style="color: ${statusColor}; font-weight: 500; font-size: 12px;">● ${safeDetail}</span>
|
||||
${progressText ? `<span style="color: #999; font-size: 11px;">内容/附件: ${progressText}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right; min-width: 70px;">
|
||||
<div style="color: #28a745; font-weight: 500;">${task.elapsed_display}</div>
|
||||
<div style="color: #28a745; font-weight: 500;">${safeElapsed}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
@@ -2522,22 +2545,28 @@
|
||||
queuingList.innerHTML = '<div style="color: #999; text-align: center; padding: 10px;">暂无排队中的任务</div>';
|
||||
} else {
|
||||
queuingList.innerHTML = data.queuing.map(task => {
|
||||
const source = sourceMap[task.source] || {text: task.source, color: '#666'};
|
||||
const sourceKey = String(task.source || '');
|
||||
const source = sourceMap[sourceKey] || {text: escapeHtml(sourceKey), color: '#666'};
|
||||
const safeUser = escapeHtml(task.user_username || '');
|
||||
const safeAccount = escapeHtml(task.username || '');
|
||||
const safeBrowse = escapeHtml(task.browse_type || '');
|
||||
const safeDetail = escapeHtml(task.detail_status || '等待资源');
|
||||
const safeElapsed = escapeHtml(task.elapsed_display || '');
|
||||
return `<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #fff8e6; border-radius: 5px; margin-bottom: 5px; border-left: 3px solid #fd7e14;">
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; flex-wrap: wrap; gap: 5px;">
|
||||
<span style="color: ${source.color}; font-weight: 500; font-size: 12px;">[${source.text}]</span>
|
||||
<span style="color: #333;">${task.user_username}</span>
|
||||
<span style="color: #333;">${safeUser}</span>
|
||||
<span style="color: #666;">→</span>
|
||||
<span style="color: #007bff; font-weight: 500;">${task.username}</span>
|
||||
<span style="background: #ffeeba; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #856404;">${task.browse_type}</span>
|
||||
<span style="color: #007bff; font-weight: 500;">${safeAccount}</span>
|
||||
<span style="background: #ffeeba; padding: 2px 6px; border-radius: 3px; font-size: 11px; color: #856404;">${safeBrowse}</span>
|
||||
</div>
|
||||
<div style="margin-top: 4px;">
|
||||
<span style="color: #fd7e14; font-size: 12px;">● ${task.detail_status || '等待资源'}</span>
|
||||
<span style="color: #fd7e14; font-size: 12px;">● ${safeDetail}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right; min-width: 80px;">
|
||||
<div style="color: #fd7e14; font-weight: 500;">等待 ${task.elapsed_display}</div>
|
||||
<div style="color: #fd7e14; font-weight: 500;">等待 ${safeElapsed}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
@@ -2563,7 +2592,7 @@
|
||||
const select = document.getElementById('logUserFilter');
|
||||
select.innerHTML = '<option value="">全部</option>';
|
||||
users.forEach(user => {
|
||||
select.innerHTML += `<option value="${user.id}">${user.username}</option>`;
|
||||
select.innerHTML += `<option value="${user.id}">${escapeHtml(user.username)}</option>`;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -2653,18 +2682,24 @@
|
||||
'immediate': {text: '即时', color: '#fd7e14'},
|
||||
'resumed': {text: '恢复', color: '#6c757d'}
|
||||
};
|
||||
const sourceInfo = sourceMap[log.source] || {text: log.source || '手动', color: '#28a745'};
|
||||
const sourceKey = log.source || 'manual';
|
||||
const sourceInfo = sourceMap[sourceKey] || {text: escapeHtml(sourceKey), color: '#28a745'};
|
||||
const safeCreatedAt = escapeHtml(log.created_at || '');
|
||||
const safeUser = escapeHtml(log.user_username || 'N/A');
|
||||
const safeAccount = escapeHtml(log.username || '');
|
||||
const safeBrowse = escapeHtml(log.browse_type || '');
|
||||
const safeError = escapeHtml(log.error_message || '-');
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${log.created_at}</td>
|
||||
<td>${safeCreatedAt}</td>
|
||||
<td><span style="color: ${sourceInfo.color}; font-weight: 500;">${sourceInfo.text}</span></td>
|
||||
<td>${log.user_username || 'N/A'}</td>
|
||||
<td>${log.username}</td>
|
||||
<td>${log.browse_type}</td>
|
||||
<td>${safeUser}</td>
|
||||
<td>${safeAccount}</td>
|
||||
<td>${safeBrowse}</td>
|
||||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||||
<td>${log.total_items} / ${log.total_attachments}</td>
|
||||
<td style="color: #2F80ED; font-weight: 500;">${formatDuration(log.duration)}</td>
|
||||
<td style="color: #dc3545; font-size: 11px;">${log.error_message || '-'}</td>
|
||||
<td style="color: #dc3545; font-size: 11px;">${safeError}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
@@ -2782,9 +2817,9 @@
|
||||
${passwordResets.map(reset => `
|
||||
<tr>
|
||||
<td>${reset.id}</td>
|
||||
<td><strong>${reset.username}</strong></td>
|
||||
<td>${reset.email || '-'}</td>
|
||||
<td>${reset.created_at}</td>
|
||||
<td><strong>${escapeHtml(reset.username)}</strong></td>
|
||||
<td>${escapeHtml(reset.email || '-')}</td>
|
||||
<td>${escapeHtml(reset.created_at)}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-small btn-success" onclick="approvePasswordReset(${reset.id})">批准</button>
|
||||
@@ -2935,13 +2970,13 @@
|
||||
feedbacksList.forEach(fb => {
|
||||
html += '<tr>';
|
||||
html += '<td>' + fb.id + '</td>';
|
||||
html += '<td><strong>' + (fb.username || 'N/A') + '</strong></td>';
|
||||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.title||'') + '">' + (fb.title||'') + '</td>';
|
||||
html += '<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.description||'') + '">' + (fb.description||'') + '</td>';
|
||||
html += '<td>' + (fb.contact || '-') + '</td>';
|
||||
html += '<td><strong>' + escapeHtml(fb.username || 'N/A') + '</strong></td>';
|
||||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + escapeHtml(fb.title || '') + '">' + escapeHtml(fb.title || '') + '</td>';
|
||||
html += '<td style="max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + escapeHtml(fb.description || '') + '">' + escapeHtml(fb.description || '') + '</td>';
|
||||
html += '<td>' + escapeHtml(fb.contact || '-') + '</td>';
|
||||
html += '<td>' + getStatusBadge(fb.status) + '</td>';
|
||||
html += '<td>' + fb.created_at + '</td>';
|
||||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + (fb.admin_reply || '') + '">' + (fb.admin_reply || '-') + '</td>';
|
||||
html += '<td>' + escapeHtml(fb.created_at) + '</td>';
|
||||
html += '<td style="max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;" title="' + escapeHtml(fb.admin_reply || '') + '">' + escapeHtml(fb.admin_reply || '-') + '</td>';
|
||||
html += '<td><div class="action-buttons">';
|
||||
if (fb.status !== 'closed') {
|
||||
html += '<button class="btn btn-small btn-primary" onclick="replyFeedback(' + fb.id + ')">回复</button>';
|
||||
@@ -3393,20 +3428,24 @@
|
||||
};
|
||||
|
||||
let html = '<div class="table-container"><table style="width: 100%; font-size: 12px;"><thead><tr>';
|
||||
html += '<th>时间</th><th>收件人</th><th>类型</th><th>主题</th><th>状态</th><th>错误</th>';
|
||||
html += '<th>时间</th><th>收件人</th><th>来源用户</th><th>类型</th><th>主题</th><th>状态</th><th>错误</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
logs.forEach(log => {
|
||||
const statusClass = log.status === 'success' ? 'color: #27ae60;' : 'color: #e74c3c;';
|
||||
const statusText = log.status === 'success' ? '成功' : '失败';
|
||||
const userLabel = log.username
|
||||
? `${log.username} (#${log.user_id})`
|
||||
: (log.user_id ? `用户#${log.user_id}` : '系统');
|
||||
|
||||
html += '<tr>';
|
||||
html += `<td style="white-space: nowrap;">${log.created_at}</td>`;
|
||||
html += `<td>${log.email_to}</td>`;
|
||||
html += `<td>${typeMap[log.email_type] || log.email_type}</td>`;
|
||||
html += `<td style="max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${log.subject}">${log.subject}</td>`;
|
||||
html += `<td style="white-space: nowrap;">${escapeHtml(log.created_at)}</td>`;
|
||||
html += `<td>${escapeHtml(log.email_to)}</td>`;
|
||||
html += `<td>${escapeHtml(userLabel)}</td>`;
|
||||
html += `<td>${escapeHtml(typeMap[log.email_type] || log.email_type)}</td>`;
|
||||
html += `<td style="max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${escapeHtml(log.subject)}">${escapeHtml(log.subject)}</td>`;
|
||||
html += `<td style="${statusClass} font-weight: bold;">${statusText}</td>`;
|
||||
html += `<td style="max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #999;" title="${log.error_message || ''}">${log.error_message || '-'}</td>`;
|
||||
html += `<td style="max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #999;" title="${escapeHtml(log.error_message || '')}">${escapeHtml(log.error_message || '-')}</td>`;
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user