@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
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
.card {
|
|
30
30
|
background: var(--container-bg);
|
|
31
31
|
border-radius: 12px;
|
|
32
|
+
border: 1px solid #2e3135;
|
|
32
33
|
box-shadow: 0 2px 10px var(--content-shadow);
|
|
33
34
|
padding: 30px;
|
|
34
35
|
margin-bottom: 20px;
|
|
@@ -69,23 +70,28 @@
|
|
|
69
70
|
/* 信息行 */
|
|
70
71
|
.info-row {
|
|
71
72
|
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
align-items: center;
|
|
75
|
+
text-align: center;
|
|
72
76
|
padding: 12px 0;
|
|
73
77
|
border-bottom: 1px solid var(--content-border);
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
.info-label {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
opacity: 0.75;
|
|
81
|
+
margin-bottom: 6px;
|
|
82
|
+
font-size: 13px;
|
|
80
83
|
font-weight: 500;
|
|
84
|
+
opacity: 0.65;
|
|
85
|
+
color: var(--text-color);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
.info-value {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
font-size: 16px;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
color: #4a5568;
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
|
|
89
95
|
/* 通用按钮 */
|
|
90
96
|
.btn {
|
|
91
97
|
padding: 10px 20px;
|
|
@@ -109,7 +115,6 @@
|
|
|
109
115
|
background: var(--btn-hover);
|
|
110
116
|
}
|
|
111
117
|
|
|
112
|
-
/* 功能按钮颜色保留原语义(不随主题变化) */
|
|
113
118
|
.btn-danger {
|
|
114
119
|
background: #e53e3e;
|
|
115
120
|
}
|
|
@@ -181,7 +186,6 @@
|
|
|
181
186
|
color: var(--text-color);
|
|
182
187
|
}
|
|
183
188
|
|
|
184
|
-
/* 消息提示(语义色保留) */
|
|
185
189
|
.message {
|
|
186
190
|
margin-top: 15px;
|
|
187
191
|
padding: 10px;
|
|
@@ -198,9 +202,54 @@
|
|
|
198
202
|
color: #742a2a;
|
|
199
203
|
}
|
|
200
204
|
|
|
201
|
-
.info {
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
#backupCodesPanel .info.info-border-flow {
|
|
206
|
+
margin-top: 15px;
|
|
207
|
+
border-style: dashed !important;
|
|
208
|
+
border-width: 1px !important;
|
|
209
|
+
border-top-color: #ff4d4d;
|
|
210
|
+
border-right-color: #4caf50;
|
|
211
|
+
border-bottom-color: #2196f3;
|
|
212
|
+
border-left-color: #9c27b0;
|
|
213
|
+
animation: borderColorFlow 4s linear infinite;
|
|
214
|
+
background-color: #6e523b;
|
|
215
|
+
border-radius: 6px;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@keyframes borderColorFlow {
|
|
219
|
+
0% {
|
|
220
|
+
border-top-color: #ff4d4d;
|
|
221
|
+
border-right-color: #4caf50;
|
|
222
|
+
border-bottom-color: #2196f3;
|
|
223
|
+
border-left-color: #9c27b0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
25% {
|
|
227
|
+
border-top-color: #9c27b0;
|
|
228
|
+
border-right-color: #ff4d4d;
|
|
229
|
+
border-bottom-color: #4caf50;
|
|
230
|
+
border-left-color: #2196f3;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
50% {
|
|
234
|
+
border-top-color: #2196f3;
|
|
235
|
+
border-right-color: #9c27b0;
|
|
236
|
+
border-bottom-color: #ff4d4d;
|
|
237
|
+
border-left-color: #4caf50;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
75% {
|
|
241
|
+
border-top-color: #4caf50;
|
|
242
|
+
border-right-color: #2196f3;
|
|
243
|
+
border-bottom-color: #9c27b0;
|
|
244
|
+
border-left-color: #ff4d4d;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
100% {
|
|
248
|
+
border-top-color: #ff4d4d;
|
|
249
|
+
border-right-color: #4caf50;
|
|
250
|
+
border-bottom-color: #2196f3;
|
|
251
|
+
border-left-color: #9c27b0;
|
|
252
|
+
}
|
|
204
253
|
}
|
|
205
254
|
|
|
206
255
|
.warning {
|
|
@@ -208,7 +257,6 @@
|
|
|
208
257
|
color: #7b341e;
|
|
209
258
|
}
|
|
210
259
|
|
|
211
|
-
/* 表单 */
|
|
212
260
|
.form-group {
|
|
213
261
|
margin-bottom: 15px;
|
|
214
262
|
}
|
|
@@ -230,26 +278,14 @@
|
|
|
230
278
|
outline: none;
|
|
231
279
|
}
|
|
232
280
|
|
|
233
|
-
/* 操作按钮组 */
|
|
234
281
|
.action-buttons {
|
|
235
282
|
display: flex;
|
|
236
283
|
gap: 10px;
|
|
237
284
|
margin-top: 20px;
|
|
238
285
|
flex-wrap: wrap;
|
|
286
|
+
justify-content: center;
|
|
239
287
|
}
|
|
240
288
|
|
|
241
|
-
.backup-actions {
|
|
242
|
-
margin-top: 15px;
|
|
243
|
-
display: flex;
|
|
244
|
-
gap: 12px;
|
|
245
|
-
justify-content: flex-start;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
.backup-actions .btn {
|
|
249
|
-
margin-right: 0;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/* 二维码 */
|
|
253
289
|
.qr-placeholder {
|
|
254
290
|
text-align: center;
|
|
255
291
|
margin: 15px 0;
|
|
@@ -263,11 +299,11 @@
|
|
|
263
299
|
background: var(--container-bg);
|
|
264
300
|
}
|
|
265
301
|
|
|
266
|
-
/* 其他辅助样式 */
|
|
267
302
|
.flex-buttons {
|
|
268
303
|
display: flex;
|
|
269
304
|
gap: 10px;
|
|
270
305
|
margin-top: 8px;
|
|
306
|
+
justify-content: center;
|
|
271
307
|
}
|
|
272
308
|
|
|
273
309
|
.webauthn-panel {
|
|
@@ -305,18 +341,14 @@
|
|
|
305
341
|
display: none !important;
|
|
306
342
|
}
|
|
307
343
|
|
|
308
|
-
#printBackupContainer {
|
|
309
|
-
display: none;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
344
|
@media print {
|
|
313
345
|
body> :not(#printBackupContainer) {
|
|
314
346
|
display: none !important;
|
|
315
347
|
}
|
|
316
348
|
|
|
317
349
|
#printBackupContainer {
|
|
318
|
-
position: static !important;
|
|
319
350
|
display: block !important;
|
|
351
|
+
position: static !important;
|
|
320
352
|
margin: 20px;
|
|
321
353
|
}
|
|
322
354
|
|
|
@@ -386,25 +418,21 @@
|
|
|
386
418
|
white-space: nowrap;
|
|
387
419
|
}
|
|
388
420
|
|
|
389
|
-
/* 信息行改为垂直布局,更易阅读 */
|
|
390
421
|
.info-row {
|
|
391
|
-
|
|
392
|
-
|
|
422
|
+
align-items: flex-start;
|
|
423
|
+
text-align: left;
|
|
424
|
+
padding: 14px 0;
|
|
393
425
|
}
|
|
394
426
|
|
|
395
427
|
.info-label {
|
|
396
|
-
|
|
428
|
+
font-size: 12px;
|
|
397
429
|
margin-bottom: 6px;
|
|
398
|
-
font-weight: 600;
|
|
399
|
-
opacity: 0.8;
|
|
400
430
|
}
|
|
401
431
|
|
|
402
432
|
.info-value {
|
|
403
|
-
width: 100%;
|
|
404
433
|
font-size: 15px;
|
|
405
434
|
}
|
|
406
435
|
|
|
407
|
-
/* 主要操作按钮组 垂直排列 */
|
|
408
436
|
.action-buttons {
|
|
409
437
|
flex-direction: column;
|
|
410
438
|
gap: 12px;
|
|
@@ -418,7 +446,6 @@
|
|
|
418
446
|
font-size: 15px;
|
|
419
447
|
}
|
|
420
448
|
|
|
421
|
-
/* 所有内联 flex 按钮组(修改资料/修改密码/删除账户)强制垂直 + 全宽 */
|
|
422
449
|
#editProfileCard>form>div:first-of-type,
|
|
423
450
|
#changePasswordCard>form>div:first-of-type,
|
|
424
451
|
#deleteAccountForm>div:first-of-type {
|
|
@@ -436,7 +463,6 @@
|
|
|
436
463
|
box-sizing: border-box;
|
|
437
464
|
}
|
|
438
465
|
|
|
439
|
-
/* 双因素启用面板的按钮组 (flex-buttons) */
|
|
440
466
|
.flex-buttons {
|
|
441
467
|
flex-direction: column;
|
|
442
468
|
gap: 12px;
|
|
@@ -447,18 +473,6 @@
|
|
|
447
473
|
margin-right: 0;
|
|
448
474
|
}
|
|
449
475
|
|
|
450
|
-
/* 备用码管理面板按钮组垂直排列 */
|
|
451
|
-
.backup-actions {
|
|
452
|
-
flex-direction: column;
|
|
453
|
-
gap: 12px;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
.backup-actions .btn {
|
|
457
|
-
width: 100%;
|
|
458
|
-
margin-right: 0;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/* 硬件验证设备列表 手机端改为垂直紧凑 */
|
|
462
476
|
.device-item {
|
|
463
477
|
flex-direction: column;
|
|
464
478
|
align-items: flex-start;
|
|
@@ -477,14 +491,12 @@
|
|
|
477
491
|
min-width: 80px;
|
|
478
492
|
}
|
|
479
493
|
|
|
480
|
-
/* 二维码适配移动屏幕 */
|
|
481
494
|
.qr-placeholder img {
|
|
482
495
|
max-width: 160px;
|
|
483
496
|
width: 100%;
|
|
484
497
|
height: auto;
|
|
485
498
|
}
|
|
486
499
|
|
|
487
|
-
/* 备用码列表展示优化 */
|
|
488
500
|
.backup-codes {
|
|
489
501
|
padding: 12px;
|
|
490
502
|
text-align: center;
|
|
@@ -498,7 +510,6 @@
|
|
|
498
510
|
letter-spacing: 0.5px;
|
|
499
511
|
}
|
|
500
512
|
|
|
501
|
-
/* 表单输入框提升字体 ≥16px 避免 IOS 缩放 */
|
|
502
513
|
.form-group input,
|
|
503
514
|
input[type="text"],
|
|
504
515
|
input[type="email"],
|
|
@@ -507,40 +518,33 @@
|
|
|
507
518
|
padding: 12px 12px;
|
|
508
519
|
}
|
|
509
520
|
|
|
510
|
-
/* 加宽可点击区域 */
|
|
511
521
|
.btn {
|
|
512
522
|
min-height: 44px;
|
|
513
523
|
padding: 10px 16px;
|
|
514
524
|
}
|
|
515
525
|
|
|
516
|
-
/* 双因素认证 h2 内部按钮优化宽度 */
|
|
517
526
|
#twofaCard h2 .btn {
|
|
518
527
|
white-space: normal;
|
|
519
528
|
word-break: keep-all;
|
|
520
529
|
}
|
|
521
530
|
|
|
522
|
-
/* 管理面板内添加设备按钮自适应 */
|
|
523
531
|
#manageAddDeviceBtn {
|
|
524
532
|
width: 100%;
|
|
525
533
|
justify-content: center;
|
|
526
534
|
}
|
|
527
535
|
|
|
528
|
-
/* 注销账户区块调整间距 */
|
|
529
536
|
#deleteAccountForm {
|
|
530
537
|
margin-top: 4px;
|
|
531
538
|
}
|
|
532
539
|
|
|
533
|
-
/* 硬件验证消息容器内边距 */
|
|
534
540
|
.webauthn-panel {
|
|
535
541
|
margin-top: 12px;
|
|
536
542
|
}
|
|
537
543
|
|
|
538
|
-
/* 备用码生成区域文案 */
|
|
539
544
|
#backupCodesPanel p.info {
|
|
540
545
|
font-size: 13px;
|
|
541
546
|
}
|
|
542
547
|
|
|
543
|
-
/* secret 密钥区域长文本换行 */
|
|
544
548
|
code#secretCode {
|
|
545
549
|
word-break: break-all;
|
|
546
550
|
display: inline-block;
|
|
@@ -604,7 +608,7 @@
|
|
|
604
608
|
</span>
|
|
605
609
|
</div>
|
|
606
610
|
<div class="info-row"><span class="info-label">注册时间</span>
|
|
607
|
-
<span class="info-value" id="createdAt"
|
|
611
|
+
<span class="info-value" id="createdAt">加载中...</span>
|
|
608
612
|
</div>
|
|
609
613
|
<div class="action-buttons">
|
|
610
614
|
<button type="button" class="btn btn-outline" id="editProfileBtn">编辑资料</button>
|
|
@@ -627,7 +631,7 @@
|
|
|
627
631
|
<input type="password" id="profileCurrentPassword" placeholder="请输入密码" maxlength="72"
|
|
628
632
|
autocomplete="off">
|
|
629
633
|
</div>
|
|
630
|
-
<div
|
|
634
|
+
<div class="flex-buttons">
|
|
631
635
|
<button type="button" class="btn" id="updateProfileBtn">保存修改</button>
|
|
632
636
|
<button type="button" class="btn btn-danger" id="cancelEditBtn">取消</button>
|
|
633
637
|
</div>
|
|
@@ -646,9 +650,9 @@
|
|
|
646
650
|
<input type="password" id="newPassword" placeholder="新密码(至少6位)" maxlength="72" autocomplete="off">
|
|
647
651
|
</div>
|
|
648
652
|
<div class="form-group">
|
|
649
|
-
<input type="password" id="confirmPassword" placeholder="
|
|
653
|
+
<input type="password" id="confirmPassword" placeholder="再次输入新密码" maxlength="72" autocomplete="off">
|
|
650
654
|
</div>
|
|
651
|
-
<div
|
|
655
|
+
<div class="flex-buttons">
|
|
652
656
|
<button type="button" class="btn" id="changePasswordBtn">确认提交</button>
|
|
653
657
|
<button type="button" class="btn btn-secondary" id="cancelChangePasswordBtn">取消</button>
|
|
654
658
|
</div>
|
|
@@ -656,7 +660,24 @@
|
|
|
656
660
|
</form>
|
|
657
661
|
</div>
|
|
658
662
|
|
|
659
|
-
<!--
|
|
663
|
+
<!-- 备份码卡片 -->
|
|
664
|
+
<div class="card" id="manage2faPanel" hidden>
|
|
665
|
+
<div id="backupManageMessage" class="message" hidden></div>
|
|
666
|
+
<div id="backupCodesPanel" hidden>
|
|
667
|
+
<p class="info">备份码已生成,请选择你喜欢的保存方式;<br>注意:备用码只显示一次,共10组,每组不可二次使用,请务必妥善保管;</p>
|
|
668
|
+
<div class="flex-buttons">
|
|
669
|
+
<button type="button" class="btn btn-outline" id="saveBackupFileBtn">💾 保存到文件</button>
|
|
670
|
+
<button type="button" class="btn btn-outline" id="printBackupCodesBtn">🖨️ 打印备份码</button>
|
|
671
|
+
</div>
|
|
672
|
+
<div id="backupCodesList" class="backup-codes"></div>
|
|
673
|
+
</div>
|
|
674
|
+
<div class="flex-buttons">
|
|
675
|
+
<button type="button" class="btn btn-warning" id="toggleBackupPanelBtn">🔄 生成新的备份码</button>
|
|
676
|
+
<button type="button" class="btn btn-danger" id="deleteBackupCodesBtn" hidden>🗑️ 删除所有备份码</button>
|
|
677
|
+
</div>
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
<!-- 2FA认证卡片 -->
|
|
660
681
|
<div class="card" id="twofaCard">
|
|
661
682
|
<h2>
|
|
662
683
|
<span class="title-text">
|
|
@@ -666,14 +687,13 @@
|
|
|
666
687
|
</h2>
|
|
667
688
|
|
|
668
689
|
<div id="enable2faPanel" hidden style="margin-top: 16px;">
|
|
669
|
-
<form onsubmit="return false;">
|
|
690
|
+
<form onsubmit="return false;" class="form-group">
|
|
670
691
|
<p>请使用 Google Authenticator 或类似应用扫描二维码添加认证:</p>
|
|
671
692
|
<div class="qr-placeholder">
|
|
672
693
|
<img id="qrCodeImg" alt="2FA QR Code">
|
|
673
694
|
</div>
|
|
674
|
-
<p
|
|
675
|
-
<input type="text" id="verifyToken" placeholder="
|
|
676
|
-
style="width: 100%; padding: 8px; margin: 12px 0;" autocomplete="off">
|
|
695
|
+
<p>或在应用中手动输入下方密钥:<code id="secretCode">-</code></p>
|
|
696
|
+
<input type="text" id="verifyToken" placeholder="请输入6位验证码" maxlength="6" autocomplete="off">
|
|
677
697
|
<div class="flex-buttons">
|
|
678
698
|
<button type="button" class="btn" id="confirm2faBtn">验证并启用</button>
|
|
679
699
|
<button type="button" class="btn btn-secondary" id="cancelEnable2faBtn">取消</button>
|
|
@@ -681,42 +701,25 @@
|
|
|
681
701
|
<div id="confirmMessage" class="message" hidden></div>
|
|
682
702
|
</form>
|
|
683
703
|
</div>
|
|
684
|
-
|
|
685
|
-
<div id="manage2faPanel" hidden style="margin-top: 16px;">
|
|
686
|
-
<button type="button" class="btn btn-warning" id="showBackupBtn">生成新的备份码</button>
|
|
687
|
-
<div id="backupManageMessage" class="message" hidden></div>
|
|
688
|
-
<div id="backupCodesPanel" hidden style="margin-top: 15px;">
|
|
689
|
-
<p class="info">备用码(仅显示一次,请妥善保存):</p>
|
|
690
|
-
<div id="backupCodesList" class="backup-codes"></div>
|
|
691
|
-
<div class="backup-actions">
|
|
692
|
-
<button type="button" class="btn btn-outline" id="saveBackupFileBtn">💾 保存到文件</button>
|
|
693
|
-
<button type="button" class="btn btn-outline" id="printBackupCodesBtn">🖨️ 打印备份码</button>
|
|
694
|
-
</div>
|
|
695
|
-
</div>
|
|
696
|
-
</div>
|
|
697
704
|
</div>
|
|
698
705
|
|
|
699
|
-
<!--
|
|
706
|
+
<!-- 硬件认证卡片 -->
|
|
700
707
|
<div class="card" id="webauthnCard">
|
|
701
708
|
<h2>
|
|
702
709
|
<span class="title-text">
|
|
703
|
-
|
|
710
|
+
硬件认证(指纹/人脸等) <span id="webauthnStatusIcon"></span>
|
|
704
711
|
</span>
|
|
705
|
-
<button type="button" class="btn" id="toggleWebAuthnBtn"
|
|
712
|
+
<button type="button" class="btn" id="toggleWebAuthnBtn">启用硬件认证</button>
|
|
706
713
|
</h2>
|
|
707
714
|
|
|
708
715
|
<div id="webauthnMessage" class="message" hidden></div>
|
|
709
716
|
<div id="manageWebAuthnPanel" class="webauthn-panel" hidden>
|
|
710
|
-
<p id="noDeviceMsg" style="color: #666;">暂无硬件设备,点击上方「添加新设备」按钮添加</p>
|
|
711
717
|
<ul id="webauthnDeviceList" class="device-list"></ul>
|
|
712
|
-
<li id="deviceItemExample" class="device-item"
|
|
718
|
+
<li id="deviceItemExample" class="device-item" hidden>
|
|
713
719
|
<span class="device-name"></span>
|
|
714
720
|
<button type="button" class="btn btn-danger btn-small">删除</button>
|
|
715
721
|
</li>
|
|
716
|
-
<
|
|
717
|
-
<button type="button" class="btn btn-outline" id="manageAddDeviceBtn">➕ 添加新设备</button>
|
|
718
|
-
</div>
|
|
719
|
-
<div id="webauthnManageMessage" class="message" hidden></div>
|
|
722
|
+
<button type="button" class="btn btn-outline" id="manageAddDeviceBtn">➕ 添加新设备</button>
|
|
720
723
|
</div>
|
|
721
724
|
</div>
|
|
722
725
|
|
|
@@ -731,10 +734,10 @@
|
|
|
731
734
|
<div id="deleteAccountForm" hidden style="margin-top:15px;">
|
|
732
735
|
<form onsubmit="return false;">
|
|
733
736
|
<div class="form-group">
|
|
734
|
-
<input type="password" id="deleteAccountPassword" placeholder="
|
|
737
|
+
<input type="password" id="deleteAccountPassword" placeholder="请输入登录密码" maxlength="72"
|
|
735
738
|
autocomplete="off">
|
|
736
739
|
</div>
|
|
737
|
-
<div
|
|
740
|
+
<div class="flex-buttons">
|
|
738
741
|
<button type="button" class="btn btn-danger" id="deleteAccountBtn">永久注销账户</button>
|
|
739
742
|
<button type="button" class="btn btn-secondary" id="cancelDeleteBtn">取消</button>
|
|
740
743
|
</div>
|
|
@@ -743,7 +746,7 @@
|
|
|
743
746
|
</div>
|
|
744
747
|
</div>
|
|
745
748
|
</div>
|
|
746
|
-
<div id="printBackupContainer"></div>
|
|
749
|
+
<div id="printBackupContainer" hidden></div>
|
|
747
750
|
|
|
748
751
|
<!-- 公共逻辑 -->
|
|
749
752
|
<script src="/static/themeModule.js" defer></script><!-- 引入主题自适应模块 -->
|
|
@@ -752,46 +755,44 @@
|
|
|
752
755
|
<script src="/static/topImg.js" defer></script> <!-- 引入返回顶部图标模块 -->
|
|
753
756
|
<script src="/static/utils/browser.js"></script> <!-- 引入@flun前端硬件验证模块 -->
|
|
754
757
|
<script>
|
|
755
|
-
//
|
|
758
|
+
// 全局变量
|
|
756
759
|
let currentUser = null, targetEmail = null, emailCheckInterval = null, isEmailPollingActive = false,
|
|
757
760
|
latestBackupCodes = [], currentWebAuthnCredentials = [];
|
|
758
761
|
|
|
759
|
-
//
|
|
760
|
-
const [
|
|
761
|
-
usernameEl, emailEl, createdAtEl,
|
|
762
|
+
// DOM 元素常量
|
|
763
|
+
const [usernameEl, emailEl, emailVerifiedBadge, createdAtEl, editProfileBtn, showChangePasswordBtn, logoutBtn,
|
|
762
764
|
editProfileCard, newUsernameInput, newEmailInput, profileCurrentPasswordInput,
|
|
763
|
-
|
|
765
|
+
updateProfileBtn, cancelEditBtn, updateProfileMessage,
|
|
764
766
|
changePasswordCard, currentPasswordInput, newPasswordInput, confirmPasswordInput,
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
backupManageMessage, emailVerifiedBadge
|
|
767
|
+
changePasswordBtn, cancelChangePasswordBtn, changePasswordMessage,
|
|
768
|
+
manage2faPanel, toggleBackupPanelBtn, backupManageMessage, backupCodesPanel,
|
|
769
|
+
backupCodesList, saveBackupFileBtn, printBackupCodesBtn, deleteBackupCodesBtn,
|
|
770
|
+
twofaCheckmark, toggle2faBtn, enable2faPanel,
|
|
771
|
+
qrCodeImg, secretCode, verifyTokenInput, confirm2faBtn, cancelEnable2faBtn, confirmMessage,
|
|
772
|
+
webauthnStatusIcon, toggleWebAuthnBtn, webauthnMessage, manageWebAuthnPanel,
|
|
773
|
+
webauthnDeviceList, deviceItemExample, manageAddDeviceBtn,
|
|
774
|
+
showDeleteAccountBtn, deleteAccountForm, deleteAccountPassword,
|
|
775
|
+
deleteAccountBtn, cancelDeleteBtn, deleteAccountMessage, printBackupContainer
|
|
775
776
|
] = [
|
|
776
|
-
'username', 'email', 'createdAt',
|
|
777
|
+
'username', 'email', 'emailVerifiedBadge', 'createdAt', 'editProfileBtn', 'showChangePasswordBtn', 'logoutBtn',
|
|
777
778
|
'editProfileCard', 'newUsername', 'newEmail', 'profileCurrentPassword',
|
|
778
|
-
'
|
|
779
|
+
'updateProfileBtn', 'cancelEditBtn', 'updateProfileMessage',
|
|
779
780
|
'changePasswordCard', 'currentPassword', 'newPassword', 'confirmPassword',
|
|
780
|
-
'
|
|
781
|
-
'
|
|
782
|
-
'
|
|
783
|
-
'
|
|
784
|
-
'
|
|
785
|
-
'
|
|
786
|
-
'
|
|
787
|
-
'
|
|
788
|
-
'
|
|
789
|
-
'backupManageMessage', 'emailVerifiedBadge'
|
|
781
|
+
'changePasswordBtn', 'cancelChangePasswordBtn', 'changePasswordMessage',
|
|
782
|
+
'manage2faPanel', 'toggleBackupPanelBtn', 'backupManageMessage', 'backupCodesPanel',
|
|
783
|
+
'backupCodesList', 'saveBackupFileBtn', 'printBackupCodesBtn', 'deleteBackupCodesBtn',
|
|
784
|
+
'twofaCheckmark', 'toggle2faBtn', 'enable2faPanel',
|
|
785
|
+
'qrCodeImg', 'secretCode', 'verifyToken', 'confirm2faBtn', 'cancelEnable2faBtn', 'confirmMessage',
|
|
786
|
+
'webauthnStatusIcon', 'toggleWebAuthnBtn', 'webauthnMessage', 'manageWebAuthnPanel',
|
|
787
|
+
'webauthnDeviceList', 'deviceItemExample', 'manageAddDeviceBtn',
|
|
788
|
+
'showDeleteAccountBtn', 'deleteAccountForm', 'deleteAccountPassword',
|
|
789
|
+
'deleteAccountBtn', 'cancelDeleteBtn', 'deleteAccountMessage', 'printBackupContainer'
|
|
790
790
|
].map(id => document.getElementById(id)),
|
|
791
|
-
//
|
|
791
|
+
// 基础工具
|
|
792
792
|
showMessage = (element, type, text) => {
|
|
793
793
|
element.className = `message ${type}`, element.textContent = text, element.hidden = false;
|
|
794
|
-
},
|
|
794
|
+
},
|
|
795
|
+
hideMessage = element => (element.hidden = true, element.textContent = '', element.className = 'message'),
|
|
795
796
|
requestApi = async (url, options, msgElement = null, btnElement = null, suppressSuccessMsg = false) => {
|
|
796
797
|
if (btnElement && typeof btnElement === 'boolean') suppressSuccessMsg = btnElement, btnElement = null;
|
|
797
798
|
if (btnElement) btnElement.disabled = true;
|
|
@@ -829,13 +830,124 @@
|
|
|
829
830
|
if (isEmailPollingActive) stopEmailPolling(), timeoutCallback?.();
|
|
830
831
|
}, 900000);
|
|
831
832
|
},
|
|
833
|
+
|
|
834
|
+
// 全局 UI
|
|
835
|
+
refreshMFAUI = () => {
|
|
836
|
+
const is2faEnabled = currentUser?.twoFactorEnabled, isWebAuthnEnabled = currentUser?.webauthnEnabled;
|
|
837
|
+
twofaCheckmark.innerHTML = is2faEnabled ? '✅' : '';
|
|
838
|
+
toggle2faBtn.textContent = is2faEnabled ? '关闭2FA' : '启用2FA';
|
|
839
|
+
toggle2faBtn.classList.toggle('btn-danger', is2faEnabled);
|
|
840
|
+
webauthnStatusIcon.innerHTML = isWebAuthnEnabled ? '✅' : '';
|
|
841
|
+
toggleWebAuthnBtn.textContent = isWebAuthnEnabled ? '关闭硬件验证' : '启用硬件验证';
|
|
842
|
+
toggleWebAuthnBtn.classList.toggle('btn-danger', isWebAuthnEnabled);
|
|
843
|
+
const hasAnyMFA = is2faEnabled || isWebAuthnEnabled;
|
|
844
|
+
if (hasAnyMFA) manage2faPanel.hidden = false;
|
|
845
|
+
else {
|
|
846
|
+
manage2faPanel.hidden = true;
|
|
847
|
+
if (!backupCodesPanel.hidden && latestBackupCodes.length === 0) hideBackupPanel();
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
updateDeleteBackupButton = () => {
|
|
851
|
+
if (currentUser?.hasBackupCodes) deleteBackupCodesBtn.hidden = false;
|
|
852
|
+
else deleteBackupCodesBtn.hidden = true;
|
|
853
|
+
},
|
|
854
|
+
syncBackupButtonState = () => {
|
|
855
|
+
const isPanelVisible = !backupCodesPanel.hidden;
|
|
856
|
+
toggleBackupPanelBtn.disabled = false;
|
|
857
|
+
if (isPanelVisible) {
|
|
858
|
+
toggleBackupPanelBtn.textContent = '📴 关闭备份码显示', toggleBackupPanelBtn.classList.remove('btn-warning');
|
|
859
|
+
toggleBackupPanelBtn.classList.add('btn-secondary');
|
|
860
|
+
} else {
|
|
861
|
+
toggleBackupPanelBtn.textContent = '🔄 生成新的备份码', toggleBackupPanelBtn.classList.remove('btn-secondary');
|
|
862
|
+
toggleBackupPanelBtn.classList.add('btn-warning');
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
hideBackupPanel = () => {
|
|
866
|
+
backupCodesPanel.hidden = true, backupCodesList.innerHTML = '', latestBackupCodes = [], syncBackupButtonState();
|
|
867
|
+
const infoPara = document.querySelector('#backupCodesPanel .info');
|
|
868
|
+
if (infoPara) infoPara.classList.remove('info-border-flow');
|
|
869
|
+
},
|
|
870
|
+
|
|
871
|
+
// 页面入口
|
|
872
|
+
loadUser = async () => {
|
|
873
|
+
try {
|
|
874
|
+
const { ok, data } = await requestApi('/api/user', { method: 'GET' }, null, null, true);
|
|
875
|
+
if (!ok || [401, 404].includes(data?.status ?? data?.code)) return window.location.href = '/login';
|
|
876
|
+
currentUser = data;
|
|
877
|
+
const { username, email, emailVerified, createdAt } = currentUser;
|
|
878
|
+
usernameEl.textContent = username;
|
|
879
|
+
emailEl.textContent = email ?? '未设置';
|
|
880
|
+
emailVerifiedBadge.textContent = emailVerified ? '✅' : '';
|
|
881
|
+
createdAtEl.textContent = new Date(createdAt).toLocaleString();
|
|
882
|
+
await Promise.all([refreshMFAUI(), loadWebAuthnData()]), updateDeleteBackupButton();
|
|
883
|
+
} catch (err) {
|
|
884
|
+
alert('加载用户信息失败,请刷新页面重试');
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
|
|
888
|
+
// 个人资料模块
|
|
832
889
|
resetEditProfileForm = () => {
|
|
833
890
|
profileCurrentPasswordInput.value = '', newUsernameInput.value = currentUser?.username;
|
|
834
891
|
newEmailInput.value = currentUser?.email;
|
|
835
892
|
},
|
|
893
|
+
closeEditProfileCard = () => {
|
|
894
|
+
if (!editProfileCard.hidden) {
|
|
895
|
+
if (isEmailPollingActive) stopEmailPolling();
|
|
896
|
+
editProfileCard.hidden = true, updateProfileBtn.disabled = false, editProfileBtn.disabled = false;
|
|
897
|
+
updateProfileBtn.textContent = '保存修改', hideMessage(updateProfileMessage), resetEditProfileForm();
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
openEditProfileCard = () => {
|
|
901
|
+
closeChangePasswordCard(), editProfileCard.hidden = false;
|
|
902
|
+
if (currentUser) newUsernameInput.value = currentUser.username, newEmailInput.value = currentUser.email;
|
|
903
|
+
if (isEmailPollingActive) stopEmailPolling();
|
|
904
|
+
profileCurrentPasswordInput.value = '', editProfileBtn.disabled = true, newUsernameInput.focus();
|
|
905
|
+
updateProfileBtn.disabled = false, updateProfileBtn.textContent = '保存修改', hideMessage(updateProfileMessage);
|
|
906
|
+
},
|
|
907
|
+
|
|
908
|
+
// 密码修改模块
|
|
836
909
|
resetPasswordForm = () => {
|
|
837
910
|
currentPasswordInput.value = '', newPasswordInput.value = '', confirmPasswordInput.value = '';
|
|
838
911
|
},
|
|
912
|
+
closeChangePasswordCard = () => {
|
|
913
|
+
if (!changePasswordCard.hidden) {
|
|
914
|
+
changePasswordCard.hidden = true, resetPasswordForm(), showChangePasswordBtn.disabled = false;
|
|
915
|
+
hideMessage(changePasswordMessage);
|
|
916
|
+
}
|
|
917
|
+
},
|
|
918
|
+
openChangePasswordCard = () => {
|
|
919
|
+
closeEditProfileCard(), changePasswordCard.hidden = false, showChangePasswordBtn.disabled = true;
|
|
920
|
+
resetPasswordForm(), setTimeout(() => currentPasswordInput.focus(), 50), hideMessage(changePasswordMessage);
|
|
921
|
+
},
|
|
922
|
+
|
|
923
|
+
// 备份码模块
|
|
924
|
+
renderBackupCodes = codes => {
|
|
925
|
+
if (codes?.length === 0) {
|
|
926
|
+
backupCodesPanel.hidden = true, latestBackupCodes = [];
|
|
927
|
+
if (currentUser) currentUser.hasBackupCodes = false;
|
|
928
|
+
updateDeleteBackupButton(), syncBackupButtonState();
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
backupCodesList.innerHTML = '', latestBackupCodes = [...codes];
|
|
932
|
+
if (currentUser) currentUser.hasBackupCodes = true;
|
|
933
|
+
updateDeleteBackupButton();
|
|
934
|
+
codes.forEach(code => {
|
|
935
|
+
const span = document.createElement('span');
|
|
936
|
+
span.className = 'code-item', span.textContent = code, backupCodesList.append(span);
|
|
937
|
+
});
|
|
938
|
+
backupCodesPanel.hidden = false, syncBackupButtonState();
|
|
939
|
+
const infoPara = document.querySelector('#backupCodesPanel .info');
|
|
940
|
+
if (infoPara) infoPara.classList.add('info-border-flow');
|
|
941
|
+
scrollToBackupPanelAndFocus();
|
|
942
|
+
},
|
|
943
|
+
scrollToBackupPanelAndFocus = () => {
|
|
944
|
+
if (manage2faPanel.hidden || backupCodesPanel.hidden || latestBackupCodes.length === 0) return;
|
|
945
|
+
requestAnimationFrame(() => {
|
|
946
|
+
manage2faPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
947
|
+
if (!saveBackupFileBtn.disabled) saveBackupFileBtn.focus();
|
|
948
|
+
else if (!printBackupCodesBtn.disabled) printBackupCodesBtn.focus();
|
|
949
|
+
});
|
|
950
|
+
},
|
|
839
951
|
saveBackupCodesToFile = (backupCodes, prefix = '我的账户备份码') => {
|
|
840
952
|
if (backupCodes?.length === 0) return alert('没有可保存的备份码');
|
|
841
953
|
const now = new Date(), time = now.toLocaleString(), fileTime = time.replace(/[\/:]/g, '-').replace(/ /g, '_');
|
|
@@ -853,56 +965,161 @@
|
|
|
853
965
|
printBackupCodes = (backupCodes, title = '双因素认证备用码') => {
|
|
854
966
|
if (!backupCodes || backupCodes.length === 0) return alert('没有可打印的备份码');
|
|
855
967
|
printBackupContainer.innerHTML = `
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
</div>
|
|
862
|
-
<div class="warning">⚠️ 每个备用码仅能使用一次,请妥善保管;</div>
|
|
863
|
-
<div class="footer">共计 ${backupCodes.length} 个备用码</div>
|
|
968
|
+
<div class="print-backup">
|
|
969
|
+
<h1>🔐 ${title}</h1>
|
|
970
|
+
<p class="timestamp">生成时间:${new Date().toLocaleString()}</p>
|
|
971
|
+
<div class="codes-grid">
|
|
972
|
+
${backupCodes.map(code => `<div class="code-card">${code}</div>`).join('')}
|
|
864
973
|
</div>
|
|
865
|
-
|
|
974
|
+
<div class="warning">⚠️ 每个备用码仅能使用一次,请妥善保管;</div>
|
|
975
|
+
<div class="footer">共计 ${backupCodes.length} 个备用码</div>
|
|
976
|
+
</div>`;
|
|
866
977
|
printBackupCodesBtn.disabled = true, window.print(), setTimeout(() => printBackupContainer.innerHTML = '', 500);
|
|
867
978
|
},
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
979
|
+
deleteBackupCodes = async () => {
|
|
980
|
+
if (!confirm('⚠️ 确定要删除所有备份码吗?删除后将无法使用备份码恢复登录')) return;
|
|
981
|
+
|
|
982
|
+
deleteBackupCodesBtn.disabled = true;
|
|
983
|
+
const result = await requestApi('/api/delete-backup-codes', { method: 'POST' }, backupManageMessage,
|
|
984
|
+
deleteBackupCodesBtn);
|
|
985
|
+
if (result.ok) {
|
|
986
|
+
if (currentUser) currentUser.hasBackupCodes = false;
|
|
987
|
+
updateDeleteBackupButton();
|
|
988
|
+
|
|
989
|
+
if (!backupCodesPanel.hidden) hideBackupPanel();
|
|
990
|
+
refreshMFAUI(), showMessage(backupManageMessage, 'success', '所有备份码已删除');
|
|
991
|
+
setTimeout(() => hideMessage(backupManageMessage), 3000);
|
|
876
992
|
}
|
|
993
|
+
deleteBackupCodesBtn.disabled = false;
|
|
877
994
|
},
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
995
|
+
|
|
996
|
+
// 2FA 认证模块
|
|
997
|
+
showEnable2faPanel = async () => {
|
|
998
|
+
verifyTokenInput.value = '', hideMessage(confirmMessage), qrCodeImg.src = '';
|
|
999
|
+
secretCode.textContent = '加载中...', enable2faPanel.hidden = false, confirm2faBtn.disabled = false;
|
|
1000
|
+
const { ok, data } = await requestApi('/api/enable-2fa', { method: 'POST' }, confirmMessage, toggle2faBtn),
|
|
1001
|
+
{ qrCode, secret } = data;
|
|
1002
|
+
if (ok && qrCode && secret) qrCodeImg.src = qrCode, secretCode.textContent = secret, verifyTokenInput.focus();
|
|
1003
|
+
else enable2faPanel.hidden = true;
|
|
1004
|
+
},
|
|
1005
|
+
hideEnable2faPanel = () => {
|
|
1006
|
+
toggle2faBtn.disabled = false, enable2faPanel.hidden = true;
|
|
1007
|
+
verifyTokenInput.value = '', hideMessage(confirmMessage);
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
// WebAuthn 硬件认证模块
|
|
1011
|
+
loadWebAuthnData = async () => {
|
|
1012
|
+
try {
|
|
1013
|
+
const { ok, data } = await requestApi('/api/webauthn/credentials', { method: 'GET' }, null, null, true);
|
|
1014
|
+
if (ok) currentWebAuthnCredentials = data.credentials || [];
|
|
1015
|
+
else throw new Error(data.message);
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
currentWebAuthnCredentials = [], showMessage(webauthnMessage, 'error', '加载硬件凭证失败,请刷新页面重试');
|
|
1018
|
+
} finally { refreshWebAuthnUI() }
|
|
1019
|
+
},
|
|
1020
|
+
addWebAuthnDevice = async () => {
|
|
1021
|
+
try {
|
|
1022
|
+
const beginResult = await requestApi('/api/webauthn/register/begin', { method: 'POST' }, webauthnMessage, null,
|
|
1023
|
+
true);
|
|
1024
|
+
if (!beginResult.ok) return false;
|
|
1025
|
+
|
|
1026
|
+
showMessage(webauthnMessage, null, "认证硬件添加中...");
|
|
1027
|
+
const options = beginResult.data,
|
|
1028
|
+
attResp = await flunWebAuthnBrowser.startRegistration({ optionsJSON: options }),
|
|
1029
|
+
completeResult = await requestApi('/api/webauthn/register/complete', {
|
|
1030
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1031
|
+
body: JSON.stringify({ attestationResponse: attResp })
|
|
1032
|
+
}, webauthnMessage);
|
|
1033
|
+
|
|
1034
|
+
if (!completeResult.ok) return false;
|
|
1035
|
+
if (completeResult.data.backupCodes) renderBackupCodes(completeResult.data.backupCodes);
|
|
1036
|
+
setTimeout(() => hideMessage(webauthnMessage), 3600), await loadUser(), await loadWebAuthnData();
|
|
1037
|
+
return true;
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
if (err.name === 'AbortError' || err.name === 'NotAllowedError') {
|
|
1040
|
+
hideMessage(webauthnMessage);
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
showMessage(webauthnMessage, 'error', '添加失败:' + (err.message));
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
},
|
|
1047
|
+
renderCredentialsList = () => {
|
|
1048
|
+
if (currentWebAuthnCredentials.length === 0) return;
|
|
1049
|
+
|
|
1050
|
+
webauthnDeviceList.innerHTML = '';
|
|
1051
|
+
currentWebAuthnCredentials.forEach((cred, idx) => {
|
|
1052
|
+
const li = deviceItemExample.cloneNode(true), nameSpan = li.querySelector('.device-name'),
|
|
1053
|
+
delBtn = li.querySelector('button'), time = new Date(cred.createdAt).toLocaleString();
|
|
1054
|
+
|
|
1055
|
+
nameSpan.textContent = `🔑 ${cred.deviceName} 硬件${idx + 1}(添加于${time})`;
|
|
1056
|
+
li.hidden = false, webauthnDeviceList.append(li);
|
|
1057
|
+
delBtn.addEventListener('click', async e => {
|
|
1058
|
+
e.preventDefault();
|
|
1059
|
+
if (!confirm('确定删除此设备吗?删除后将无法用于登录验证;')) return;
|
|
1060
|
+
const { ok } = await requestApi('/api/webauthn/credentials/delete',
|
|
1061
|
+
{
|
|
1062
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1063
|
+
body: JSON.stringify({ credentialId: cred.id })
|
|
1064
|
+
}, webauthnMessage, delBtn);
|
|
1065
|
+
if (ok) await loadUser(), await loadWebAuthnData();
|
|
1066
|
+
});
|
|
884
1067
|
});
|
|
885
|
-
backupCodesPanel.hidden = false, syncBackupButtonState();
|
|
886
1068
|
},
|
|
887
|
-
|
|
888
|
-
|
|
1069
|
+
refreshWebAuthnUI = () => {
|
|
1070
|
+
hideMessage(webauthnMessage);
|
|
1071
|
+
const isEnabled = currentUser?.webauthnEnabled === true;
|
|
1072
|
+
webauthnStatusIcon.innerHTML = isEnabled ? '✅' : '';
|
|
1073
|
+
toggleWebAuthnBtn.textContent = isEnabled ? '关闭硬件验证' : '启用硬件验证';
|
|
1074
|
+
toggleWebAuthnBtn.classList.toggle('btn-danger', isEnabled);
|
|
1075
|
+
toggleWebAuthnBtn.disabled = false, manageAddDeviceBtn.disabled = false;
|
|
1076
|
+
if (isEnabled) webauthnMessage.hidden = true, manageWebAuthnPanel.hidden = false, renderCredentialsList();
|
|
1077
|
+
else webauthnMessage.hidden = false, manageWebAuthnPanel.hidden = true, webauthnDeviceList.innerHTML = '';
|
|
889
1078
|
},
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1079
|
+
toggleWebAuthn = async () => {
|
|
1080
|
+
await loadWebAuthnData();
|
|
1081
|
+
const targetState = !currentUser?.webauthnEnabled;
|
|
1082
|
+
if (!targetState) {
|
|
1083
|
+
if (!confirm('确定关闭硬件验证吗?关闭后登录时不再需要硬件验证;')) return;
|
|
1084
|
+
const oldUser = { ...currentUser };
|
|
1085
|
+
if (currentUser) currentUser.webauthnEnabled = false;
|
|
1086
|
+
refreshWebAuthnUI();
|
|
1087
|
+
const { ok, data } = await requestApi('/api/webauthn/toggle', { method: 'POST' }, null, toggleWebAuthnBtn);
|
|
1088
|
+
if (ok) await loadUser();
|
|
1089
|
+
else {
|
|
1090
|
+
if (currentUser) currentUser.webauthnEnabled = oldUser?.webauthnEnabled;
|
|
1091
|
+
refreshWebAuthnUI(), alert(data.message);
|
|
1092
|
+
}
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (currentWebAuthnCredentials.length === 0) {
|
|
1096
|
+
toggleWebAuthnBtn.disabled = true;
|
|
1097
|
+
try {
|
|
1098
|
+
await addWebAuthnDevice();
|
|
1099
|
+
} finally { toggleWebAuthnBtn.disabled = false }
|
|
1100
|
+
} else {
|
|
1101
|
+
const { ok, data } = await requestApi('/api/webauthn/toggle', { method: 'POST' }, null, toggleWebAuthnBtn);
|
|
1102
|
+
if (ok) {
|
|
1103
|
+
await loadUser(), await loadWebAuthnData();
|
|
1104
|
+
if (data.backupCodes?.length) renderBackupCodes(data.backupCodes);
|
|
1105
|
+
}
|
|
896
1106
|
}
|
|
897
1107
|
},
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1108
|
+
|
|
1109
|
+
// 注销账户模块
|
|
1110
|
+
toggleDeleteForm = show => {
|
|
1111
|
+
hideMessage(deleteAccountMessage);
|
|
1112
|
+
if (show) {
|
|
1113
|
+
showDeleteAccountBtn.hidden = true, deleteAccountForm.hidden = false;
|
|
1114
|
+
setTimeout(() => deleteAccountPassword.focus(), 50);
|
|
1115
|
+
} else {
|
|
1116
|
+
showDeleteAccountBtn.hidden = false, deleteAccountForm.hidden = true, deleteAccountPassword.value = '';
|
|
1117
|
+
showDeleteAccountBtn.focus();
|
|
1118
|
+
}
|
|
904
1119
|
};
|
|
905
1120
|
|
|
1121
|
+
// ==================== 事件绑定 ====================
|
|
1122
|
+
// 1. 个人资料
|
|
906
1123
|
editProfileBtn.addEventListener('click', openEditProfileCard);
|
|
907
1124
|
cancelEditBtn.addEventListener('click', () => { closeEditProfileCard(), editProfileBtn.focus() });
|
|
908
1125
|
updateProfileBtn.addEventListener('click', async () => {
|
|
@@ -917,8 +1134,7 @@
|
|
|
917
1134
|
let skipFinallyRestore = false;
|
|
918
1135
|
try {
|
|
919
1136
|
const { ok } = await requestApi('/api/update-profile', {
|
|
920
|
-
method: 'POST',
|
|
921
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1137
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
922
1138
|
body: JSON.stringify({ username, email, currentPassword: pwd })
|
|
923
1139
|
}, updateProfileMessage, updateProfileBtn);
|
|
924
1140
|
if (ok) {
|
|
@@ -938,18 +1154,7 @@
|
|
|
938
1154
|
}
|
|
939
1155
|
});
|
|
940
1156
|
|
|
941
|
-
//
|
|
942
|
-
const closeChangePasswordCard = () => {
|
|
943
|
-
if (!changePasswordCard.hidden) {
|
|
944
|
-
changePasswordCard.hidden = true, resetPasswordForm(), showChangePasswordBtn.disabled = false;
|
|
945
|
-
hideMessage(changePasswordMessage);
|
|
946
|
-
}
|
|
947
|
-
},
|
|
948
|
-
openChangePasswordCard = () => {
|
|
949
|
-
closeEditProfileCard(), changePasswordCard.hidden = false, showChangePasswordBtn.disabled = true;
|
|
950
|
-
resetPasswordForm(), setTimeout(() => currentPasswordInput.focus(), 50), hideMessage(changePasswordMessage);
|
|
951
|
-
};
|
|
952
|
-
|
|
1157
|
+
// 2. 修改密码
|
|
953
1158
|
showChangePasswordBtn.addEventListener('click', openChangePasswordCard);
|
|
954
1159
|
cancelChangePasswordBtn.addEventListener('click', () => {
|
|
955
1160
|
closeChangePasswordCard(), showChangePasswordBtn.focus();
|
|
@@ -959,37 +1164,45 @@
|
|
|
959
1164
|
.map(input => input.value.trim());
|
|
960
1165
|
|
|
961
1166
|
if (!current || !newPwd || !confirm) return showMessage(changePasswordMessage, 'error', '请填写所有字段 ');
|
|
962
|
-
if (newPwd.length < 6) return showMessage(changePasswordMessage, 'error', '新密码至少6位');
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
}, changePasswordMessage, changePasswordBtn);
|
|
1167
|
+
if (newPwd.length < 6) return showMessage(changePasswordMessage, 'error', '新密码至少6位'); if (newPwd !== confirm)
|
|
1168
|
+
return showMessage(changePasswordMessage, 'error', '两次密码不一致'); const { ok } = await
|
|
1169
|
+
requestApi('/api/change-password', {
|
|
1170
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body:
|
|
1171
|
+
JSON.stringify({ currentPassword: current, newPassword: newPwd })
|
|
1172
|
+
}, changePasswordMessage, changePasswordBtn);
|
|
969
1173
|
if (ok) setTimeout(() => (closeChangePasswordCard(), window.location.href = '/login'), 1000);
|
|
970
1174
|
});
|
|
971
1175
|
|
|
972
|
-
//
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
const { ok, data } = await requestApi('/api/enable-2fa', { method: 'POST' }, confirmMessage, toggle2faBtn),
|
|
977
|
-
{ qrCode, secret } = data;
|
|
978
|
-
if (ok && qrCode && secret) qrCodeImg.src = qrCode, secretCode.textContent = secret, verifyTokenInput.focus();
|
|
979
|
-
else enable2faPanel.hidden = true;
|
|
980
|
-
},
|
|
981
|
-
hideEnable2faPanel = () => {
|
|
982
|
-
toggle2faBtn.disabled = false, enable2faPanel.hidden = true, verifyTokenInput.value = '', hideMessage(confirmMessage);
|
|
983
|
-
},
|
|
984
|
-
refreshTwofaUI = () => {
|
|
985
|
-
toggle2faBtn.disabled = false;
|
|
986
|
-
const isEnabled = currentUser?.twoFactorEnabled;
|
|
987
|
-
twofaCheckmark.innerHTML = isEnabled ? '✅' : '', twofaCheckmark.title = isEnabled ? '已启用双因素认证' : '';
|
|
988
|
-
toggle2faBtn.textContent = isEnabled ? '关闭2FA' : '启用2FA', toggle2faBtn.classList.toggle('btn-danger', isEnabled);
|
|
989
|
-
if (isEnabled) manage2faPanel.hidden = false, hideBackupPanel();
|
|
990
|
-
else manage2faPanel.hidden = true, hideMessage(confirmMessage);
|
|
991
|
-
};
|
|
1176
|
+
// 3. 备份码管理
|
|
1177
|
+
toggleBackupPanelBtn.addEventListener('click', async () => {
|
|
1178
|
+
saveBackupFileBtn.disabled = false, printBackupCodesBtn.disabled = false, toggleBackupPanelBtn.disabled = true;
|
|
1179
|
+
toggleBackupPanelBtn.textContent = '加载中...';
|
|
992
1180
|
|
|
1181
|
+
try {
|
|
1182
|
+
const isPanelVisible = !backupCodesPanel.hidden;
|
|
1183
|
+
if (isPanelVisible) return hideBackupPanel();
|
|
1184
|
+
if (!confirm('重新生成备用码,将会使旧备用码失效,是否继续?')) return toggleBackupPanelBtn.disabled = false;
|
|
1185
|
+
|
|
1186
|
+
const result = await requestApi('/api/regenerate-backup-codes', { method: 'POST' }, backupManageMessage,
|
|
1187
|
+
toggleBackupPanelBtn);
|
|
1188
|
+
if (result.ok && result.data.backupCodes) renderBackupCodes(result.data.backupCodes),
|
|
1189
|
+
hideMessage(backupManageMessage);
|
|
1190
|
+
else backupCodesPanel.hidden = true, backupCodesList.innerHTML = '', syncBackupButtonState();
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
backupCodesPanel.hidden = true, backupCodesList.innerHTML = '', syncBackupButtonState();
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
saveBackupFileBtn.addEventListener('click', () => {
|
|
1196
|
+
if (latestBackupCodes.length) saveBackupCodesToFile(latestBackupCodes, `${currentUser?.username}*备用码`);
|
|
1197
|
+
else alert('没有备份码可保存');
|
|
1198
|
+
});
|
|
1199
|
+
printBackupCodesBtn.addEventListener('click', () => {
|
|
1200
|
+
if (latestBackupCodes.length) printBackupCodes(latestBackupCodes, `${currentUser?.username}*备用码`);
|
|
1201
|
+
else alert('没有备份码可打印');
|
|
1202
|
+
});
|
|
1203
|
+
deleteBackupCodesBtn.addEventListener('click', deleteBackupCodes);
|
|
1204
|
+
|
|
1205
|
+
// 4. 2FA认证
|
|
993
1206
|
toggle2faBtn.addEventListener('click', async () => {
|
|
994
1207
|
const isEnabled = currentUser?.twoFactorEnabled;
|
|
995
1208
|
if (!isEnabled) {
|
|
@@ -997,9 +1210,14 @@
|
|
|
997
1210
|
else hideEnable2faPanel();
|
|
998
1211
|
} else {
|
|
999
1212
|
if (!confirm('确定关闭双因素认证吗?关闭后账户安全性将降低;')) return;
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1213
|
+
toggle2faBtn.disabled = true;
|
|
1214
|
+
try {
|
|
1215
|
+
const { ok, data } = await requestApi('/api/disable-2fa', { method: 'POST' }, null, null, true);
|
|
1216
|
+
if (ok) await loadUser();
|
|
1217
|
+
else alert(data.message);
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
alert(`关闭失败:${err.message}`);
|
|
1220
|
+
} finally { toggle2faBtn.disabled = false }
|
|
1003
1221
|
}
|
|
1004
1222
|
});
|
|
1005
1223
|
cancelEnable2faBtn.addEventListener('click', hideEnable2faPanel);
|
|
@@ -1007,149 +1225,25 @@
|
|
|
1007
1225
|
const token = verifyTokenInput.value.trim();
|
|
1008
1226
|
if (!token) return showMessage(confirmMessage, 'error', '请输入6位验证码');
|
|
1009
1227
|
const { ok, data } = await requestApi('/api/confirm-2fa', {
|
|
1010
|
-
method: 'POST',
|
|
1011
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1228
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1012
1229
|
body: JSON.stringify({ token })
|
|
1013
1230
|
}, confirmMessage, confirm2faBtn);
|
|
1231
|
+
|
|
1014
1232
|
if (ok && data.backupCodes) {
|
|
1015
1233
|
await loadUser(), renderBackupCodes(data.backupCodes), hideEnable2faPanel();
|
|
1016
1234
|
setTimeout(() => hideMessage(confirmMessage), 3000);
|
|
1017
|
-
} else
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
saveBackupFileBtn.disabled = false, printBackupCodesBtn.disabled = false;
|
|
1021
|
-
showBackupBtn.disabled = true, showBackupBtn.textContent = '加载中...', hideMessage(backupManageMessage);
|
|
1022
|
-
try {
|
|
1023
|
-
const isPanelVisible = !backupCodesPanel.hidden;
|
|
1024
|
-
if (isPanelVisible) return hideBackupPanel(), showBackupBtn.disabled = false;
|
|
1025
|
-
if (!confirm('重新生成备用码,将会使旧备用码失效,是否继续?')) return showBackupBtn.disabled = false;
|
|
1026
|
-
backupCodesPanel.hidden = false;
|
|
1027
|
-
const result = await requestApi('/api/regenerate-backup-codes', { method: 'POST' }, backupManageMessage, showBackupBtn);
|
|
1028
|
-
if (result.ok && result.data.backupCodes) renderBackupCodes(result.data.backupCodes);
|
|
1029
|
-
else backupCodesPanel.hidden = true, backupCodesList.innerHTML = '', syncBackupButtonState();
|
|
1030
|
-
} catch (err) {
|
|
1031
|
-
backupCodesPanel.hidden = true, backupCodesList.innerHTML = '', syncBackupButtonState();
|
|
1235
|
+
} else if (ok) {
|
|
1236
|
+
await loadUser(), hideEnable2faPanel(), showMessage(confirmMessage, 'success', '2FA 已启用');
|
|
1237
|
+
setTimeout(() => hideMessage(confirmMessage), 3000);
|
|
1032
1238
|
}
|
|
1239
|
+
else verifyTokenInput.value = '', verifyTokenInput.focus();
|
|
1033
1240
|
});
|
|
1034
|
-
saveBackupFileBtn.addEventListener('click', () => {
|
|
1035
|
-
if (latestBackupCodes.length) saveBackupCodesToFile(latestBackupCodes, `${currentUser?.username}*2FA备用码`);
|
|
1036
|
-
else alert('没有备份码可保存');
|
|
1037
|
-
});
|
|
1038
|
-
printBackupCodesBtn.addEventListener('click', () => {
|
|
1039
|
-
if (latestBackupCodes.length) printBackupCodes(latestBackupCodes, `${currentUser?.username}*2FA备用码`);
|
|
1040
|
-
else alert('没有备份码可打印');
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
// ==================== WebAuthn 硬件验证模块 ====================
|
|
1044
|
-
const loadWebAuthnData = async () => {
|
|
1045
|
-
try {
|
|
1046
|
-
const { ok, data } = await requestApi('/api/webauthn/credentials', { method: 'GET' }, null, null, true);
|
|
1047
|
-
if (ok) currentWebAuthnCredentials = data.credentials || [];
|
|
1048
|
-
else throw new Error(data.message);
|
|
1049
|
-
} catch (err) {
|
|
1050
|
-
currentWebAuthnCredentials = [];
|
|
1051
|
-
const targetMsg = manageWebAuthnPanel.hidden ? webauthnMessage : webauthnManageMessage;
|
|
1052
|
-
showMessage(targetMsg, 'error', '加载硬件凭证失败,请刷新页面重试');
|
|
1053
|
-
} finally { refreshWebAuthnUI(); }
|
|
1054
|
-
},
|
|
1055
|
-
renderCredentialsList = () => {
|
|
1056
|
-
if (currentWebAuthnCredentials.length === 0) return noDeviceMsg.style.display = 'block';
|
|
1057
|
-
|
|
1058
|
-
webauthnDeviceList.innerHTML = '', noDeviceMsg.style.display = 'none';
|
|
1059
|
-
currentWebAuthnCredentials.forEach((cred, idx) => {
|
|
1060
|
-
const li = deviceItemExample.cloneNode(true), nameSpan = li.querySelector('.device-name'),
|
|
1061
|
-
delBtn = li.querySelector('button'), time = new Date(cred.createdAt).toLocaleString();
|
|
1062
1241
|
|
|
1063
|
-
|
|
1064
|
-
li.style.display = 'flex', webauthnDeviceList.append(li);
|
|
1065
|
-
delBtn.addEventListener('click', async e => {
|
|
1066
|
-
e.preventDefault();
|
|
1067
|
-
if (!confirm('确定删除此设备吗?删除后将无法用于登录验证;')) return;
|
|
1068
|
-
const { ok } = await requestApi('/api/webauthn/credentials/delete',
|
|
1069
|
-
{
|
|
1070
|
-
method: 'POST',
|
|
1071
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1072
|
-
body: JSON.stringify({ credentialId: cred.id })
|
|
1073
|
-
}, webauthnManageMessage, delBtn);
|
|
1074
|
-
if (ok) await loadUser(), await loadWebAuthnData();
|
|
1075
|
-
});
|
|
1076
|
-
});
|
|
1077
|
-
},
|
|
1078
|
-
refreshWebAuthnUI = () => {
|
|
1079
|
-
hideMessage(webauthnMessage), hideMessage(webauthnManageMessage);
|
|
1080
|
-
const isEnabled = currentUser?.webauthnEnabled === true;
|
|
1081
|
-
webauthnStatusIcon.innerHTML = isEnabled ? '✅' : '';
|
|
1082
|
-
toggleWebAuthnBtn.textContent = isEnabled ? '关闭硬件验证' : '启用硬件验证';
|
|
1083
|
-
toggleWebAuthnBtn.classList.toggle('btn-danger', isEnabled);
|
|
1084
|
-
toggleWebAuthnBtn.disabled = false, manageAddDeviceBtn.disabled = false;
|
|
1085
|
-
if (isEnabled) webauthnMessage.hidden = true, manageWebAuthnPanel.hidden = false, renderCredentialsList();
|
|
1086
|
-
else {
|
|
1087
|
-
manageWebAuthnPanel.hidden = true, webauthnMessage.hidden = false;
|
|
1088
|
-
webauthnDeviceList.innerHTML = '', noDeviceMsg.style.display = 'block';
|
|
1089
|
-
}
|
|
1090
|
-
},
|
|
1091
|
-
startAddDeviceAndEnableIfNeeded = async () => {
|
|
1092
|
-
hideMessage(webauthnMessage), hideMessage(webauthnManageMessage);
|
|
1093
|
-
try {
|
|
1094
|
-
const beginResult = await requestApi('/api/webauthn/register/begin', { method: 'POST' }, webauthnMessage, null, true);
|
|
1095
|
-
if (!beginResult.ok) return;
|
|
1096
|
-
const options = beginResult.data, attResp = await flunWebAuthnBrowser.startRegistration({ optionsJSON: options }),
|
|
1097
|
-
completeResult = await requestApi('/api/webauthn/register/complete', {
|
|
1098
|
-
method: 'POST',
|
|
1099
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1100
|
-
body: JSON.stringify({ attestationResponse: attResp })
|
|
1101
|
-
}, webauthnMessage);
|
|
1102
|
-
if (!completeResult.ok) return;
|
|
1103
|
-
await loadUser(), await loadWebAuthnData();
|
|
1104
|
-
if (!currentUser?.webauthnEnabled) {
|
|
1105
|
-
const { ok, data } = await requestApi('/api/webauthn/toggle', { method: 'POST' }, webauthnMessage);
|
|
1106
|
-
if (ok) await loadUser();
|
|
1107
|
-
}
|
|
1108
|
-
} catch (err) {
|
|
1109
|
-
if (err.name === 'AbortError' || err.name === 'NotAllowedError') return;
|
|
1110
|
-
let errorMsg = '添加失败:';
|
|
1111
|
-
if (err.name === 'NotAllowedError') errorMsg += '操作被拒绝或超时,请确保使用 HTTPS 且设备已配置';
|
|
1112
|
-
else errorMsg += '未知错误';
|
|
1113
|
-
showMessage(webauthnMessage, 'error', errorMsg);
|
|
1114
|
-
}
|
|
1115
|
-
},
|
|
1116
|
-
toggleWebAuthn = async () => {
|
|
1117
|
-
const targetState = !currentUser?.webauthnEnabled;
|
|
1118
|
-
if (!targetState) {
|
|
1119
|
-
if (!confirm('确定关闭硬件验证吗?关闭后登录时不再需要硬件验证;')) return;
|
|
1120
|
-
const oldUser = { ...currentUser };
|
|
1121
|
-
if (currentUser) currentUser.webauthnEnabled = false;
|
|
1122
|
-
refreshWebAuthnUI();
|
|
1123
|
-
const { ok, data } = await requestApi('/api/webauthn/toggle', { method: 'POST' }, null, toggleWebAuthnBtn);
|
|
1124
|
-
if (ok) await loadUser();
|
|
1125
|
-
else {
|
|
1126
|
-
if (currentUser) currentUser.webauthnEnabled = oldUser?.webauthnEnabled;
|
|
1127
|
-
refreshWebAuthnUI(), alert(data.message);
|
|
1128
|
-
}
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
if (currentWebAuthnCredentials.length > 0) {
|
|
1132
|
-
const { ok } = await requestApi('/api/webauthn/toggle', { method: 'POST' }, webauthnMessage, toggleWebAuthnBtn);
|
|
1133
|
-
if (ok) await loadUser();
|
|
1134
|
-
} else {
|
|
1135
|
-
try { await startAddDeviceAndEnableIfNeeded() }
|
|
1136
|
-
finally { toggleWebAuthnBtn.disabled = false }
|
|
1137
|
-
}
|
|
1138
|
-
};
|
|
1242
|
+
// 5. WebAuthn 硬件验证
|
|
1139
1243
|
toggleWebAuthnBtn.addEventListener('click', toggleWebAuthn);
|
|
1140
|
-
manageAddDeviceBtn.addEventListener('click',
|
|
1141
|
-
|
|
1142
|
-
// ==================== 注销账户模块 ====================
|
|
1143
|
-
const toggleDeleteForm = show => {
|
|
1144
|
-
if (show) {
|
|
1145
|
-
showDeleteAccountBtn.hidden = true, deleteAccountForm.hidden = false;
|
|
1146
|
-
setTimeout(() => deleteAccountPassword.focus(), 50), hideMessage(deleteAccountMessage);
|
|
1147
|
-
} else {
|
|
1148
|
-
showDeleteAccountBtn.hidden = false, deleteAccountForm.hidden = true, deleteAccountPassword.value = '';
|
|
1149
|
-
hideMessage(deleteAccountMessage), showDeleteAccountBtn.focus();
|
|
1150
|
-
}
|
|
1151
|
-
};
|
|
1244
|
+
manageAddDeviceBtn.addEventListener('click', () => addWebAuthnDevice());
|
|
1152
1245
|
|
|
1246
|
+
// 6. 注销账户
|
|
1153
1247
|
showDeleteAccountBtn.addEventListener('click', () => toggleDeleteForm(true));
|
|
1154
1248
|
cancelDeleteBtn.addEventListener('click', () => toggleDeleteForm(false));
|
|
1155
1249
|
deleteAccountBtn.addEventListener('click', async () => {
|
|
@@ -1158,36 +1252,26 @@
|
|
|
1158
1252
|
if (!confirm('您确定要永久注销账户吗?此操作不可撤销,所有数据将被删除!!!')) return;
|
|
1159
1253
|
|
|
1160
1254
|
const { ok } = await requestApi('/api/delete-account', {
|
|
1161
|
-
method: 'POST',
|
|
1162
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1255
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1163
1256
|
body: JSON.stringify({ password: pwd })
|
|
1164
1257
|
}, deleteAccountMessage, deleteAccountBtn);
|
|
1165
1258
|
if (ok) setTimeout(() => window.location.href = '/login', 2000);
|
|
1166
1259
|
});
|
|
1260
|
+
|
|
1261
|
+
// 7. 退出登录
|
|
1167
1262
|
logoutBtn.addEventListener('click', async () => {
|
|
1168
1263
|
const { ok } = await requestApi('/api/logout', { method: 'POST' }, null, logoutBtn);
|
|
1169
1264
|
if (ok) window.location.href = '/login';
|
|
1170
1265
|
else alert('退出失败,请重试');
|
|
1171
1266
|
});
|
|
1172
1267
|
|
|
1173
|
-
// ====================
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
usernameEl.textContent = username, emailEl.textContent = email ?? '未设置';
|
|
1181
|
-
emailVerifiedBadge.textContent = emailVerified ? '✅' : '';
|
|
1182
|
-
createdAtEl.textContent = new Date(createdAt).toLocaleString(), await Promise.all([refreshTwofaUI(), loadWebAuthnData()]);
|
|
1183
|
-
} catch (err) { alert('加载用户信息失败,请刷新页面重试'); }
|
|
1184
|
-
},
|
|
1185
|
-
enterHandlers = [
|
|
1186
|
-
{ input: profileCurrentPasswordInput, button: updateProfileBtn },
|
|
1187
|
-
{ input: confirmPasswordInput, button: changePasswordBtn },
|
|
1188
|
-
{ input: deleteAccountPassword, button: deleteAccountBtn },
|
|
1189
|
-
{ input: verifyTokenInput, button: confirm2faBtn }
|
|
1190
|
-
],
|
|
1268
|
+
// ==================== 辅助:回车提交 & 自动清除消息 ====================
|
|
1269
|
+
const enterHandlers = [
|
|
1270
|
+
{ input: profileCurrentPasswordInput, button: updateProfileBtn },
|
|
1271
|
+
{ input: confirmPasswordInput, button: changePasswordBtn },
|
|
1272
|
+
{ input: deleteAccountPassword, button: deleteAccountBtn },
|
|
1273
|
+
{ input: verifyTokenInput, button: confirm2faBtn }
|
|
1274
|
+
],
|
|
1191
1275
|
clearMessageMappings = [
|
|
1192
1276
|
{ messageEl: updateProfileMessage, inputs: [newUsernameInput, newEmailInput, profileCurrentPasswordInput] },
|
|
1193
1277
|
{ messageEl: changePasswordMessage, inputs: [currentPasswordInput, newPasswordInput, confirmPasswordInput] },
|
|
@@ -1208,8 +1292,7 @@
|
|
|
1208
1292
|
if (e.key === 'Enter') e.preventDefault(), button.click();
|
|
1209
1293
|
});
|
|
1210
1294
|
});
|
|
1211
|
-
|
|
1212
|
-
window.addEventListener('beforeunload', stopEmailPolling), setupInputClearMessage(), loadUser();
|
|
1295
|
+
setupInputClearMessage(), window.addEventListener('beforeunload', stopEmailPolling), loadUser();
|
|
1213
1296
|
</script>
|
|
1214
1297
|
</body>
|
|
1215
1298
|
|