feat(panel, PANEL_ENHANCE_PLAN 批1-A): 访问级 4 位 PIN 鉴权 + 后端只读端点 power/overview/ams/svcd + audio 电平键
鉴权(用户定案:访问面板即需登录): - PANEL.pin 存 sha256+salt(salt:hex,绝不含明文);与未来锁屏 PIN 同源预留 system.pin(g_sets 注记)。 - 未设 PIN → /api/auth/setup 首次设置流程;未设 PIN 前读/写端点全拦(require_auth 无条件,登录/设置流程除外)。 - token 128bit(16B→32hex) 只存进程内存(重启自然失效),3 天固定不滑动续期,多会话并存。 - 连错 5 次 PIN 锁 5 分钟(429 LOCKED,status 透出 lock_remaining_ms)。 - 旧密码机制 panel.auth_enabled/panel.auth_pass 兼容迁移:未设 PIN 时可暂用旧密码登录(status=legacy),设 PIN 后以 PIN 为准。 新只读端点(均需 PIN 会话): - /api/power: meow_powermgr MSG_PM_QUERY(0xD0)→RESULT(0xD1) battery/estimate/breakdown,confidence 原样透传。 - /api/audio: 补 rms/peak/lvlseq(追加键,ABI 兼容),抽 audio_fill() 供 overview 复用。 - /api/ams: am list 解析 App 表(pkg/name/state/pid/launches/crashes/last_kind/last_code)。 - /api/svcd: meow_svcd status all 解析服务表(name/state/pid/restart/backoff_ms/trigger/src/exit/exec)。 - /api/overview: 单线程后端只暴露聚合——perf/power/audio/net/ams/svcd 六源顺序 fan-out,任一源失败该子块 available:false,绝不假数据。 前端最小支撑:登录页(PIN 输入+首次设置双态)+ 401 自动跳登录 + token 存 localStorage 随请求带 X-MeowToken(后端亦认 Authorization: Bearer)。 红线:仅新增读级端点,未加任何写端点(音量/路由/策略等);本任务不含批1-B 页面。 md5: meowpanel 3696e85b5eaf111f530c5c39fa68886e(mipsel 静态)/ meowpanel_host 3180e4f87e77db8717597619097805eb。 验证:host+mips -Werror 0 warning;假 IPC/am/svcd 桩六源聚合(含 power 宕机降级)PASS;PIN 全流程(设置→登录→连错5次锁→token 过期)PASS;qemu-mipsel 冒烟 PASS。
This commit is contained in:
1 parent
30e2ec901f
commit
d12164305d
6 files changed
+627
-155
No files matched your search
@@ -116,12 +116,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录页(面板鉴权启用后显示) -->
|
||||
<!-- 登录 / 首次设置 PIN(访问级 PIN 鉴权;访问面板即需登录) -->
|
||||
<div id="login" class="login-scrim" hidden>
|
||||
<div class="login-box">
|
||||
<div class="login-brand"><span class="login-brand-mark">M</span><span>MeowPanel</span></div>
|
||||
<p class="login-sub">此面板已开启密码鉴权,请输入密码后使用。</p>
|
||||
<input type="password" id="login-password" class="login-input" placeholder="面板密码" autocomplete="current-password">
|
||||
<p class="login-sub" id="login-sub">此面板已开启访问鉴权,请输入 4 位 PIN。</p>
|
||||
<input type="password" id="login-pin" class="login-input" placeholder="4 位 PIN" inputmode="numeric" autocomplete="off" maxlength="4">
|
||||
<input type="password" id="login-pin2" class="login-input" placeholder="再次输入 PIN" inputmode="numeric" autocomplete="off" maxlength="4" hidden>
|
||||
<p class="login-err" id="login-err" hidden></p>
|
||||
<button class="btn btn-primary login-btn" id="login-btn">登 录</button>
|
||||
</div>
|
||||
|
||||
@@ -129,39 +129,80 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 鉴权(可选密码登录) ---------------- */
|
||||
function authRequired() { // 任一路由收到 401 → 弹登录页
|
||||
/* ---------------- 访问级 PIN 鉴权(设置 PIN → 登录;401 自动跳登录;token 存 localStorage) ----------------
|
||||
* 后端:访问面板即需登录;token 存面板进程内存(重启失效,3 天固定不滑动续期);连续 5 次错 PIN 锁 5 分钟。
|
||||
* 前端:token 存 localStorage 随请求带 X-MeowToken;401 自动切登录页;未设 PIN 走首次设置流程。 */
|
||||
let authMode = 'login'; // 'login' | 'setup'
|
||||
function loginErr(msg) { const err = $('#login-err'); if (err) { err.textContent = msg; err.hidden = false; } }
|
||||
function authRequired() { // 任一路由收到 401 → 弹登录页(会话失效/未认证)
|
||||
state.unauth = true;
|
||||
showLogin();
|
||||
refreshAuthGate();
|
||||
}
|
||||
function showLogin() {
|
||||
async function refreshAuthGate() { // 查状态决定「首次设置」还是「登录」
|
||||
let st = null;
|
||||
try { st = await api('GET', '/api/auth/status'); } catch (e) { st = null; }
|
||||
if (st && st.session) { state.unauth = false; hideLogin(); return; }
|
||||
if (st && !st.pin_set && !st.legacy) showLogin('setup');
|
||||
else showLogin('login');
|
||||
}
|
||||
function showLogin(mode) {
|
||||
state.unauth = true;
|
||||
authMode = mode || 'login';
|
||||
const lg = $('#login'); if (lg) lg.hidden = false;
|
||||
const ip = $('#login-password'); if (ip) ip.focus();
|
||||
const sub = $('#login-sub');
|
||||
const pin2 = $('#login-pin2');
|
||||
const btn = $('#login-btn');
|
||||
if (authMode === 'setup') {
|
||||
if (sub) sub.textContent = '首次使用:请设置一个 4 位数字 PIN 作为本面板访问口令。';
|
||||
if (pin2) pin2.hidden = false;
|
||||
if (btn) btn.textContent = '设 置';
|
||||
} else {
|
||||
if (sub) sub.textContent = '此面板已开启访问鉴权,请输入 4 位 PIN。';
|
||||
if (pin2) pin2.hidden = true;
|
||||
if (btn) btn.textContent = '登 录';
|
||||
}
|
||||
const ip = $('#login-pin'); if (ip) { ip.value = ''; ip.focus(); }
|
||||
const ip2 = $('#login-pin2'); if (ip2 && authMode !== 'setup') ip2.value = '';
|
||||
const err = $('#login-err'); if (err) err.hidden = true;
|
||||
}
|
||||
function hideLogin() { const lg = $('#login'); if (lg) lg.hidden = true; }
|
||||
async function doLogin() {
|
||||
const pw = $('#login-password').value;
|
||||
const err = $('#login-err');
|
||||
if (err) err.hidden = true;
|
||||
if (!pw) { if (err) { err.textContent = '请输入密码'; err.hidden = false; } return; }
|
||||
const pin = ($('#login-pin').value || '').trim();
|
||||
const err = $('#login-err'); if (err) err.hidden = true;
|
||||
if (!pin) { loginErr('请输入 PIN'); return; }
|
||||
if (authMode === 'setup') {
|
||||
const pin2 = ($('#login-pin2').value || '').trim();
|
||||
if (pin.length !== 4 || /[^0-9]/.test(pin)) { loginErr('PIN 必须为 4 位数字'); return; }
|
||||
if (pin !== pin2) { loginErr('两次输入的 PIN 不一致'); return; }
|
||||
const btn = $('#login-btn'); if (btn) btn.disabled = true;
|
||||
try {
|
||||
await api('POST', '/api/auth/setup', { pin: pin });
|
||||
toast('PIN 已设置,请登录');
|
||||
showLogin('login');
|
||||
} catch (e2) { loginErr((e2 && e2.message) ? e2.message : '设置失败'); }
|
||||
finally { if (btn) btn.disabled = false; }
|
||||
return;
|
||||
}
|
||||
const btn = $('#login-btn'); if (btn) btn.disabled = true;
|
||||
try {
|
||||
const d = await api('POST', '/api/auth/login', { password: pw });
|
||||
const d = await api('POST', '/api/auth/login', { pin: pin });
|
||||
storageSet('meowpanel.token', d.token);
|
||||
state.unauth = false;
|
||||
hideLogin();
|
||||
toast('已登录');
|
||||
if (started) route(); else startApp();
|
||||
} catch (e) {
|
||||
if (err) { err.textContent = (e && e.message) ? e.message : '登录失败'; err.hidden = false; }
|
||||
if (e && (e.code === 'LOCKED' || e.status === 429)) loginErr(e.message || '错误次数过多,已锁定,请稍后重试');
|
||||
else loginErr((e && e.message) ? e.message : '登录失败');
|
||||
} finally {
|
||||
const b = $('#login-btn'); if (b) b.disabled = false;
|
||||
}
|
||||
}
|
||||
function bindLogin() {
|
||||
const ok = $('#login-btn'); if (ok) ok.addEventListener('click', doLogin);
|
||||
const ip = $('#login-password'); if (ip) ip.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
|
||||
const ip = $('#login-pin'); if (ip) ip.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
|
||||
// PIN 输入:只允许数字,最长 4 位
|
||||
[ip, $('#login-pin2')].forEach(el => { if (el) el.addEventListener('input', () => { el.value = el.value.replace(/[^0-9]/g, '').slice(0, 4); }); });
|
||||
}
|
||||
function passwordDlg(title, body) {
|
||||
return new Promise(res => {
|
||||
@@ -1488,7 +1529,7 @@
|
||||
$('#menu-btn').addEventListener('click', () => $('#sidebar').classList.toggle('open'));
|
||||
}
|
||||
function route() {
|
||||
if (state.unauth) { showLogin(); return; } // 未认证 → 登录页
|
||||
if (state.unauth) { showLogin(authMode); return; } // 未认证 → 登录页(保持当前设置/登录态)
|
||||
const id = (location.hash || '#/perf').replace('#/', '').split('?')[0] || 'perf';
|
||||
const r = ROUTES[id] || ROUTES.perf;
|
||||
state.route = id;
|
||||
@@ -1517,11 +1558,13 @@
|
||||
bindFiles(); bindApps(); bindFonts(); bindSettings(); bindRemote(); bindScreen(); bindSystem(); bindLogs(); bindNotif(); bindNet();
|
||||
bindLogin();
|
||||
bindPwDlg();
|
||||
// 首次进入探鉴权状态:启用且无有效会话 → 登录页;否则直接进应用
|
||||
// 首次进入:访问级 PIN 鉴权——已登录直接进;未设 PIN(且无旧密码)→ 首次设置流程;已设 → 登录
|
||||
let st = null;
|
||||
try { st = await api('GET', '/api/auth/status'); } catch (e) { st = null; }
|
||||
if (st && st.auth_enabled && !st.session) { showLogin(); return; }
|
||||
startApp();
|
||||
if (st && st.session) { startApp(); return; }
|
||||
if (st && !st.pin_set && !st.legacy) { showLogin('setup'); return; }
|
||||
if (st) { showLogin('login'); return; }
|
||||
startApp(); // 后端不可达:仍启动(显示离线横幅)
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
})();
|
||||
Reference in new issue
Block a user