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>
53 lines
2.7 KiB
HTML
53 lines
2.7 KiB
HTML
{{define "title"}}节点{{end}}
|
||
{{define "content"}}
|
||
<h1>节点</h1>
|
||
{{if not .Data.ProvisionReady}}<div class="warn">节点替换服务(#14)尚未接入,相关按钮已置灰。</div>{{end}}
|
||
{{if not .Data.LifecycleReady}}<div class="warn">节点生命周期服务(#5)尚未接入,上/下线按钮已置灰。</div>{{end}}
|
||
|
||
<table>
|
||
<thead><tr><th>ID</th><th>名称</th><th>地区</th><th>tier</th><th>权重</th><th>状态</th><th>厂商</th><th>近期事件</th><th>操作</th></tr></thead>
|
||
<tbody>
|
||
{{range .Data.Nodes}}
|
||
<tr>
|
||
<td>{{.ID}}</td>
|
||
<td>{{.NameZH}}<br><span class="muted">{{.UUID}}</span></td>
|
||
<td>{{.Region}}</td>
|
||
<td>{{.Tier}}</td>
|
||
<td>{{.Weight}}</td>
|
||
<td><span class="status status-{{.Status}}">{{.Status}}</span></td>
|
||
<td>{{.Provider}}</td>
|
||
<td>
|
||
{{range .RecentEvents}}<div class="ev">{{.Event}} <span class="muted">{{fmtTime .At}}</span></div>{{else}}<span class="muted">—</span>{{end}}
|
||
</td>
|
||
<td class="ops">
|
||
<form method="post" action="/nodes/op" class="inline confirm" data-confirm="确认替换节点 {{.ID}}({{.NameZH}})?将开通新节点并下线旧节点。">
|
||
<input type="hidden" name="csrf_token" value="{{$.CSRF}}">
|
||
<input type="hidden" name="node_id" value="{{.ID}}">
|
||
<input type="hidden" name="op" value="replace">
|
||
<input type="hidden" name="confirm" value="yes">
|
||
<button type="submit" {{if not $.Data.ProvisionReady}}disabled{{end}}>替换</button>
|
||
</form>
|
||
<form method="post" action="/nodes/op" class="inline confirm" data-confirm="确认将节点 {{.ID}} 置为 draining(停止接入新连接)?">
|
||
<input type="hidden" name="csrf_token" value="{{$.CSRF}}">
|
||
<input type="hidden" name="node_id" value="{{.ID}}">
|
||
<input type="hidden" name="op" value="draining">
|
||
<input type="hidden" name="confirm" value="yes">
|
||
<button type="submit" {{if not $.Data.LifecycleReady}}disabled{{end}}>draining</button>
|
||
</form>
|
||
<form method="post" action="/nodes/op" class="inline confirm" data-confirm="确认将节点 {{.ID}} 置为 up(恢复接入)?">
|
||
<input type="hidden" name="csrf_token" value="{{$.CSRF}}">
|
||
<input type="hidden" name="node_id" value="{{.ID}}">
|
||
<input type="hidden" name="op" value="up">
|
||
<input type="hidden" name="confirm" value="yes">
|
||
<button type="submit" {{if not $.Data.LifecycleReady}}disabled{{end}}>up</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{{else}}
|
||
<tr><td colspan="9" class="muted">暂无节点</td></tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
<script src="/static/confirm.js"></script>
|
||
{{end}}
|