diff --git a/README.md b/README.md index aea4cdb..e61ed46 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ - `index.html`:工具入口 - `channel-recommendation.html`:包名、品类、国家 → 渠道优先级建议 - `link-generator.html`:登录后粘贴原始 Mintegral Click Link 并转换为已确认的平台宏 -- `login.html`:本机密码与 Google Authenticator 动态码验证 +- `login.html`:输入 Shareit ID,本机设置密码并扫码绑定 Google Authenticator - `link-history.html`:按登录用户查看独立的链接记录 -登录、Authenticator 绑定和链接记录均保存在当前浏览器,不会跨设备同步;这是内部辅助识别方案,不可替代服务端身份认证或安全审计。 +登录页不公开展示用户白名单或角色。登录、Authenticator 绑定和链接记录均保存在当前浏览器,不会跨设备同步;这是内部辅助识别方案,不可替代服务端身份认证或安全审计。二维码由固定版本的前端组件在本地生成,第三方说明见 `THIRD_PARTY_NOTICES.md`。 ## 可维护数据 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..84fe924 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,11 @@ +# Third-party notices + +## qrcode-generator 1.4.4 + +The login page uses the pinned browser build of `qrcode-generator` 1.4.4 to render the Google Authenticator enrollment QR code locally. The script is loaded from CDNJS with Subresource Integrity verification and does not receive the TOTP setup URI as a network parameter. + +- Package: [qrcode-generator on npm](https://www.npmjs.com/package/qrcode-generator) +- Pinned distribution: [CDNJS qrcode-generator 1.4.4](https://cdnjs.com/libraries/qrcode-generator/1.4.4) +- License: MIT + +Content was rephrased for compliance with licensing restrictions. diff --git a/assets/link-generator.js b/assets/link-generator.js index 286ffd8..b8d5b2e 100644 --- a/assets/link-generator.js +++ b/assets/link-generator.js @@ -14,7 +14,7 @@ let activeResult = null; let inputTimer; - $('currentUser').innerHTML = `${escapeHtml(current.username)}${current.role === 'admin' ? '管理员' : '普通用户'}`; + $('currentUser').textContent = current.username; $('templateCount').textContent = `${templates.length} 个平台模板`; templates.forEach((template, index) => platformSelect.add(new Option(template.name, index))); diff --git a/assets/link-history.js b/assets/link-history.js index 30be85e..ca6c699 100644 --- a/assets/link-history.js +++ b/assets/link-history.js @@ -68,8 +68,8 @@ clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 1800); } - $('currentUser').innerHTML = `${escapeHtml(current.username)}${current.role === 'admin' ? '管理员' : '普通用户'}`; - $('historyScope').textContent = current.role === 'admin' ? '管理员可查看当前浏览器内全部用户的记录。' : '普通用户仅可查看当前浏览器内自己的记录。'; + $('currentUser').textContent = current.username; + $('historyScope').textContent = '查看当前浏览器内你有权限访问的链接记录。'; $('logoutBtn').addEventListener('click', () => { auth.logout(); location.href = 'login.html'; }); $('historySearch').addEventListener('input', render); $('clearHistory').addEventListener('click', () => { diff --git a/assets/login.js b/assets/login.js index 544838d..15a1a4a 100644 --- a/assets/login.js +++ b/assets/login.js @@ -2,9 +2,7 @@ const auth = window.TrafficAuth; const $ = (id) => document.getElementById(id); const enrollmentByUser = new Map(); - const userSelect = $('loginUser'); - - auth.users.forEach((user) => userSelect.add(new Option(`${user.username} · ${user.role === 'admin' ? '管理员' : '普通用户'}`, user.username))); + let selectedUsername = ''; function nextPage() { const next = new URLSearchParams(location.search).get('next'); @@ -18,36 +16,86 @@ } function currentEnrollment() { - const username = userSelect.value; - if (!enrollmentByUser.has(username)) enrollmentByUser.set(username, auth.createEnrollment(username)); - return enrollmentByUser.get(username); + if (!enrollmentByUser.has(selectedUsername)) enrollmentByUser.set(selectedUsername, auth.createEnrollment(selectedUsername)); + return enrollmentByUser.get(selectedUsername); } - function renderMode() { - const username = userSelect.value; - const user = auth.findUser(username); - const enrolled = auth.isEnrolled(username); - $('selectedRole').innerHTML = `${user.role === 'admin' ? '管理员' : '普通用户'}${enrolled ? '已在当前浏览器绑定' : '首次使用,需要完成本机设置'}`; - $('signinForm').classList.toggle('hidden', !enrolled); - $('enrollForm').classList.toggle('hidden', enrolled); - $('loginTitle').textContent = enrolled ? '用户登录' : '首次绑定'; - $('loginSubtitle').textContent = enrolled ? '密码和动态验证码双重验证' : '设置密码并绑定 Google Authenticator'; - $('signinMessage').textContent = ''; - $('enrollMessage').textContent = ''; - if (!enrolled) { - const enrollment = currentEnrollment(); - $('setupKey').textContent = enrollment.secret.match(/.{1,4}/g).join(' '); - $('authAccountName').textContent = `Traffic Intelligence (${username})`; + function renderQr(otpauthUrl) { + const container = $('qrCode'); + container.innerHTML = ''; + if (typeof window.qrcode !== 'function') { + container.innerHTML = '二维码加载失败,请刷新页面重试'; + return false; + } + try { + const qr = window.qrcode(0, 'M'); + qr.addData(otpauthUrl); + qr.make(); + container.innerHTML = qr.createSvgTag(5, 4); + return true; + } catch (_) { + container.innerHTML = '二维码生成失败,请刷新页面重试'; + return false; } } + function resetForms() { + ['signinPassword','signinCode','newPassword','confirmPassword','setupCode'].forEach((id) => { $(id).value = ''; }); + $('accountMessage').textContent = ''; + $('signinMessage').textContent = ''; + $('enrollMessage').textContent = ''; + } + + function openAccount(username) { + selectedUsername = username; + const enrolled = auth.isEnrolled(username); + $('accountForm').classList.add('hidden'); + $('confirmedAccount').classList.remove('hidden'); + $('confirmedUser').textContent = username; + $('signinForm').classList.toggle('hidden', !enrolled); + $('enrollForm').classList.toggle('hidden', enrolled); + $('loginTitle').textContent = enrolled ? '欢迎回来' : '首次注册'; + $('loginSubtitle').textContent = enrolled ? '输入密码和动态验证码登录' : '扫描二维码并设置本机密码'; + if (!enrolled) { + const ready = renderQr(currentEnrollment().otpauthUrl); + $('enrollMessage').textContent = ready ? '' : '二维码组件加载失败,暂时无法完成注册'; + } + } + + function changeAccount() { + selectedUsername = ''; + resetForms(); + $('confirmedAccount').classList.add('hidden'); + $('signinForm').classList.add('hidden'); + $('enrollForm').classList.add('hidden'); + $('accountForm').classList.remove('hidden'); + $('loginTitle').textContent = 'Shareit ID 登录'; + $('loginSubtitle').textContent = '输入你的 Shareit ID 继续'; + $('loginUser').focus(); + } + + $('accountForm').addEventListener('submit', (event) => { + event.preventDefault(); + const username = $('loginUser').value.trim().toLowerCase(); + if (!username) { + $('accountMessage').textContent = '请输入 Shareit ID'; + return; + } + if (!auth.findUser(username)) { + $('accountMessage').textContent = '该 Shareit ID 不在授权名单中'; + return; + } + resetForms(); + openAccount(username); + }); + $('signinForm').addEventListener('submit', async (event) => { event.preventDefault(); const button = event.submitter; button.disabled = true; $('signinMessage').textContent = '正在验证…'; try { - await auth.login(userSelect.value, $('signinPassword').value, $('signinCode').value); + await auth.login(selectedUsername, $('signinPassword').value, $('signinCode').value); location.href = nextPage(); } catch (error) { $('signinMessage').textContent = error.message; @@ -62,11 +110,15 @@ $('enrollMessage').textContent = '两次输入的密码不一致'; return; } + if (typeof window.qrcode !== 'function') { + $('enrollMessage').textContent = '二维码组件尚未加载,请刷新页面重试'; + return; + } const button = event.submitter; button.disabled = true; - $('enrollMessage').textContent = '正在绑定…'; + $('enrollMessage').textContent = '正在注册…'; try { - await auth.enroll(userSelect.value, password, currentEnrollment().secret, $('setupCode').value); + await auth.enroll(selectedUsername, password, currentEnrollment().secret, $('setupCode').value); location.href = nextPage(); } catch (error) { $('enrollMessage').textContent = error.message; @@ -74,32 +126,20 @@ } }); - $('copySetupKey').addEventListener('click', async () => { - const secret = currentEnrollment().secret; - try { await navigator.clipboard.writeText(secret); } - catch (_) { - const area = document.createElement('textarea'); area.value = secret; - document.body.appendChild(area); area.select(); document.execCommand('copy'); area.remove(); - } - showToast('设置密钥已复制'); - }); - + $('changeAccount').addEventListener('click', changeAccount); $('resetEnrollment').addEventListener('click', () => { - const username = userSelect.value; - if (!window.confirm(`确定清除 ${username} 在当前浏览器的密码和 Authenticator 绑定吗?`)) return; + if (!window.confirm(`确定清除 ${selectedUsername} 在当前浏览器的密码和 Authenticator 绑定吗?`)) return; try { - auth.clearEnrollment(username); - enrollmentByUser.delete(username); - $('signinPassword').value = ''; - $('signinCode').value = ''; - renderMode(); - showToast('本机绑定已清除,请重新设置'); + auth.clearEnrollment(selectedUsername); + enrollmentByUser.delete(selectedUsername); + resetForms(); + openAccount(selectedUsername); + showToast('本机绑定已清除,请重新注册'); } catch (error) { $('signinMessage').textContent = error.message; } }); - userSelect.addEventListener('change', renderMode); if (auth.currentUser()) location.replace(nextPage()); - else renderMode(); + else $('loginUser').focus(); })(); diff --git a/assets/styles.css b/assets/styles.css index 30e0cd0..72ac4d3 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -16,3 +16,7 @@ .history-shortcut{min-height:72px;padding:15px 21px;display:flex;align-items:center;gap:13px;transition:border-color .2s,transform .2s}.history-shortcut:hover{border-color:#9eaff4;transform:translateY(-2px)}.history-shortcut-icon{width:37px;height:37px;border-radius:50%;background:#e5eaff;color:var(--blue-dark);display:grid;place-items:center;font-size:19px}.history-shortcut p{display:flex;flex-direction:column;margin:0}.history-shortcut strong{font-size:12px}.history-shortcut small{font-size:9px;color:var(--muted)}.history-shortcut>b{margin-left:auto;min-width:26px;height:23px;padding:0 7px;border-radius:99px;background:#eaf7c9;color:#58790a;display:grid;place-items:center;font:9px var(--mono)}.history-shortcut>i{font-style:normal;font-size:17px}.history-page-panel{min-height:440px}.history-heading-actions{display:flex;align-items:center;gap:8px}.link-button{display:inline-flex;align-items:center;justify-content:center}.history-toolbar{display:flex;align-items:flex-end;gap:16px;padding:17px 0}.history-toolbar label{width:min(420px,100%)}.history-toolbar label>span{display:block;font-size:9px;font-weight:700;margin-bottom:5px}.history-toolbar>span{margin-left:auto;color:var(--muted);font:9px var(--mono);padding-bottom:12px}.history-empty{height:245px}.history-empty h3{font-size:13px;color:var(--ink);margin:9px 0 1px}.history-empty a{color:var(--blue-dark);font-size:10px}.auth-history-table{min-width:1030px}.auth-history-table th:first-child{width:145px}.auth-history-table th:nth-child(2){width:115px}.auth-history-table th:nth-child(3){width:95px}.auth-history-table th:nth-child(4){width:190px}.record-user{display:inline-block;border-radius:99px;background:#eef0ed;color:#42504b;padding:4px 7px;font:8px var(--mono)} @media(max-width:900px){.login-main{grid-template-columns:1fr;gap:34px;padding-block:45px}.login-intro h1{font-size:48px}.login-intro>p:not(.eyebrow){max-width:620px}.login-card{width:min(100%,520px)}.header-user span small{display:none}} @media(max-width:620px){.header-user>span{display:none}.header-user{gap:4px}.main-nav{gap:9px}.main-nav a{font-size:9px}.login-main{padding-block:38px}.login-intro h1{font-size:39px}.login-card{padding:22px}.setup-steps{grid-template-columns:1fr}.history-shortcut{padding:13px 15px}.history-shortcut small{display:none}.history-heading-actions{align-items:stretch;flex-direction:column}.history-toolbar{align-items:stretch;flex-direction:column}.history-toolbar>span{margin:0;padding:0}.auth-history-table{min-width:940px}} + +/* Shareit ID entry and local QR enrollment */ +.account-form{margin-top:20px}.confirmed-account{display:flex;align-items:center;gap:10px;margin-top:18px;padding:11px 13px;border:1px solid var(--line);border-radius:10px;background:#f6f6f2}.confirmed-account>span{font:8px var(--mono);color:var(--muted)}.confirmed-account>strong{font:12px var(--mono)}.confirmed-account>button{margin-left:auto;border:0;background:transparent;color:var(--blue-dark);font-size:9px}.qr-setup{display:grid;grid-template-columns:190px 1fr;gap:18px;align-items:center;margin:18px 0 22px;padding:17px;border:1px solid var(--line);border-radius:13px;background:#fafaf7}.qr-code{width:190px;min-height:190px;display:grid;place-items:center;background:#fff;border:1px solid #e8eae6;border-radius:10px;overflow:hidden}.qr-code svg{display:block;width:180px;height:180px}.qr-code>span{padding:15px;text-align:center;font-size:9px;color:var(--muted)}.qr-code .qr-error{color:#b74b39}.qr-instructions>b{display:inline-block;border-radius:99px;background:#e5eaff;color:#2d47b3;padding:4px 8px;font-size:8px}.qr-instructions ol{margin:13px 0 0;padding-left:20px}.qr-instructions li{margin:8px 0;font-size:10px;color:#58635f}.header-user>span{font:10px var(--mono);color:var(--ink)} +@media(max-width:620px){.qr-setup{grid-template-columns:1fr}.qr-code{margin:auto}.confirmed-account{flex-wrap:wrap}} diff --git a/link-generator.html b/link-generator.html index 30fa14c..a3ee80d 100644 --- a/link-generator.html +++ b/link-generator.html @@ -5,7 +5,7 @@ 自投链接转换器 · Traffic Intelligence - +