feat: channels/audit UI unify, apply flow hardening, bump v1.1.12

This commit is contained in:
2026-03-10 03:32:40 +08:00
parent 52b0d742a7
commit 8b2557b2bf
15 changed files with 2311 additions and 262 deletions

135
templates/audit.html Normal file
View File

@@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧾 审计日志 - 虾记</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f0f2f5;color:#333;min-height:100vh}
.header{background:linear-gradient(135deg,#ff6b6b,#ee5a24);color:#fff;padding:20px;position:sticky;top:0;z-index:100;box-shadow:0 2px 10px rgba(0,0,0,.15);display:flex;justify-content:space-between;align-items:center;gap:10px}
.header .title{font-weight:700}
.header .sub{font-size:12px;opacity:.9}
.header a{color:#fff;text-decoration:none;background:rgba(255,255,255,.2);padding:6px 10px;border-radius:8px;font-size:13px}
.header a:hover{background:rgba(255,255,255,.35)}
.wrap{max-width:600px;margin:0 auto;padding:15px}
.filters{background:#fff;border-radius:12px;padding:12px;box-shadow:0 1px 4px rgba(0,0,0,.05);margin-bottom:10px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.filters input,.filters select{width:100%;padding:8px;border:1px solid #ddd;border-radius:8px;font-size:13px;background:#fff}
small{color:#6b7280}
.actions{margin:0 0 10px;display:flex;gap:8px;flex-wrap:wrap}
button{border:none;border-radius:8px;padding:8px 12px;cursor:pointer;color:#fff;background:#ee5a24;font-size:13px}
button:hover{background:#d63031}
button.secondary{background:#6b7280}
button.secondary:hover{background:#4b5563}
.list{display:flex;flex-direction:column;gap:10px}
.log-card{background:#fff;border-radius:12px;padding:12px;box-shadow:0 1px 4px rgba(0,0,0,.05)}
.row{display:flex;justify-content:space-between;gap:8px;align-items:flex-start}
.tag{display:inline-block;padding:2px 8px;border-radius:999px;font-size:12px}
.tag.success{background:#dcfce7;color:#166534}
.tag.failed{background:#fee2e2;color:#991b1b}
.tag.denied{background:#fef3c7;color:#92400e}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:#4b5563;word-break:break-all}
.note{font-size:13px;color:#374151;margin-top:6px;white-space:pre-wrap;word-break:break-word}
.empty{text-align:center;padding:40px 10px;color:#999}
@media(max-width:640px){
.header{padding:14px 12px 10px;align-items:flex-start;flex-direction:column}
.header>div:last-child{display:flex;gap:8px}
.wrap{padding:12px}
.filters{grid-template-columns:1fr}
}
</style>
</head>
<body>
<div class="header">
<div>
<div class="title">🧾 审计日志</div>
<div class="sub">{{.version}}</div>
</div>
<div>
<a href="/">返回首页</a>
<a href="/channels">渠道配置</a>
<a href="/logout">退出</a>
</div>
</div>
<div class="wrap">
<div class="filters">
<div><small>操作类型</small><input id="fAction" placeholder="如record.delete.self"></div>
<div><small>目标类型</small><input id="fTarget" placeholder="如transaction"></div>
<div><small>结果</small><select id="fResult"><option value="">全部</option><option value="success">成功</option><option value="denied">拒绝</option><option value="failed">失败</option></select></div>
<div><small>操作人ID</small><input id="fActor" placeholder="如1"></div>
<div><small>开始时间RFC3339</small><input id="fFrom" placeholder="2026-03-09T00:00:00+08:00"></div>
<div><small>结束时间RFC3339</small><input id="fTo" placeholder="2026-03-10T00:00:00+08:00"></div>
</div>
<div class="actions">
<button onclick="loadAudit()">查询</button>
<button class="secondary" onclick="resetFilters()">重置</button>
</div>
<div id="list" class="list"></div>
</div>
<script>
function esc(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
function qs(id){return document.getElementById(id).value.trim();}
const actionMap={
'auth.login.success':'登录成功','auth.login.failed':'登录失败','auth.logout':'退出登录',
'record.delete.self':'删除本人记录','record.delete.all':'删除全员记录','record.export':'导出记录',
'flag.update':'修改高级开关',
'channel_update_draft':'更新渠道草稿','channel_publish':'发布渠道草稿','channel_reload':'热加载渠道',
'channel_disable_all':'一键关闭全部渠道','channel_enable':'启用渠道','channel_disable':'停用渠道','channel_test':'测试渠道连接'
};
const targetMap={
'transaction':'记账记录','feature_flag':'高级开关','channel':'渠道','user':'用户','system':'系统'
};
function actionLabel(v){return actionMap[v]||v||'-';}
function targetLabel(v){return targetMap[v]||v||'-';}
function parseResult(note){
const m=String(note||'').match(/result=(success|failed|denied)/);
return m?m[1]:'';
}
function resultLabel(r){
if(r==='success') return '成功';
if(r==='failed') return '失败';
if(r==='denied') return '拒绝';
return '未标注';
}
function resetFilters(){['fAction','fTarget','fResult','fActor','fFrom','fTo'].forEach(id=>document.getElementById(id).value='');loadAudit();}
async function loadAudit(){
const p=new URLSearchParams();
const m={action:qs('fAction'),target_type:qs('fTarget'),result:qs('fResult'),actor_id:qs('fActor'),from:qs('fFrom'),to:qs('fTo')};
Object.entries(m).forEach(([k,v])=>{if(v)p.set(k,v)});
p.set('limit','200');
const listEl=document.getElementById('list');
listEl.innerHTML='<div class="empty">加载中...</div>';
const r=await fetch('/api/v1/admin/audit?'+p.toString());
const data=await r.json().catch(()=>[]);
const list=Array.isArray(data)?data:[];
if(!list.length){listEl.innerHTML='<div class="empty">暂无审计记录</div>';return;}
listEl.innerHTML=list.map(it=>{
const result=parseResult(it.note);
const resultClass=result||'success';
const note=String(it.note||'').replace(/\s*\|\s*result=(success|failed|denied)\s*$/,'').trim();
return `<div class="log-card">
<div class="row"><strong>${actionLabel(it.action)}</strong><span class="tag ${resultClass}">${resultLabel(result)}</span></div>
<div class="mono" style="margin-top:4px;">#${it.id} · ${esc(it.created_at||'')}</div>
<div class="mono" style="margin-top:2px;">操作人: ${it.actor_id} · 目标: ${targetLabel(it.target_type)} (${esc(it.target_id||'')})</div>
<div class="note">${esc(note||'(无备注)')}</div>
</div>`;
}).join('');
}
loadAudit();
</script>
</body>
</html>