diff --git a/lib/index.js b/lib/index.js index bcfe433..43a2145 100644 --- a/lib/index.js +++ b/lib/index.js @@ -115,6 +115,15 @@ export function tokenForHost(host) { if (isLanOverrideHost(host)) return getLanToken(); return classifyHost(host) === 'public' ? getAccessToken() : getLanToken(); } + +/** + * 登录页凭据语义:长期有效的 PIN 允许密码管理器保存;Quick Tunnel 自动生成的 + * 公网 PIN 每次隧道开启都会轮换,不应作为长期密码保存。 + */ +export function credentialKindForHost(host) { + if (isLanOverrideHost(host) || classifyHost(host) !== 'public') return 'persistent'; + return pinCustom('public') || tunnelMode() === 'named' ? 'persistent' : 'ephemeral'; +} /** 去掉 Host 的端口(与 classifyHost 一致),用于和手动设置的局域网地址比较。 */ function hostNameOnly(host) { let name = String(host ?? '').trim().toLowerCase(); @@ -313,6 +322,7 @@ export function apply(ctx, config = {}, internals = {}) { // dsh web 重启/更新后 sessionKey 变化 → 手机需重新输入 sessionKey: randomBytes(16).toString('hex'), getToken: (host) => tokenForHost(host), + getCredentialKind: (host) => credentialKindForHost(host), isProtected: (host) => { if (isLanOverrideHost(host)) return lanAuthEnabled(); return classifyHost(host) === 'public' ? true : lanAuthEnabled(); diff --git a/lib/proxy.mjs b/lib/proxy.mjs index 8e29df1..39bc1a4 100644 --- a/lib/proxy.mjs +++ b/lib/proxy.mjs @@ -195,10 +195,18 @@ function parseCookies(header) { return out; } -/** 登录页:按访问来源显示提示(局域网 / 公网);error: false|true|'locked'(locked 带剩余秒数)。 */ -function loginPageHtml(error, isPublic, retryAfter = 0) { +/** + * 登录页:按访问来源显示提示(局域网 / 公网)。 + * credentialKind: 'persistent'(可长期复用)|'ephemeral'(短期轮换,不建议保存)。 + * error: false|true|'locked'(locked 带剩余秒数)。 + */ +function loginPageHtml(error, isPublic, retryAfter = 0, credentialKind = 'ephemeral') { const where = isPublic ? '此公网地址' : '此局域网地址'; const whereEn = isPublic ? 'This public address' : 'This LAN address'; + const autocomplete = credentialKind === 'persistent' ? 'current-password' : 'off'; + const usernameField = credentialKind === 'persistent' + ? '' + : ''; const errMsg = error === 'locked' ? `尝试次数过多,请 ${retryAfter} 秒后再试 | Too many attempts — try again in ${retryAfter}s` : error ? '密码错误,请重试 | Wrong PIN, try again' : ''; @@ -210,18 +218,54 @@ body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:c .card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:28px 24px;max-width:320px;width:calc(100% - 48px);text-align:center} h1{font-size:16px;margin:0 0 4px;color:#111827} p{font-size:13px;color:#6b7280;margin:0 0 16px} -input{width:100%;box-sizing:border-box;padding:10px 12px;font-size:18px;letter-spacing:6px;text-align:center;border:1px solid #d1d5db;border-radius:8px;outline:none;margin-bottom:12px} -input:focus{border-color:#4f6ef7} -button{width:100%;padding:10px;font-size:15px;background:#4f6ef7;color:#fff;border:none;border-radius:8px;cursor:pointer} +label.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} +.implicit-username{display:none} +.pin-field{position:relative;margin-bottom:12px} +input{width:100%;box-sizing:border-box;padding:10px 50px;font-size:18px;line-height:24px;letter-spacing:6px;text-align:center;caret-color:#4f6ef7;border:1px solid #d1d5db;border-radius:8px;outline:none} +input::-ms-reveal,input::-ms-clear{display:none} +input:focus{border-color:#4f6ef7;box-shadow:0 0 0 1px #4f6ef7} +.pin-toggle{position:absolute;right:2px;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;width:44px;height:44px;padding:0;background:transparent;color:#6b7280;border:0;border-radius:6px;cursor:pointer} +.pin-toggle:hover{color:#374151;background:#f3f4f6} +.pin-toggle:focus-visible{outline:2px solid #4f6ef7;outline-offset:1px} +.pin-toggle svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;pointer-events:none} +.pin-toggle .is-hidden{display:none} +.enter-button{width:100%;padding:10px;font-size:15px;background:#4f6ef7;color:#fff;border:none;border-radius:8px;cursor:pointer} .err{color:#dc2626;font-size:12px;margin-bottom:10px;min-height:16px}
${where}受访问密码保护,请输入访问密码 | ${whereEn} is password-protected — enter the access PIN