@flun/html-template 4.4.3 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/customize/account.js +125 -64
- package/dev-server.js +1 -1
- package/f-CHANGELOG.md +18 -7
- package/f-README.md +22 -23
- package/package.json +1 -1
- package/templates/account/2fa.html +192 -111
- package/templates/account/forgot-password.html +3 -4
- package/templates/account/login.html +17 -51
- package/templates/account/profile.html +446 -363
- package/templates/account/register.html +3 -2
- package/templates/account/reset-password.html +4 -4
- package/templates/account/verify-email.html +7 -6
|
@@ -5,28 +5,24 @@
|
|
|
5
5
|
<meta charset="UTF-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>双因素验证处理页</title>
|
|
8
|
-
|
|
9
|
-
<link rel="stylesheet" href="/static/
|
|
10
|
-
<link rel="stylesheet" href="/static/
|
|
11
|
-
<link rel="stylesheet" href="/static/
|
|
12
|
-
<link rel="stylesheet" href="/static/topImg.css" /> <!-- 返回顶部图标 -->
|
|
13
|
-
|
|
8
|
+
<link rel="stylesheet" href="/static/constants.css" />
|
|
9
|
+
<link rel="stylesheet" href="/static/public.css" />
|
|
10
|
+
<link rel="stylesheet" href="/static/themeImg.css" />
|
|
11
|
+
<link rel="stylesheet" href="/static/topImg.css" />
|
|
14
12
|
<style>
|
|
15
|
-
/* 覆盖 body 布局:本页需要居中卡片,而非默认的纵向弹性布局 */
|
|
16
13
|
body {
|
|
17
14
|
display: flex;
|
|
18
15
|
align-items: center;
|
|
19
16
|
justify-content: center;
|
|
20
17
|
padding: 20px;
|
|
21
|
-
/* 背景由公共变量接管,自动适配深浅色 */
|
|
22
18
|
background-color: var(--bg-color);
|
|
23
19
|
background-image: var(--body-bg);
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
/* 卡片容器 */
|
|
27
22
|
.card {
|
|
28
23
|
background: var(--container-bg);
|
|
29
24
|
border-radius: 12px;
|
|
25
|
+
border: 1px solid #2e3135;
|
|
30
26
|
box-shadow: 0 20px 40px var(--content-shadow);
|
|
31
27
|
width: 100%;
|
|
32
28
|
max-width: 400px;
|
|
@@ -34,7 +30,6 @@
|
|
|
34
30
|
transition: background 0.5s ease, box-shadow 0.5s ease;
|
|
35
31
|
}
|
|
36
32
|
|
|
37
|
-
/* 卡片标题 */
|
|
38
33
|
.card h2 {
|
|
39
34
|
color: var(--h2-color);
|
|
40
35
|
margin-bottom: 20px;
|
|
@@ -43,18 +38,10 @@
|
|
|
43
38
|
font-size: 28px;
|
|
44
39
|
}
|
|
45
40
|
|
|
46
|
-
/* 提示文字 */
|
|
47
|
-
.card p {
|
|
48
|
-
text-align: center;
|
|
49
|
-
margin-bottom: 20px;
|
|
50
|
-
color: var(--p-color);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
41
|
.form-group {
|
|
54
42
|
margin-bottom: 20px;
|
|
55
43
|
}
|
|
56
44
|
|
|
57
|
-
/* 验证码输入框 */
|
|
58
45
|
.form-group input {
|
|
59
46
|
width: 100%;
|
|
60
47
|
padding: 12px 16px;
|
|
@@ -62,20 +49,20 @@
|
|
|
62
49
|
border-radius: 8px;
|
|
63
50
|
font-size: 16px;
|
|
64
51
|
text-align: center;
|
|
65
|
-
letter-spacing: 4px;
|
|
66
52
|
background: var(--li-bg);
|
|
67
53
|
color: var(--text-color);
|
|
68
|
-
transition: border-color 0.3s ease, background 0.5s ease, color 0.5s ease;
|
|
69
54
|
}
|
|
70
55
|
|
|
71
56
|
.form-group input:focus {
|
|
72
57
|
border-color: var(--link-color);
|
|
73
58
|
}
|
|
74
59
|
|
|
75
|
-
|
|
60
|
+
.totp-input {
|
|
61
|
+
letter-spacing: 4px;
|
|
62
|
+
}
|
|
63
|
+
|
|
76
64
|
.btn {
|
|
77
65
|
width: 100%;
|
|
78
|
-
display: block;
|
|
79
66
|
padding: 14px;
|
|
80
67
|
background: var(--btn-bg);
|
|
81
68
|
color: var(--text-color);
|
|
@@ -84,7 +71,6 @@
|
|
|
84
71
|
font-size: 16px;
|
|
85
72
|
font-weight: 600;
|
|
86
73
|
cursor: pointer;
|
|
87
|
-
margin-top: 0;
|
|
88
74
|
box-shadow: 0 4px 12px var(--content-shadow);
|
|
89
75
|
transition: background 0.3s ease, transform 0.2s ease;
|
|
90
76
|
}
|
|
@@ -101,7 +87,15 @@
|
|
|
101
87
|
transform: none;
|
|
102
88
|
}
|
|
103
89
|
|
|
104
|
-
|
|
90
|
+
.btn-secondary {
|
|
91
|
+
background: var(--btn-secondary-bg, #6c757d);
|
|
92
|
+
margin-top: 10px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.btn-secondary:hover {
|
|
96
|
+
background: var(--btn-secondary-hover, #5a6268);
|
|
97
|
+
}
|
|
98
|
+
|
|
105
99
|
.error {
|
|
106
100
|
color: #e53e3e;
|
|
107
101
|
font-size: 14px;
|
|
@@ -109,76 +103,34 @@
|
|
|
109
103
|
text-align: center;
|
|
110
104
|
}
|
|
111
105
|
|
|
112
|
-
|
|
113
|
-
.backup-link {
|
|
106
|
+
.backup-panel {
|
|
114
107
|
margin-top: 20px;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
color: var(--text-color);
|
|
108
|
+
padding-top: 15px;
|
|
109
|
+
border-top: 1px solid var(--content-border);
|
|
118
110
|
}
|
|
119
111
|
|
|
120
|
-
.
|
|
121
|
-
|
|
122
|
-
|
|
112
|
+
.hardware-loader {
|
|
113
|
+
text-align: center;
|
|
114
|
+
margin: 20px 0;
|
|
115
|
+
color: var(--text-color);
|
|
123
116
|
}
|
|
124
117
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
text-decoration: underline;
|
|
118
|
+
[hidden] {
|
|
119
|
+
display: none !important;
|
|
128
120
|
}
|
|
129
121
|
|
|
130
|
-
/* ==================== 移动端样式适配 (≤640px) ==================== */
|
|
131
122
|
@media (max-width: 640px) {
|
|
132
|
-
body {
|
|
133
|
-
padding: 16px;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
123
|
.card {
|
|
137
|
-
max-width: 100%;
|
|
138
124
|
padding: 28px 20px;
|
|
139
|
-
border-radius: 16px;
|
|
140
125
|
}
|
|
141
126
|
|
|
142
127
|
.card h2 {
|
|
143
128
|
font-size: 24px;
|
|
144
|
-
margin-bottom: 16px;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.card p {
|
|
148
|
-
font-size: 14px;
|
|
149
|
-
margin-bottom: 16px;
|
|
150
129
|
}
|
|
151
130
|
|
|
152
131
|
.form-group input {
|
|
153
132
|
font-size: 16px;
|
|
154
|
-
/* 防止iOS缩放 */
|
|
155
|
-
padding: 12px 12px;
|
|
156
|
-
letter-spacing: 2px;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.btn {
|
|
160
133
|
padding: 12px;
|
|
161
|
-
font-size: 16px;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.backup-link {
|
|
165
|
-
font-size: 13px;
|
|
166
|
-
margin-top: 18px;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/* 极小屏幕 (≤480px) 进一步微调 */
|
|
171
|
-
@media (max-width: 480px) {
|
|
172
|
-
.card {
|
|
173
|
-
padding: 24px 16px;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
.card h2 {
|
|
177
|
-
font-size: 22px;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
.form-group input {
|
|
181
|
-
padding: 10px 10px;
|
|
182
134
|
}
|
|
183
135
|
}
|
|
184
136
|
</style>
|
|
@@ -187,52 +139,181 @@
|
|
|
187
139
|
<body>
|
|
188
140
|
<div class="card">
|
|
189
141
|
<h2>双因素验证</h2>
|
|
190
|
-
|
|
191
|
-
|
|
142
|
+
|
|
143
|
+
<!-- TOTP 模式区域 -->
|
|
144
|
+
<div id="totpSection" hidden>
|
|
145
|
+
<p id="promptText">请输入身份验证器中的6位数字验证码</p>
|
|
192
146
|
<div class="form-group">
|
|
193
|
-
<input type="text" id="
|
|
194
|
-
autocomplete="one-time-code">
|
|
147
|
+
<input type="text" id="totpInput" placeholder="6位验证码" maxlength="6" autofocus
|
|
148
|
+
autocomplete="one-time-code" class="totp-input">
|
|
195
149
|
</div>
|
|
196
|
-
<button type="button" class="btn" id="
|
|
197
|
-
</form>
|
|
198
|
-
<div class="error" id="message"></div>
|
|
199
|
-
<div class="backup-link">
|
|
200
|
-
<a href="#" id="useBackup">使用备用码</a>
|
|
150
|
+
<button type="button" class="btn" id="verifyTotpBtn">验证</button>
|
|
201
151
|
</div>
|
|
152
|
+
|
|
153
|
+
<!-- 硬件验证专用区域 -->
|
|
154
|
+
<div id="webauthnSection" hidden>
|
|
155
|
+
<p>正在请求硬件验证,请按提示操作...</p>
|
|
156
|
+
<div class="hardware-loader">
|
|
157
|
+
<button type="button" class="btn btn-secondary" id="retryHardwareBtn">重新尝试硬件验证</button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<!-- 备份码面板(异常时动态显示) -->
|
|
162
|
+
<div id="backupPanel" class="backup-panel" hidden>
|
|
163
|
+
<div class="form-group">
|
|
164
|
+
<input type="text" id="backupCodeInput" placeholder="请输入10位备用码" maxlength="10" autocomplete="off">
|
|
165
|
+
</div>
|
|
166
|
+
<button type="button" class="btn" id="submitBackupBtn">登录</button>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div class="error" id="message"></div>
|
|
202
170
|
</div>
|
|
203
171
|
|
|
204
|
-
<!--
|
|
205
|
-
<script src="/static/themeModule.js" defer></script
|
|
206
|
-
<script src="/static/mouseOrTouch.js" defer></script
|
|
207
|
-
<script src="/static/themeImg.js" defer></script>
|
|
208
|
-
<script src="/static/topImg.js" defer></script>
|
|
172
|
+
<!-- 公共脚本 -->
|
|
173
|
+
<script src="/static/themeModule.js" defer></script>
|
|
174
|
+
<script src="/static/mouseOrTouch.js" defer></script>
|
|
175
|
+
<script src="/static/themeImg.js" defer></script>
|
|
176
|
+
<script src="/static/topImg.js" defer></script>
|
|
177
|
+
<script src="/static/utils/browser.js"></script>
|
|
209
178
|
<script>
|
|
210
|
-
const [
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
179
|
+
const [totpSection, webauthnSection, backupPanel, messageDiv, totpInput, verifyTotpBtn, backupCodeInput, submitBackupBtn,
|
|
180
|
+
retryHardwareBtn] = ['totpSection', 'webauthnSection', 'backupPanel', 'message', 'totpInput',
|
|
181
|
+
'verifyTotpBtn', 'backupCodeInput', 'submitBackupBtn', 'retryHardwareBtn']
|
|
182
|
+
.map(id => document.getElementById(id)),
|
|
183
|
+
urlParams = new URLSearchParams(window.location.search), method = urlParams.get('method'),
|
|
184
|
+
urlUsername = urlParams.get('username') || '', maxNum = 3; // 失败次数达到3次时显示备用码面板
|
|
185
|
+
|
|
186
|
+
let hardwareRunning = false, currentUsername = urlUsername;
|
|
187
|
+
const setMessage = msg => messageDiv.textContent = msg, clearMessage = () => messageDiv.textContent = '',
|
|
188
|
+
// 禁用/启用表单控件
|
|
189
|
+
setFormDisabled = disabled => {
|
|
190
|
+
const inputs = [totpInput, backupCodeInput, verifyTotpBtn, submitBackupBtn, retryHardwareBtn];
|
|
191
|
+
inputs.forEach(el => { if (el) el.disabled = disabled; });
|
|
214
192
|
},
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
193
|
+
// 显示备份码面板
|
|
194
|
+
showBackupPanel = optionalMsg => {
|
|
195
|
+
if (optionalMsg) setMessage(optionalMsg);
|
|
196
|
+
totpSection.hidden = true, webauthnSection.hidden = true, backupPanel.hidden = false;
|
|
197
|
+
backupCodeInput.value = '', backupCodeInput.focus();
|
|
198
|
+
},
|
|
199
|
+
// 统一处理验证响应(TOTP 和备份码共用)
|
|
200
|
+
handleVerifyResponse = async token => {
|
|
201
|
+
if (!token) {
|
|
202
|
+
setMessage('请输入验证码/备用码');
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
const totp = token.length === 6, isBackup = token.length === 10;
|
|
206
|
+
if (!totp && !isBackup) {
|
|
207
|
+
setMessage('验证码必须为6位数字,备用码为10位');
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 临时禁用提交按钮防止重复请求
|
|
212
|
+
const [activeInput, activeBtn] = totp ? [totpInput, verifyTotpBtn] : [backupCodeInput, submitBackupBtn];
|
|
213
|
+
activeBtn.disabled = true, clearMessage();
|
|
214
|
+
|
|
219
215
|
try {
|
|
220
216
|
const response = await fetch('/api/verify-2fa', {
|
|
221
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
217
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
218
|
+
body: JSON.stringify({ token })
|
|
222
219
|
}), data = await response.json();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
220
|
+
|
|
221
|
+
if (response.ok) {
|
|
222
|
+
window.location.href = '/';
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 处理锁定情况
|
|
227
|
+
if (data.locked) {
|
|
228
|
+
setMessage(data.message), setFormDisabled(true);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 处理普通失败(包含 remainingAttempts)
|
|
233
|
+
if (data.remainingAttempts !== undefined) {
|
|
234
|
+
const failuresSoFar = 9 - data.remainingAttempts;
|
|
235
|
+
setMessage(`${data.message} (已失败 ${failuresSoFar} 次)`);
|
|
236
|
+
if (failuresSoFar >= maxNum && totp) showBackupPanel(`连续失败 ${failuresSoFar} 次,请使用备用码登录`);
|
|
237
|
+
}
|
|
238
|
+
else setMessage(data.message);
|
|
239
|
+
activeInput.value = '', activeInput.focus(), activeBtn.disabled = false;
|
|
240
|
+
return false;
|
|
241
|
+
} catch (err) {
|
|
242
|
+
setMessage(err.message), activeBtn.disabled = false;
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
// TOTP验证和备份码提交入口
|
|
247
|
+
performTotpVerification = () => handleVerifyResponse(totpInput.value.trim()),
|
|
248
|
+
submitBackupCode = () => handleVerifyResponse(backupCodeInput.value.trim()),
|
|
249
|
+
// 硬件验证核心
|
|
250
|
+
performHardwareVerification = async () => {
|
|
251
|
+
if (!window.PublicKeyCredential) {
|
|
252
|
+
showBackupPanel('当前浏览器不支持硬件安全验证,请使用备用码登录');
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
if (!currentUsername) {
|
|
256
|
+
showBackupPanel('无法获取用户身份,请使用备用码登录');
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
if (retryHardwareBtn && !retryHardwareBtn.hasListener)
|
|
260
|
+
retryHardwareBtn.addEventListener('click', () => performHardwareVerification()), retryHardwareBtn.hasListener = true;
|
|
261
|
+
if (hardwareRunning) return false;
|
|
262
|
+
|
|
263
|
+
hardwareRunning = true, clearMessage();
|
|
264
|
+
try {
|
|
265
|
+
let platformAvailable = false;
|
|
266
|
+
if (window.PublicKeyCredential) {
|
|
267
|
+
try {
|
|
268
|
+
platformAvailable = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
269
|
+
} catch (err) { console.warn(err); }
|
|
270
|
+
}
|
|
271
|
+
if (!platformAvailable) throw new Error('当前环境不支持平台硬件验证');
|
|
272
|
+
|
|
273
|
+
const beginRes = await fetch('/api/webauthn/login/begin', {
|
|
274
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
body: JSON.stringify({ username: currentUsername })
|
|
276
|
+
}), options = await beginRes.json();
|
|
277
|
+
|
|
278
|
+
if (!beginRes.ok) throw new Error(options.message);
|
|
279
|
+
const assertionResp = await flunWebAuthnBrowser.startAuthentication(options),
|
|
280
|
+
completeRes = await fetch('/api/webauthn/login/complete', {
|
|
281
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
282
|
+
body: JSON.stringify({ assertionResponse: assertionResp })
|
|
283
|
+
}), completeData = await completeRes.json();
|
|
284
|
+
|
|
285
|
+
if (completeRes.ok) {
|
|
286
|
+
window.location.href = '/';
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
else throw new Error(completeData.message);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
let errorMsg = err.message;
|
|
292
|
+
if (err.name === 'NotAllowedError') errorMsg = '硬件验证已取消';
|
|
293
|
+
showBackupPanel(`${errorMsg},请使用备用码登录`);
|
|
294
|
+
return false;
|
|
295
|
+
} finally { hardwareRunning = false }
|
|
296
|
+
},
|
|
297
|
+
// 页面初始化
|
|
298
|
+
initPage = () => {
|
|
299
|
+
backupPanel.hidden = true;
|
|
300
|
+
if (method === 'webauthn')
|
|
301
|
+
totpSection.hidden = true, webauthnSection.hidden = false, performHardwareVerification().catch(e => console.warn(e));
|
|
302
|
+
else totpSection.hidden = false, webauthnSection.hidden = true, totpInput.focus();
|
|
303
|
+
|
|
304
|
+
// 事件监听
|
|
305
|
+
submitBackupBtn.addEventListener('click', submitBackupCode);
|
|
306
|
+
verifyTotpBtn.addEventListener('click', performTotpVerification);
|
|
307
|
+
backupCodeInput.addEventListener('keypress', e => {
|
|
308
|
+
if (e.key === 'Enter') submitBackupCode();
|
|
309
|
+
});
|
|
310
|
+
totpInput.addEventListener('keypress', e => {
|
|
311
|
+
if (e.key === 'Enter') performTotpVerification();
|
|
312
|
+
});
|
|
313
|
+
[backupCodeInput, totpInput].forEach(el => el.addEventListener('input', clearMessage));
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
initPage();
|
|
236
317
|
</script>
|
|
237
318
|
</body>
|
|
238
319
|
|
|
@@ -18,16 +18,15 @@
|
|
|
18
18
|
align-items: center;
|
|
19
19
|
justify-content: center;
|
|
20
20
|
padding: 20px;
|
|
21
|
-
/* 背景由公共变量接管,自动适配深浅色 */
|
|
22
21
|
background-color: var(--bg-color);
|
|
23
22
|
background-image: var(--body-bg);
|
|
24
|
-
/* 保留页面原有的字体偏好,覆盖 public.css 中的通用字体 */
|
|
25
23
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
.card {
|
|
29
27
|
background: var(--container-bg);
|
|
30
28
|
border-radius: 12px;
|
|
29
|
+
border: 1px solid #2e3135;
|
|
31
30
|
box-shadow: 0 20px 40px var(--content-shadow);
|
|
32
31
|
width: 100%;
|
|
33
32
|
max-width: 400px;
|
|
@@ -129,8 +128,8 @@
|
|
|
129
128
|
text-decoration: underline;
|
|
130
129
|
}
|
|
131
130
|
|
|
132
|
-
|
|
133
|
-
display: none;
|
|
131
|
+
[hidden] {
|
|
132
|
+
display: none !important;
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
/* ==================== 移动端样式适配 (≤640px) ==================== */
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
.card {
|
|
31
31
|
background: var(--container-bg);
|
|
32
32
|
border-radius: 12px;
|
|
33
|
+
border: 1px solid #2e3135;
|
|
33
34
|
box-shadow: 0 20px 40px var(--content-shadow);
|
|
34
35
|
width: 100%;
|
|
35
36
|
max-width: 400px;
|
|
@@ -156,7 +157,6 @@
|
|
|
156
157
|
|
|
157
158
|
.form-group input {
|
|
158
159
|
font-size: 16px;
|
|
159
|
-
/* 防止iOS缩放 */
|
|
160
160
|
padding: 12px 14px;
|
|
161
161
|
}
|
|
162
162
|
|
|
@@ -175,7 +175,6 @@
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
/* 极小屏幕 (≤480px) 微调 */
|
|
179
178
|
@media (max-width: 480px) {
|
|
180
179
|
.card {
|
|
181
180
|
padding: 24px 16px;
|
|
@@ -217,77 +216,44 @@
|
|
|
217
216
|
</div>
|
|
218
217
|
|
|
219
218
|
<!-- 公共逻辑 -->
|
|
220
|
-
<script src="/static/themeModule.js" defer></script
|
|
221
|
-
<script src="/static/mouseOrTouch.js" defer></script
|
|
222
|
-
<script src="/static/themeImg.js" defer></script>
|
|
223
|
-
<script src="/static/topImg.js" defer></script>
|
|
224
|
-
<script src="/static/utils/browser.js"></script> <!-- 引入@flun前端硬件验证模块 -->
|
|
219
|
+
<script src="/static/themeModule.js" defer></script>
|
|
220
|
+
<script src="/static/mouseOrTouch.js" defer></script>
|
|
221
|
+
<script src="/static/themeImg.js" defer></script>
|
|
222
|
+
<script src="/static/topImg.js" defer></script>
|
|
225
223
|
<script>
|
|
226
|
-
const [usernameEl, passwordEl, messageDiv, loginBtn] = ['username', 'password', 'message', 'loginBtn']
|
|
224
|
+
const [usernameEl, passwordEl, messageDiv, loginBtn,] = ['username', 'password', 'message', 'loginBtn',]
|
|
227
225
|
.map(id => document.getElementById(id)),
|
|
228
226
|
handleError = msg => {
|
|
229
227
|
messageDiv.textContent = msg, loginBtn.disabled = false;
|
|
230
228
|
};
|
|
231
229
|
|
|
230
|
+
// 登录主逻辑(统一跳转到2fa页面,携带对应验证方式)
|
|
232
231
|
loginBtn.addEventListener('click', async () => {
|
|
233
232
|
loginBtn.disabled = true, messageDiv.textContent = '';
|
|
233
|
+
|
|
234
234
|
const [username, password] = [usernameEl, passwordEl].map(input => input.value.trim());
|
|
235
235
|
if (!username || !password) return handleError('请输入用户名和密码');
|
|
236
|
-
|
|
237
236
|
try {
|
|
238
|
-
// 密码验证
|
|
239
237
|
const response = await fetch('/api/login', {
|
|
240
|
-
method: 'POST',
|
|
241
|
-
headers: { 'Content-Type': 'application/json' },
|
|
238
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
242
239
|
body: JSON.stringify({ username, password })
|
|
243
240
|
}), data = await response.json();
|
|
244
241
|
|
|
245
242
|
if (!response.ok) return handleError(data.message);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
platformAvailable = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
252
|
-
} catch (err) { console.warn(err) };
|
|
253
|
-
}
|
|
254
|
-
if (!platformAvailable)
|
|
255
|
-
return handleError('当前环境不支持硬件验证,请使用HTTPS或localhost访问,并确保浏览器支持指纹/人脸功能;');
|
|
256
|
-
try {
|
|
257
|
-
// 获取 WebAuthn 登录选项
|
|
258
|
-
const beginRes = await fetch('/api/webauthn/login/begin', {
|
|
259
|
-
method: 'POST',
|
|
260
|
-
headers: { 'Content-Type': 'application/json' },
|
|
261
|
-
body: JSON.stringify({ username })
|
|
262
|
-
}), options = await beginRes.json();
|
|
263
|
-
if (!beginRes.ok) return handleError(options.message);
|
|
264
|
-
|
|
265
|
-
// 调用浏览器硬件验证
|
|
266
|
-
const assertionResp = await flunWebAuthnBrowser.startAuthentication(options),
|
|
267
|
-
completeRes = await fetch('/api/webauthn/login/complete', {
|
|
268
|
-
method: 'POST',
|
|
269
|
-
headers: { 'Content-Type': 'application/json' },
|
|
270
|
-
body: JSON.stringify({ assertionResponse: assertionResp })
|
|
271
|
-
}), completeData = await completeRes.json();
|
|
272
|
-
|
|
273
|
-
if (completeRes.ok) window.location.href = '/';
|
|
274
|
-
else return handleError(completeData.message);
|
|
275
|
-
} catch (webauthnErr) {
|
|
276
|
-
let msg = webauthnErr.message;
|
|
277
|
-
if (webauthnErr.name === 'NotAllowedError') msg = '认证操作已取消';
|
|
278
|
-
handleError(msg);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
else if (data.require2FA) window.location.href = '/2fa';
|
|
282
|
-
else if (data.success) window.location.href = '/';
|
|
243
|
+
if (data.requireWebAuthn)
|
|
244
|
+
window.location.href = `/2fa?method=webauthn&username=${encodeURIComponent(username)}`;
|
|
245
|
+
else if (data.require2FA)
|
|
246
|
+
window.location.href = `/2fa?method=totp&username=${encodeURIComponent(username)}`;
|
|
247
|
+
else if (data.success) window.location.href = '/'; // 无二次验证,直接进入首页
|
|
283
248
|
else handleError(data.message);
|
|
284
|
-
} catch (err) { handleError(
|
|
249
|
+
} catch (err) { handleError(err.message) }
|
|
285
250
|
});
|
|
286
251
|
|
|
252
|
+
// 输入时清除错误消息
|
|
253
|
+
[usernameEl, passwordEl].forEach(el => el.addEventListener('input', () => messageDiv.textContent = ''));
|
|
287
254
|
passwordEl.addEventListener('keypress', e => {
|
|
288
255
|
if (e.key === 'Enter') loginBtn.click();
|
|
289
256
|
});
|
|
290
|
-
[usernameEl, passwordEl].forEach(el => el.addEventListener('input', () => messageDiv.textContent = ''));
|
|
291
257
|
</script>
|
|
292
258
|
</body>
|
|
293
259
|
|