6e99e32285
实现「一个二进制三个监听」中的管理端:
- 独立监听 ADMIN_LISTEN(默认 127.0.0.1:9443,FromEnv 拒绝 0.0.0.0/:: 等公网绑定)
- 中间件链:mw_ipallow(CIDR 白名单,仅信任 RemoteAddr,默认内网段)→
mw_session(HttpOnly+Secure+SameSite=Strict cookie,Redis 30min 滑动 TTL)
- 登录双因素:argon2id 密码 + RFC6238 TOTP;失败限流与临时锁定;
TOTP 密钥 AES-GCM 加密入库;migration 000008 增补 admins 表;
首个管理员由 cmd/adminctl create 创建(终端输出 otpauth URI/Secret)
- 功能三块(html/template + embed 静态资源,原生 JS 二次确认):
1. 码批次:表单生成→明文仅在本次 CSV 下载出现(不落盘/不入日志);
批次列表 + 整批作废(复用 #3 codes,新增 Store.ListBatches/VoidBatch)
2. 节点操作:列表 + 近期 node_events;replace 经 #14 ProvisionService、
draining/up 经 #5 Lifecycle(二者未就绪→注入 stub,UI 置灰);二次确认 + CSRF
3. audit_log 查看:actor/action/target/时间范围过滤分页;同页查 node_events
- 所有写操作写 audit_log(actor=用户名,meta 不含明文);管理端仅记安全事件
(登录失败/锁定/白名单拦截),不记常规访问日志
- internal/totp 复用包(与 doc/05 用户中心 2FA 同算法,纯标准库)
测试:totp RFC6238 向量、argon2/AES 往返、白名单放行/拦截、会话滑动过期、
登录成功/密码错/TOTP 错/未知用户/锁定、批次 CSV 含明文且 audit 不泄露、
作废/节点操作的二次确认与 CSRF 缺失被拒、mock 断言 service 调用参数、审计过滤。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
48 lines
2.1 KiB
HTML
48 lines
2.1 KiB
HTML
{{define "title"}}审计{{end}}
|
|
{{define "content"}}
|
|
<h1>审计日志</h1>
|
|
<form method="get" action="/audit" class="filters">
|
|
<label>操作者<input type="text" name="actor" value="{{.Data.Filter.Actor}}"></label>
|
|
<label>动作<input type="text" name="action" value="{{.Data.Filter.Action}}"></label>
|
|
<label>目标<input type="text" name="target" value="{{.Data.Filter.Target}}"></label>
|
|
<label>起<input type="text" name="from" value="{{.Data.FromStr}}" placeholder="2006-01-02"></label>
|
|
<label>止<input type="text" name="to" value="{{.Data.ToStr}}" placeholder="2006-01-02"></label>
|
|
<button type="submit">筛选</button>
|
|
</form>
|
|
|
|
<table>
|
|
<thead><tr><th>ID</th><th>时间</th><th>操作者</th><th>动作</th><th>目标</th><th>meta</th></tr></thead>
|
|
<tbody>
|
|
{{range .Data.Entries}}
|
|
<tr><td>{{.ID}}</td><td>{{fmtTime .At}}</td><td>{{.Actor}}</td><td>{{.Action}}</td><td>{{.Target}}</td><td class="meta">{{.Meta}}</td></tr>
|
|
{{else}}
|
|
<tr><td colspan="6" class="muted">无匹配记录</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
<div class="pager">
|
|
{{if .Data.HasPrev}}<a href="{{.Data.PrevURL}}">上一页</a>{{end}}
|
|
<span class="muted">共 {{.Data.Total}} 条</span>
|
|
{{if .Data.HasNext}}<a href="{{.Data.NextURL}}">下一页</a>{{end}}
|
|
</div>
|
|
|
|
<h2>节点事件</h2>
|
|
<form method="get" action="/audit" class="filters">
|
|
<input type="hidden" name="actor" value="{{.Data.Filter.Actor}}">
|
|
<input type="hidden" name="action" value="{{.Data.Filter.Action}}">
|
|
<input type="hidden" name="target" value="{{.Data.Filter.Target}}">
|
|
<label>节点 ID<input type="text" name="node_id" value="{{.Data.NodeIDStr}}"></label>
|
|
<button type="submit">查看节点事件</button>
|
|
</form>
|
|
<table>
|
|
<thead><tr><th>ID</th><th>时间</th><th>节点</th><th>事件</th><th>detail</th></tr></thead>
|
|
<tbody>
|
|
{{range .Data.NodeEvents}}
|
|
<tr><td>{{.ID}}</td><td>{{fmtTime .At}}</td><td>{{.NodeID}}</td><td>{{.Event}}</td><td class="meta">{{.Detail}}</td></tr>
|
|
{{else}}
|
|
<tr><td colspan="5" class="muted">输入节点 ID 查看其事件</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{end}}
|