feat: v0.12.0 —— 表格每行绝对等高 + 管理 WebUI(2FA 绑定才能进入)
① 每行绝对等高 - 行高与内容长度彻底解耦:新增 --row-h: 72px 作为唯一基准 - 名称列改为「文本区限两行 + 徽标区固定单行」两段式;文本区用 .clamp2 - 关键点:必须用 height 而非 max-height —— 后者会让短内容缩成一行、 长内容撑到两行,反而产生 66px/71px 两种行高(实测踩过) - 分组标题行用 :not(.group-row) 排除在数据行规则之外 - 事件表与监控表格共用同一个 --row-h,两张表视觉上也严格对齐 - 完整值仍可通过悬停 title 查看(已加断言强制所有截断单元格带 title) - 卡片视图同样等高(13 视口 × 250px 全一致) ② 管理 WebUI + 2FA - 新增 /admin 与 /api/admin/*;未绑定 2FA 时除绑定流程外一律 403 - 零依赖实现 TOTP(RFC 6238),用 RFC 4226/6238 官方测试向量验证(16 个向量) - 内置 QR 码生成器(byte 模式 / ECC L / 版本 1-10,GF(256) RS 纠错 + 掩码评分), 用独立解码器实测:版本 1 与版本 6 均能读出逐字节一致的内容 - 绑定流程:口令 → 生成密钥(扫码/手输/otpauth 链接)→ 验证 6 位码 → 10 枚恢复码; 完成后凭本次验证码直接换取会话 - 安全:会话 id 32 字节随机数且服务端只存哈希;HttpOnly+SameSite=Strict; 30 分钟空闲/2 小时最长;同时间步的码防重放;恢复码一次性; 登录失败按 IP 指数退避锁定;不解析 X-Forwarded-For - 管理功能:运行状态、探测范围、展示口径、维护窗口增删、立即重探、通知自检、 目标只读列表、最近事件 - 所有写操作只落 local.monitors.json(私有不入库),绝不改 public/monitors.json 过程中修掉三个真 bug: 1. 会话 cookie 的 Path=/admin 覆盖不到 /api/admin/*,登录 200 但后续全部 401 2. 恢复码登录被 verifyTotp 的 invalid-format 提前挡掉(恢复码非 6 位数字) 3. 超大请求体用 req.destroy() 硬断连接,客户端只看到 fetch failed 拿不到状态码 测试:新增 scripts/verify-admin.cjs(102 项),全项目共 7 套 274 项全部通过
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>管理后台 · Shiroko Monitor</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<link rel="stylesheet" href="/assets/theme-shiroko.css">
|
||||
<link rel="stylesheet" href="/assets/admin.css">
|
||||
</head>
|
||||
<body class="admin-body">
|
||||
<header class="admin-topbar">
|
||||
<div class="brand">
|
||||
<span class="logo" aria-hidden="true"></span>
|
||||
<div class="brand-text">
|
||||
<h1>管理后台</h1>
|
||||
<p>需绑定并通过 2FA 验证后才能进入</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-topbar-right">
|
||||
<span class="admin-ver" id="admin-ver">–</span>
|
||||
<a class="admin-link" href="/">← 返回面板</a>
|
||||
<button type="button" class="admin-btn admin-btn-ghost" id="logout-btn" hidden>退出登录</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="admin-main">
|
||||
<!-- 状态探测中 -->
|
||||
<section class="admin-card" id="loading"><p class="admin-muted">正在检查 2FA 状态…</p></section>
|
||||
|
||||
<!-- ① 未绑定:先绑定 2FA -->
|
||||
<section class="admin-card" id="bind-panel" hidden>
|
||||
<h2>第一步:绑定 2FA</h2>
|
||||
<p class="admin-muted">管理后台必须绑定两步验证后才能进入。请先在启动探针的环境里设置 <code>SHIROKO_ADMIN_SETUP_KEY</code>,然后在这里输入它以生成密钥。</p>
|
||||
|
||||
<div class="admin-step" id="step-key">
|
||||
<label for="setup-key">绑定口令(SHIROKO_ADMIN_SETUP_KEY)</label>
|
||||
<div class="admin-row">
|
||||
<input type="password" id="setup-key" autocomplete="off" placeholder="环境变量里设置的那个值" inputmode="text">
|
||||
<button type="button" class="admin-btn" id="setup-start">生成密钥</button>
|
||||
</div>
|
||||
<p class="admin-msg" id="setup-key-msg"></p>
|
||||
</div>
|
||||
|
||||
<div class="admin-step" id="step-secret" hidden>
|
||||
<h3>第二步:把密钥加入你的验证器 App</h3>
|
||||
<ol class="admin-ol">
|
||||
<li>用 Google Authenticator / Microsoft Authenticator / 1Password / Aegis 扫描二维码,或</li>
|
||||
<li>手动输入下面的密钥(Base32):</li>
|
||||
</ol>
|
||||
<div class="qr-box"><canvas id="qr" width="220" height="220" aria-label="2FA 绑定二维码"></canvas></div>
|
||||
<div class="secret-box">
|
||||
<code id="secret-text">–</code>
|
||||
<button type="button" class="admin-btn admin-btn-sm" id="copy-secret">复制密钥</button>
|
||||
</div>
|
||||
<details class="admin-details">
|
||||
<summary>扫码不方便?复制 otpauth 链接</summary>
|
||||
<div class="secret-box"><code id="uri-text">–</code><button type="button" class="admin-btn admin-btn-sm" id="copy-uri">复制链接</button></div>
|
||||
</details>
|
||||
<h3>第三步:输入 App 显示的 6 位验证码完成绑定</h3>
|
||||
<div class="admin-row">
|
||||
<input type="text" id="confirm-code" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="6 位数字">
|
||||
<button type="button" class="admin-btn" id="confirm-btn">完成绑定</button>
|
||||
</div>
|
||||
<p class="admin-msg" id="confirm-msg"></p>
|
||||
</div>
|
||||
|
||||
<div class="admin-step" id="step-recovery" hidden>
|
||||
<h3>第四步:保存恢复码(只显示这一次)</h3>
|
||||
<p class="admin-muted">每个恢复码只能用一次,用于手机丢失、验证器清库时登录。请离线保存,不要只存在这台机器上。</p>
|
||||
<div class="recovery-box" id="recovery-list"></div>
|
||||
<div class="admin-row">
|
||||
<button type="button" class="admin-btn admin-btn-sm" id="copy-recovery">复制全部恢复码</button>
|
||||
<button type="button" class="admin-btn admin-btn-primary" id="recovery-done">我已保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ② 已绑定未登录:输 2FA 码登录 -->
|
||||
<section class="admin-card admin-card-narrow" id="login-panel" hidden>
|
||||
<h2>两步验证</h2>
|
||||
<p class="admin-muted">输入验证器 App 的 6 位动态码,或使用一枚未用过的恢复码。</p>
|
||||
<div class="admin-row">
|
||||
<input type="text" id="login-code" inputmode="numeric" autocomplete="one-time-code" placeholder="验证码 / 恢复码" autofocus>
|
||||
<button type="button" class="admin-btn admin-btn-primary" id="login-btn">进入</button>
|
||||
</div>
|
||||
<p class="admin-msg" id="login-msg"></p>
|
||||
<details class="admin-details">
|
||||
<summary>验证码设备丢了?</summary>
|
||||
<p class="admin-muted">用一枚未使用的恢复码登录;或用 <code>SHIROKO_ADMIN_SETUP_KEY</code> 重置 2FA(重置后需重新绑定)。</p>
|
||||
<div class="admin-row">
|
||||
<input type="password" id="reset-key" placeholder="SHIROKO_ADMIN_SETUP_KEY" autocomplete="off">
|
||||
<button type="button" class="admin-btn admin-btn-danger" id="reset-btn">重置 2FA</button>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<!-- ③ 已登录:管理面板 -->
|
||||
<section id="console-panel" hidden>
|
||||
<div class="admin-card" id="stat-card">
|
||||
<h2>运行状态</h2>
|
||||
<div class="admin-stats" id="admin-stats"></div>
|
||||
<p class="admin-muted" id="updated-at">–</p>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<h2>口径与范围</h2>
|
||||
<div class="admin-field">
|
||||
<label for="probe-scope">探测范围</label>
|
||||
<select id="probe-scope">
|
||||
<option value="all">全部(自建 + UptimeRobot)</option>
|
||||
<option value="config">仅自建配置</option>
|
||||
<option value="uptimerobot">仅 UptimeRobot</option>
|
||||
</select>
|
||||
<p class="admin-hint">决定探针真正发请求的范围;改动立即生效并热重载配置。</p>
|
||||
</div>
|
||||
<div class="admin-field">
|
||||
<label for="data-filter">展示口径</label>
|
||||
<select id="data-filter">
|
||||
<option value="both">全部</option>
|
||||
<option value="config">仅自建配置</option>
|
||||
<option value="uptimerobot">仅 UptimeRobot</option>
|
||||
</select>
|
||||
<p class="admin-hint">只影响面板展示哪些目标;与探测范围相互独立。</p>
|
||||
</div>
|
||||
<div class="admin-row">
|
||||
<button type="button" class="admin-btn" id="probe-now">立即重新探测</button>
|
||||
<button type="button" class="admin-btn" id="notify-test">发送通知自检</button>
|
||||
</div>
|
||||
<p class="admin-msg" id="action-msg"></p>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<h2>维护窗口</h2>
|
||||
<p class="admin-muted">窗口内的目标不计入可用率、也不发故障告警。</p>
|
||||
<div class="admin-field-row">
|
||||
<div class="admin-field">
|
||||
<label for="mw-from">开始</label>
|
||||
<input type="datetime-local" id="mw-from">
|
||||
</div>
|
||||
<div class="admin-field">
|
||||
<label for="mw-to">结束</label>
|
||||
<input type="datetime-local" id="mw-to">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-field-row">
|
||||
<div class="admin-field">
|
||||
<label for="mw-id">仅限目标(留空 = 全部)</label>
|
||||
<select id="mw-id"><option value="">全部目标</option></select>
|
||||
</div>
|
||||
<div class="admin-field">
|
||||
<label for="mw-note">备注</label>
|
||||
<input type="text" id="mw-note" maxlength="200" placeholder="例如:升级数据库">
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-row">
|
||||
<button type="button" class="admin-btn admin-btn-primary" id="mw-add">新增窗口</button>
|
||||
</div>
|
||||
<div class="mw-list" id="mw-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<h2>监控目标</h2>
|
||||
<p class="admin-muted">当前配置中的目标(探测范围为 uptimerobot 时,本地目标不会被真正探测)。</p>
|
||||
<div class="table-scroll">
|
||||
<table class="admin-table">
|
||||
<thead><tr><th>名称</th><th>分组</th><th>类型</th><th>目标</th><th>间隔</th></tr></thead>
|
||||
<tbody id="monitor-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<h2>最近事件</h2>
|
||||
<div class="table-scroll">
|
||||
<table class="admin-table ev-table">
|
||||
<thead><tr><th>时间</th><th>类型</th><th>目标</th><th>详情</th></tr></thead>
|
||||
<tbody id="event-list"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-card">
|
||||
<h2>安全</h2>
|
||||
<p class="admin-muted" id="twofa-summary">–</p>
|
||||
<p class="admin-muted">会话最长 2 小时,30 分钟无操作自动失效;进程重启会清空所有会话。</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script type="module" src="/assets/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user