feat: register Shareit IDs with QR

This commit is contained in:
王源
2026-10-20 17:03:12 +08:00
parent 58f33f94cc
commit bc575812cb
9 changed files with 140 additions and 81 deletions
+2 -2
View File
@@ -7,10 +7,10 @@
- `index.html`:工具入口 - `index.html`:工具入口
- `channel-recommendation.html`:包名、品类、国家 → 渠道优先级建议 - `channel-recommendation.html`:包名、品类、国家 → 渠道优先级建议
- `link-generator.html`:登录后粘贴原始 Mintegral Click Link 并转换为已确认的平台宏 - `link-generator.html`:登录后粘贴原始 Mintegral Click Link 并转换为已确认的平台宏
- `login.html`:本机密码与 Google Authenticator 动态码验证 - `login.html`:输入 Shareit ID,本机设置密码并扫码绑定 Google Authenticator
- `link-history.html`:按登录用户查看独立的链接记录 - `link-history.html`:按登录用户查看独立的链接记录
登录、Authenticator 绑定和链接记录均保存在当前浏览器,不会跨设备同步;这是内部辅助识别方案,不可替代服务端身份认证或安全审计。 登录页不公开展示用户白名单或角色。登录、Authenticator 绑定和链接记录均保存在当前浏览器,不会跨设备同步;这是内部辅助识别方案,不可替代服务端身份认证或安全审计。二维码由固定版本的前端组件在本地生成,第三方说明见 `THIRD_PARTY_NOTICES.md`。
## 可维护数据 ## 可维护数据
+11
View File
@@ -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.
+1 -1
View File
@@ -14,7 +14,7 @@
let activeResult = null; let activeResult = null;
let inputTimer; let inputTimer;
$('currentUser').innerHTML = `<b>${escapeHtml(current.username)}</b><small>${current.role === 'admin' ? '管理员' : '普通用户'}</small>`; $('currentUser').textContent = current.username;
$('templateCount').textContent = `${templates.length} 个平台模板`; $('templateCount').textContent = `${templates.length} 个平台模板`;
templates.forEach((template, index) => platformSelect.add(new Option(template.name, index))); templates.forEach((template, index) => platformSelect.add(new Option(template.name, index)));
+2 -2
View File
@@ -68,8 +68,8 @@
clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 1800); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 1800);
} }
$('currentUser').innerHTML = `<b>${escapeHtml(current.username)}</b><small>${current.role === 'admin' ? '管理员' : '普通用户'}</small>`; $('currentUser').textContent = current.username;
$('historyScope').textContent = current.role === 'admin' ? '管理员可查看当前浏览器内全部用户的记录。' : '普通用户仅可查看当前浏览器内自己的记录。'; $('historyScope').textContent = '查看当前浏览器内你有权限访问的链接记录。';
$('logoutBtn').addEventListener('click', () => { auth.logout(); location.href = 'login.html'; }); $('logoutBtn').addEventListener('click', () => { auth.logout(); location.href = 'login.html'; });
$('historySearch').addEventListener('input', render); $('historySearch').addEventListener('input', render);
$('clearHistory').addEventListener('click', () => { $('clearHistory').addEventListener('click', () => {
+81 -41
View File
@@ -2,9 +2,7 @@
const auth = window.TrafficAuth; const auth = window.TrafficAuth;
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const enrollmentByUser = new Map(); const enrollmentByUser = new Map();
const userSelect = $('loginUser'); let selectedUsername = '';
auth.users.forEach((user) => userSelect.add(new Option(`${user.username} · ${user.role === 'admin' ? '管理员' : '普通用户'}`, user.username)));
function nextPage() { function nextPage() {
const next = new URLSearchParams(location.search).get('next'); const next = new URLSearchParams(location.search).get('next');
@@ -18,36 +16,86 @@
} }
function currentEnrollment() { function currentEnrollment() {
const username = userSelect.value; if (!enrollmentByUser.has(selectedUsername)) enrollmentByUser.set(selectedUsername, auth.createEnrollment(selectedUsername));
if (!enrollmentByUser.has(username)) enrollmentByUser.set(username, auth.createEnrollment(username)); return enrollmentByUser.get(selectedUsername);
return enrollmentByUser.get(username);
} }
function renderMode() { function renderQr(otpauthUrl) {
const username = userSelect.value; const container = $('qrCode');
const user = auth.findUser(username); container.innerHTML = '';
const enrolled = auth.isEnrolled(username); if (typeof window.qrcode !== 'function') {
$('selectedRole').innerHTML = `<span class="user-role ${user.role}">${user.role === 'admin' ? '管理员' : '普通用户'}</span><small>${enrolled ? '已在当前浏览器绑定' : '首次使用,需要完成本机设置'}</small>`; container.innerHTML = '<span class="qr-error">二维码加载失败,请刷新页面重试</span>';
$('signinForm').classList.toggle('hidden', !enrolled); return false;
$('enrollForm').classList.toggle('hidden', enrolled); }
$('loginTitle').textContent = enrolled ? '用户登录' : '首次绑定'; try {
$('loginSubtitle').textContent = enrolled ? '密码和动态验证码双重验证' : '设置密码并绑定 Google Authenticator'; const qr = window.qrcode(0, 'M');
qr.addData(otpauthUrl);
qr.make();
container.innerHTML = qr.createSvgTag(5, 4);
return true;
} catch (_) {
container.innerHTML = '<span class="qr-error">二维码生成失败,请刷新页面重试</span>';
return false;
}
}
function resetForms() {
['signinPassword','signinCode','newPassword','confirmPassword','setupCode'].forEach((id) => { $(id).value = ''; });
$('accountMessage').textContent = '';
$('signinMessage').textContent = ''; $('signinMessage').textContent = '';
$('enrollMessage').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) { if (!enrolled) {
const enrollment = currentEnrollment(); const ready = renderQr(currentEnrollment().otpauthUrl);
$('setupKey').textContent = enrollment.secret.match(/.{1,4}/g).join(' '); $('enrollMessage').textContent = ready ? '' : '二维码组件加载失败,暂时无法完成注册';
$('authAccountName').textContent = `Traffic Intelligence (${username})`;
} }
} }
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) => { $('signinForm').addEventListener('submit', async (event) => {
event.preventDefault(); event.preventDefault();
const button = event.submitter; const button = event.submitter;
button.disabled = true; button.disabled = true;
$('signinMessage').textContent = '正在验证…'; $('signinMessage').textContent = '正在验证…';
try { try {
await auth.login(userSelect.value, $('signinPassword').value, $('signinCode').value); await auth.login(selectedUsername, $('signinPassword').value, $('signinCode').value);
location.href = nextPage(); location.href = nextPage();
} catch (error) { } catch (error) {
$('signinMessage').textContent = error.message; $('signinMessage').textContent = error.message;
@@ -62,11 +110,15 @@
$('enrollMessage').textContent = '两次输入的密码不一致'; $('enrollMessage').textContent = '两次输入的密码不一致';
return; return;
} }
if (typeof window.qrcode !== 'function') {
$('enrollMessage').textContent = '二维码组件尚未加载,请刷新页面重试';
return;
}
const button = event.submitter; const button = event.submitter;
button.disabled = true; button.disabled = true;
$('enrollMessage').textContent = '正在绑定…'; $('enrollMessage').textContent = '正在注册…';
try { try {
await auth.enroll(userSelect.value, password, currentEnrollment().secret, $('setupCode').value); await auth.enroll(selectedUsername, password, currentEnrollment().secret, $('setupCode').value);
location.href = nextPage(); location.href = nextPage();
} catch (error) { } catch (error) {
$('enrollMessage').textContent = error.message; $('enrollMessage').textContent = error.message;
@@ -74,32 +126,20 @@
} }
}); });
$('copySetupKey').addEventListener('click', async () => { $('changeAccount').addEventListener('click', changeAccount);
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('设置密钥已复制');
});
$('resetEnrollment').addEventListener('click', () => { $('resetEnrollment').addEventListener('click', () => {
const username = userSelect.value; if (!window.confirm(`确定清除 ${selectedUsername} 在当前浏览器的密码和 Authenticator 绑定吗?`)) return;
if (!window.confirm(`确定清除 ${username} 在当前浏览器的密码和 Authenticator 绑定吗?`)) return;
try { try {
auth.clearEnrollment(username); auth.clearEnrollment(selectedUsername);
enrollmentByUser.delete(username); enrollmentByUser.delete(selectedUsername);
$('signinPassword').value = ''; resetForms();
$('signinCode').value = ''; openAccount(selectedUsername);
renderMode(); showToast('本机绑定已清除,请重新注册');
showToast('本机绑定已清除,请重新设置');
} catch (error) { } catch (error) {
$('signinMessage').textContent = error.message; $('signinMessage').textContent = error.message;
} }
}); });
userSelect.addEventListener('change', renderMode);
if (auth.currentUser()) location.replace(nextPage()); if (auth.currentUser()) location.replace(nextPage());
else renderMode(); else $('loginUser').focus();
})(); })();
+4
View File
@@ -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)} .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: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}} @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}}
+8 -8
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="登录后将原始 Tracking Link 转换为 Mintegral 平台宏"> <meta name="description" content="登录后将原始 Tracking Link 转换为 Mintegral 平台宏">
<title>自投链接转换器 · Traffic Intelligence</title> <title>自投链接转换器 · Traffic Intelligence</title>
<link rel="stylesheet" href="assets/styles.css?v=auth1"> <link rel="stylesheet" href="assets/styles.css?v=auth2">
</head> </head>
<body> <body>
<header class="site-header inner-header"> <header class="site-header inner-header">
@@ -56,12 +56,12 @@
<div class="toast" id="toast" role="status"></div> <div class="toast" id="toast" role="status"></div>
<footer class="site-footer shell"><span>Traffic Intelligence Hub</span><span>© <b data-year></b> Media Operations</span></footer> <footer class="site-footer shell"><span>Traffic Intelligence Hub</span><span>© <b data-year></b> Media Operations</span></footer>
<script src="assets/data/users.js?v=auth1"></script> <script src="assets/data/users.js?v=auth2"></script>
<script src="assets/auth.js?v=auth1"></script> <script src="assets/auth.js?v=auth2"></script>
<script src="assets/data/link-templates.js?v=auth1"></script> <script src="assets/data/link-templates.js?v=auth2"></script>
<script src="assets/link-converter-core.js?v=auth1"></script> <script src="assets/link-converter-core.js?v=auth2"></script>
<script src="assets/link-history-store.js?v=auth1"></script> <script src="assets/link-history-store.js?v=auth2"></script>
<script src="assets/common.js?v=auth1"></script> <script src="assets/common.js?v=auth2"></script>
<script src="assets/link-generator.js?v=auth1"></script> <script src="assets/link-generator.js?v=auth2"></script>
</body> </body>
</html> </html>
+6 -6
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="按用户查看 Mintegral 平台链接转换记录"> <meta name="description" content="按用户查看 Mintegral 平台链接转换记录">
<title>平台链接记录 · Traffic Intelligence</title> <title>平台链接记录 · Traffic Intelligence</title>
<link rel="stylesheet" href="assets/styles.css?v=auth1"> <link rel="stylesheet" href="assets/styles.css?v=auth2">
</head> </head>
<body> <body>
<header class="site-header inner-header"> <header class="site-header inner-header">
@@ -30,10 +30,10 @@
<div class="toast" id="toast" role="status"></div> <div class="toast" id="toast" role="status"></div>
<footer class="site-footer shell"><span>Traffic Intelligence Hub</span><span>© <b data-year></b> Media Operations</span></footer> <footer class="site-footer shell"><span>Traffic Intelligence Hub</span><span>© <b data-year></b> Media Operations</span></footer>
<script src="assets/data/users.js?v=auth1"></script> <script src="assets/data/users.js?v=auth2"></script>
<script src="assets/auth.js?v=auth1"></script> <script src="assets/auth.js?v=auth2"></script>
<script src="assets/link-history-store.js?v=auth1"></script> <script src="assets/link-history-store.js?v=auth2"></script>
<script src="assets/common.js?v=auth1"></script> <script src="assets/common.js?v=auth2"></script>
<script src="assets/link-history.js?v=auth1"></script> <script src="assets/link-history.js?v=auth2"></script>
</body> </body>
</html> </html>
+22 -18
View File
@@ -3,9 +3,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Traffic Intelligence 用户登录与 Google Authenticator 验证"> <meta name="description" content="使用 Shareit ID 与 Google Authenticator 登录 Traffic Intelligence">
<title>用户登录 · Traffic Intelligence</title> <title>用户登录 · Traffic Intelligence</title>
<link rel="stylesheet" href="assets/styles.css?v=auth1"> <link rel="stylesheet" href="assets/styles.css?v=auth2">
</head> </head>
<body class="login-page"> <body class="login-page">
<header class="site-header"> <header class="site-header">
@@ -17,15 +17,21 @@
<section class="login-intro"> <section class="login-intro">
<p class="eyebrow"><span></span> IDENTITY CHECK</p> <p class="eyebrow"><span></span> IDENTITY CHECK</p>
<h1>登录后开始<br><em>转换链接。</em></h1> <h1>登录后开始<br><em>转换链接。</em></h1>
<p>请选择自己的账号,并使用密码和 Google Authenticator 动态码完成验证。</p> <p>输入你的 Shareit ID,并使用密码和 Google Authenticator 动态码完成验证。</p>
<div class="login-note"><b>!</b><span><strong>简易本机验证</strong>账号绑定、登录状态和链接记录仅保存在当前浏览器,不属于服务端安全认证。</span></div> <div class="login-note"><b>!</b><span><strong>简易本机验证</strong>账号绑定、登录状态和链接记录仅保存在当前浏览器,不属于服务端安全认证。</span></div>
</section> </section>
<section class="panel login-card"> <section class="panel login-card">
<div class="panel-title"><span>ACCOUNT</span><h2 id="loginTitle">用户登录</h2><p id="loginSubtitle">选择你的账号</p></div> <div class="panel-title"><span>ACCOUNT</span><h2 id="loginTitle">Shareit ID 登录</h2><p id="loginSubtitle">输入你的 Shareit ID 继续</p></div>
<label class="field-label" for="loginUser">用户账号</label>
<select id="loginUser"></select> <form id="accountForm" class="account-form" novalidate>
<div class="selected-role" id="selectedRole"></div> <label class="field-label" for="loginUser">Shareit ID</label>
<input id="loginUser" type="text" autocomplete="username" autocapitalize="none" spellcheck="false" placeholder="请输入 Shareit ID">
<div class="auth-message" id="accountMessage" role="alert"></div>
<button class="primary-btn full-btn" type="submit"><span>继续</span><b>→</b></button>
</form>
<div class="confirmed-account hidden" id="confirmedAccount"><span>Shareit ID</span><strong id="confirmedUser"></strong><button id="changeAccount" type="button">更换账号</button></div>
<form id="signinForm" class="auth-form hidden" novalidate> <form id="signinForm" class="auth-form hidden" novalidate>
<label class="field-label" for="signinPassword">登录密码</label> <label class="field-label" for="signinPassword">登录密码</label>
@@ -38,13 +44,10 @@
</form> </form>
<form id="enrollForm" class="auth-form hidden" novalidate> <form id="enrollForm" class="auth-form hidden" novalidate>
<div class="setup-steps"> <div class="qr-setup">
<div><b>1</b><p><strong>打开 Google Authenticator</strong><span>点击“+”并选择“输入设置密钥”</span></p></div> <div class="qr-code" id="qrCode" aria-label="Google Authenticator 绑定二维码"><span>正在生成二维码…</span></div>
<div><b>2</b><p><strong>填写账号和设置密钥</strong><span>类型选择“基于时间”</span></p></div> <div class="qr-instructions"><b>首次注册</b><ol><li>打开 Google Authenticator</li><li>点击“+”并选择“扫描二维码”</li><li>扫描左侧二维码</li></ol></div>
</div> </div>
<label class="field-label">Authenticator 设置密钥</label>
<div class="setup-key"><code id="setupKey">生成中…</code><button id="copySetupKey" type="button">复制</button></div>
<p class="field-help">账号名称:<strong id="authAccountName"></strong></p>
<label class="field-label" for="newPassword">设置本机登录密码</label> <label class="field-label" for="newPassword">设置本机登录密码</label>
<input id="newPassword" type="password" autocomplete="new-password" placeholder="至少 8 位"> <input id="newPassword" type="password" autocomplete="new-password" placeholder="至少 8 位">
<label class="field-label" for="confirmPassword">再次输入密码</label> <label class="field-label" for="confirmPassword">再次输入密码</label>
@@ -52,15 +55,16 @@
<label class="field-label" for="setupCode">输入 App 中显示的 6 位验证码</label> <label class="field-label" for="setupCode">输入 App 中显示的 6 位验证码</label>
<input id="setupCode" class="otp-input" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="000000"> <input id="setupCode" class="otp-input" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="000000">
<div class="auth-message" id="enrollMessage" role="alert"></div> <div class="auth-message" id="enrollMessage" role="alert"></div>
<button class="primary-btn full-btn" type="submit"><span>完成绑定并登录</span><b>→</b></button> <button class="primary-btn full-btn" type="submit"><span>完成注册并登录</span><b>→</b></button>
</form> </form>
</section> </section>
</main> </main>
<div class="toast" id="toast" role="status"></div> <div class="toast" id="toast" role="status"></div>
<script src="assets/data/users.js?v=auth1"></script> <script src="assets/data/users.js?v=auth2"></script>
<script src="assets/auth.js?v=auth1"></script> <script src="assets/auth.js?v=auth2"></script>
<script src="assets/common.js?v=auth1"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js" integrity="sha512-ZDSPMa/JM1D+7kdg2x3BsruQ6T/JpJo3jWDWkCZsP+5yVyp1KfESqLI+7RqB5k24F7p2cV7i2YHh/890y6P6Sw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="assets/login.js?v=auth1"></script> <script src="assets/common.js?v=auth2"></script>
<script src="assets/login.js?v=auth2"></script>
</body> </body>
</html> </html>