@flun/html-template 4.4.2 → 5.0.0
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 +13 -7
- package/f-README.md +22 -23
- package/package.json +1 -1
- package/templates/account/2fa.html +211 -69
- package/templates/account/forgot-password.html +53 -4
- package/templates/account/login.html +79 -49
- package/templates/account/profile.html +652 -331
- package/templates/account/register.html +58 -2
- package/templates/account/reset-password.html +54 -3
- package/templates/account/verify-email.html +61 -6
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="UTF-8">
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
7
7
|
<title>个人资料 - 安全账户中心</title>
|
|
8
8
|
|
|
9
9
|
<!-- 引入公共主题变量与全局样式 -->
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<link rel="stylesheet" href="/static/topImg.css" /> <!-- 返回顶部图标 -->
|
|
14
14
|
|
|
15
15
|
<style>
|
|
16
|
+
/* ========== 基础移动优先增强 ========== */
|
|
16
17
|
body {
|
|
17
18
|
background: var(--body-bg);
|
|
18
19
|
background-color: var(--bg-color);
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
.card {
|
|
29
30
|
background: var(--container-bg);
|
|
30
31
|
border-radius: 12px;
|
|
32
|
+
border: 1px solid #2e3135;
|
|
31
33
|
box-shadow: 0 2px 10px var(--content-shadow);
|
|
32
34
|
padding: 30px;
|
|
33
35
|
margin-bottom: 20px;
|
|
@@ -68,22 +70,28 @@
|
|
|
68
70
|
/* 信息行 */
|
|
69
71
|
.info-row {
|
|
70
72
|
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
align-items: center;
|
|
75
|
+
text-align: center;
|
|
71
76
|
padding: 12px 0;
|
|
72
77
|
border-bottom: 1px solid var(--content-border);
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
.info-label {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
opacity: 0.75;
|
|
81
|
+
margin-bottom: 6px;
|
|
82
|
+
font-size: 13px;
|
|
79
83
|
font-weight: 500;
|
|
84
|
+
opacity: 0.65;
|
|
85
|
+
color: var(--text-color);
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
.info-value {
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
font-size: 16px;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
color: #4a5568;
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
|
|
87
95
|
/* 通用按钮 */
|
|
88
96
|
.btn {
|
|
89
97
|
padding: 10px 20px;
|
|
@@ -96,13 +104,17 @@
|
|
|
96
104
|
cursor: pointer;
|
|
97
105
|
transition: background 0.2s, opacity 0.2s;
|
|
98
106
|
margin-right: 10px;
|
|
107
|
+
text-align: center;
|
|
108
|
+
display: inline-flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
justify-content: center;
|
|
111
|
+
gap: 6px;
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
.btn:hover {
|
|
102
115
|
background: var(--btn-hover);
|
|
103
116
|
}
|
|
104
117
|
|
|
105
|
-
/* 功能按钮颜色保留原语义(不随主题变化) */
|
|
106
118
|
.btn-danger {
|
|
107
119
|
background: #e53e3e;
|
|
108
120
|
}
|
|
@@ -174,7 +186,6 @@
|
|
|
174
186
|
color: var(--text-color);
|
|
175
187
|
}
|
|
176
188
|
|
|
177
|
-
/* 消息提示(语义色保留) */
|
|
178
189
|
.message {
|
|
179
190
|
margin-top: 15px;
|
|
180
191
|
padding: 10px;
|
|
@@ -191,9 +202,54 @@
|
|
|
191
202
|
color: #742a2a;
|
|
192
203
|
}
|
|
193
204
|
|
|
194
|
-
.info {
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
}
|
|
197
253
|
}
|
|
198
254
|
|
|
199
255
|
.warning {
|
|
@@ -201,7 +257,6 @@
|
|
|
201
257
|
color: #7b341e;
|
|
202
258
|
}
|
|
203
259
|
|
|
204
|
-
/* 表单 */
|
|
205
260
|
.form-group {
|
|
206
261
|
margin-bottom: 15px;
|
|
207
262
|
}
|
|
@@ -215,6 +270,7 @@
|
|
|
215
270
|
background: var(--li-bg);
|
|
216
271
|
color: var(--text-color);
|
|
217
272
|
transition: border-color 0.2s, background 0.5s ease, color 0.5s ease;
|
|
273
|
+
box-sizing: border-box;
|
|
218
274
|
}
|
|
219
275
|
|
|
220
276
|
.form-group input:focus {
|
|
@@ -222,26 +278,14 @@
|
|
|
222
278
|
outline: none;
|
|
223
279
|
}
|
|
224
280
|
|
|
225
|
-
/* 操作按钮组 */
|
|
226
281
|
.action-buttons {
|
|
227
282
|
display: flex;
|
|
228
283
|
gap: 10px;
|
|
229
284
|
margin-top: 20px;
|
|
230
285
|
flex-wrap: wrap;
|
|
286
|
+
justify-content: center;
|
|
231
287
|
}
|
|
232
288
|
|
|
233
|
-
.backup-actions {
|
|
234
|
-
margin-top: 15px;
|
|
235
|
-
display: flex;
|
|
236
|
-
gap: 12px;
|
|
237
|
-
justify-content: flex-start;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
.backup-actions .btn {
|
|
241
|
-
margin-right: 0;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/* 二维码 */
|
|
245
289
|
.qr-placeholder {
|
|
246
290
|
text-align: center;
|
|
247
291
|
margin: 15px 0;
|
|
@@ -255,11 +299,11 @@
|
|
|
255
299
|
background: var(--container-bg);
|
|
256
300
|
}
|
|
257
301
|
|
|
258
|
-
/* 其他辅助样式 */
|
|
259
302
|
.flex-buttons {
|
|
260
303
|
display: flex;
|
|
261
304
|
gap: 10px;
|
|
262
305
|
margin-top: 8px;
|
|
306
|
+
justify-content: center;
|
|
263
307
|
}
|
|
264
308
|
|
|
265
309
|
.webauthn-panel {
|
|
@@ -285,6 +329,7 @@
|
|
|
285
329
|
display: flex;
|
|
286
330
|
align-items: center;
|
|
287
331
|
gap: 8px;
|
|
332
|
+
word-break: break-word;
|
|
288
333
|
}
|
|
289
334
|
|
|
290
335
|
.btn-small {
|
|
@@ -296,18 +341,14 @@
|
|
|
296
341
|
display: none !important;
|
|
297
342
|
}
|
|
298
343
|
|
|
299
|
-
#printBackupContainer {
|
|
300
|
-
display: none;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
344
|
@media print {
|
|
304
345
|
body> :not(#printBackupContainer) {
|
|
305
346
|
display: none !important;
|
|
306
347
|
}
|
|
307
348
|
|
|
308
349
|
#printBackupContainer {
|
|
309
|
-
position: static !important;
|
|
310
350
|
display: block !important;
|
|
351
|
+
position: static !important;
|
|
311
352
|
margin: 20px;
|
|
312
353
|
}
|
|
313
354
|
|
|
@@ -347,6 +388,207 @@
|
|
|
347
388
|
color: #666;
|
|
348
389
|
}
|
|
349
390
|
}
|
|
391
|
+
|
|
392
|
+
/* ==================== 移动端样式适配 (≤640px) ==================== */
|
|
393
|
+
@media (max-width: 640px) {
|
|
394
|
+
body {
|
|
395
|
+
padding: 16px 12px;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.container {
|
|
399
|
+
max-width: 100%;
|
|
400
|
+
padding: 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.card {
|
|
404
|
+
padding: 20px 16px;
|
|
405
|
+
border-radius: 16px;
|
|
406
|
+
margin-bottom: 16px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.card h2 {
|
|
410
|
+
font-size: 20px;
|
|
411
|
+
margin-bottom: 16px;
|
|
412
|
+
gap: 8px;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.card h2 .btn {
|
|
416
|
+
font-size: 12px;
|
|
417
|
+
padding: 6px 12px;
|
|
418
|
+
white-space: nowrap;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.info-row {
|
|
422
|
+
align-items: flex-start;
|
|
423
|
+
text-align: left;
|
|
424
|
+
padding: 14px 0;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.info-label {
|
|
428
|
+
font-size: 12px;
|
|
429
|
+
margin-bottom: 6px;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.info-value {
|
|
433
|
+
font-size: 15px;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.action-buttons {
|
|
437
|
+
flex-direction: column;
|
|
438
|
+
gap: 12px;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.action-buttons .btn {
|
|
442
|
+
width: 100%;
|
|
443
|
+
margin-right: 0;
|
|
444
|
+
justify-content: center;
|
|
445
|
+
padding: 12px 16px;
|
|
446
|
+
font-size: 15px;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#editProfileCard>form>div:first-of-type,
|
|
450
|
+
#changePasswordCard>form>div:first-of-type,
|
|
451
|
+
#deleteAccountForm>div:first-of-type {
|
|
452
|
+
flex-direction: column !important;
|
|
453
|
+
gap: 12px !important;
|
|
454
|
+
width: 100%;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#editProfileCard>form>div:first-of-type .btn,
|
|
458
|
+
#changePasswordCard>form>div:first-of-type .btn,
|
|
459
|
+
#deleteAccountForm>div:first-of-type .btn {
|
|
460
|
+
width: 100% !important;
|
|
461
|
+
margin-right: 0 !important;
|
|
462
|
+
margin-left: 0 !important;
|
|
463
|
+
box-sizing: border-box;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.flex-buttons {
|
|
467
|
+
flex-direction: column;
|
|
468
|
+
gap: 12px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.flex-buttons .btn {
|
|
472
|
+
width: 100%;
|
|
473
|
+
margin-right: 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.device-item {
|
|
477
|
+
flex-direction: column;
|
|
478
|
+
align-items: flex-start;
|
|
479
|
+
gap: 12px;
|
|
480
|
+
padding: 12px 6px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.device-name {
|
|
484
|
+
width: 100%;
|
|
485
|
+
font-size: 14px;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.device-item .btn-small {
|
|
489
|
+
align-self: flex-start;
|
|
490
|
+
margin-top: 4px;
|
|
491
|
+
min-width: 80px;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.qr-placeholder img {
|
|
495
|
+
max-width: 160px;
|
|
496
|
+
width: 100%;
|
|
497
|
+
height: auto;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.backup-codes {
|
|
501
|
+
padding: 12px;
|
|
502
|
+
text-align: center;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.code-item {
|
|
506
|
+
display: inline-block;
|
|
507
|
+
font-size: 14px;
|
|
508
|
+
padding: 6px 10px;
|
|
509
|
+
margin: 6px 4px;
|
|
510
|
+
letter-spacing: 0.5px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.form-group input,
|
|
514
|
+
input[type="text"],
|
|
515
|
+
input[type="email"],
|
|
516
|
+
input[type="password"] {
|
|
517
|
+
font-size: 16px;
|
|
518
|
+
padding: 12px 12px;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.btn {
|
|
522
|
+
min-height: 44px;
|
|
523
|
+
padding: 10px 16px;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
#twofaCard h2 .btn {
|
|
527
|
+
white-space: normal;
|
|
528
|
+
word-break: keep-all;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
#manageAddDeviceBtn {
|
|
532
|
+
width: 100%;
|
|
533
|
+
justify-content: center;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#deleteAccountForm {
|
|
537
|
+
margin-top: 4px;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.webauthn-panel {
|
|
541
|
+
margin-top: 12px;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#backupCodesPanel p.info {
|
|
545
|
+
font-size: 13px;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
code#secretCode {
|
|
549
|
+
word-break: break-all;
|
|
550
|
+
display: inline-block;
|
|
551
|
+
max-width: 100%;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/* 针对极小屏幕 (≤480px) 微调边距 */
|
|
556
|
+
@media (max-width: 480px) {
|
|
557
|
+
body {
|
|
558
|
+
padding: 12px 10px;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.card {
|
|
562
|
+
padding: 16px 14px;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.card h2 {
|
|
566
|
+
font-size: 18px;
|
|
567
|
+
gap: 6px;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.info-label {
|
|
571
|
+
font-size: 13px;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.info-value {
|
|
575
|
+
font-size: 14px;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.btn {
|
|
579
|
+
font-size: 14px;
|
|
580
|
+
padding: 10px 12px;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.code-item {
|
|
584
|
+
font-size: 13px;
|
|
585
|
+
padding: 5px 8px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.qr-placeholder img {
|
|
589
|
+
max-width: 140px;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
350
592
|
</style>
|
|
351
593
|
</head>
|
|
352
594
|
|
|
@@ -366,7 +608,7 @@
|
|
|
366
608
|
</span>
|
|
367
609
|
</div>
|
|
368
610
|
<div class="info-row"><span class="info-label">注册时间</span>
|
|
369
|
-
<span class="info-value" id="createdAt"
|
|
611
|
+
<span class="info-value" id="createdAt">加载中...</span>
|
|
370
612
|
</div>
|
|
371
613
|
<div class="action-buttons">
|
|
372
614
|
<button type="button" class="btn btn-outline" id="editProfileBtn">编辑资料</button>
|
|
@@ -389,7 +631,7 @@
|
|
|
389
631
|
<input type="password" id="profileCurrentPassword" placeholder="请输入密码" maxlength="72"
|
|
390
632
|
autocomplete="off">
|
|
391
633
|
</div>
|
|
392
|
-
<div
|
|
634
|
+
<div class="flex-buttons">
|
|
393
635
|
<button type="button" class="btn" id="updateProfileBtn">保存修改</button>
|
|
394
636
|
<button type="button" class="btn btn-danger" id="cancelEditBtn">取消</button>
|
|
395
637
|
</div>
|
|
@@ -408,9 +650,9 @@
|
|
|
408
650
|
<input type="password" id="newPassword" placeholder="新密码(至少6位)" maxlength="72" autocomplete="off">
|
|
409
651
|
</div>
|
|
410
652
|
<div class="form-group">
|
|
411
|
-
<input type="password" id="confirmPassword" placeholder="
|
|
653
|
+
<input type="password" id="confirmPassword" placeholder="再次输入新密码" maxlength="72" autocomplete="off">
|
|
412
654
|
</div>
|
|
413
|
-
<div
|
|
655
|
+
<div class="flex-buttons">
|
|
414
656
|
<button type="button" class="btn" id="changePasswordBtn">确认提交</button>
|
|
415
657
|
<button type="button" class="btn btn-secondary" id="cancelChangePasswordBtn">取消</button>
|
|
416
658
|
</div>
|
|
@@ -418,7 +660,24 @@
|
|
|
418
660
|
</form>
|
|
419
661
|
</div>
|
|
420
662
|
|
|
421
|
-
<!--
|
|
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认证卡片 -->
|
|
422
681
|
<div class="card" id="twofaCard">
|
|
423
682
|
<h2>
|
|
424
683
|
<span class="title-text">
|
|
@@ -428,14 +687,13 @@
|
|
|
428
687
|
</h2>
|
|
429
688
|
|
|
430
689
|
<div id="enable2faPanel" hidden style="margin-top: 16px;">
|
|
431
|
-
<form onsubmit="return false;">
|
|
690
|
+
<form onsubmit="return false;" class="form-group">
|
|
432
691
|
<p>请使用 Google Authenticator 或类似应用扫描二维码添加认证:</p>
|
|
433
692
|
<div class="qr-placeholder">
|
|
434
693
|
<img id="qrCodeImg" alt="2FA QR Code">
|
|
435
694
|
</div>
|
|
436
|
-
<p
|
|
437
|
-
<input type="text" id="verifyToken" placeholder="
|
|
438
|
-
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">
|
|
439
697
|
<div class="flex-buttons">
|
|
440
698
|
<button type="button" class="btn" id="confirm2faBtn">验证并启用</button>
|
|
441
699
|
<button type="button" class="btn btn-secondary" id="cancelEnable2faBtn">取消</button>
|
|
@@ -443,42 +701,25 @@
|
|
|
443
701
|
<div id="confirmMessage" class="message" hidden></div>
|
|
444
702
|
</form>
|
|
445
703
|
</div>
|
|
446
|
-
|
|
447
|
-
<div id="manage2faPanel" hidden style="margin-top: 16px;">
|
|
448
|
-
<button type="button" class="btn btn-warning" id="showBackupBtn">生成新的备份码</button>
|
|
449
|
-
<div id="backupManageMessage" class="message" hidden></div>
|
|
450
|
-
<div id="backupCodesPanel" hidden style="margin-top: 15px;">
|
|
451
|
-
<p class="info">备用码(仅显示一次,请妥善保存):</p>
|
|
452
|
-
<div id="backupCodesList" class="backup-codes"></div>
|
|
453
|
-
<div class="backup-actions">
|
|
454
|
-
<button type="button" class="btn btn-outline" id="saveBackupFileBtn">💾 保存到文件</button>
|
|
455
|
-
<button type="button" class="btn btn-outline" id="printBackupCodesBtn">🖨️ 打印备份码</button>
|
|
456
|
-
</div>
|
|
457
|
-
</div>
|
|
458
|
-
</div>
|
|
459
704
|
</div>
|
|
460
705
|
|
|
461
|
-
<!--
|
|
706
|
+
<!-- 硬件认证卡片 -->
|
|
462
707
|
<div class="card" id="webauthnCard">
|
|
463
708
|
<h2>
|
|
464
709
|
<span class="title-text">
|
|
465
|
-
|
|
710
|
+
硬件认证(指纹/人脸等) <span id="webauthnStatusIcon"></span>
|
|
466
711
|
</span>
|
|
467
|
-
<button type="button" class="btn" id="toggleWebAuthnBtn"
|
|
712
|
+
<button type="button" class="btn" id="toggleWebAuthnBtn">启用硬件认证</button>
|
|
468
713
|
</h2>
|
|
469
714
|
|
|
470
715
|
<div id="webauthnMessage" class="message" hidden></div>
|
|
471
716
|
<div id="manageWebAuthnPanel" class="webauthn-panel" hidden>
|
|
472
|
-
<p id="noDeviceMsg" style="color: #666;">暂无硬件设备,点击上方「添加新设备」按钮添加</p>
|
|
473
717
|
<ul id="webauthnDeviceList" class="device-list"></ul>
|
|
474
|
-
<li id="deviceItemExample" class="device-item"
|
|
718
|
+
<li id="deviceItemExample" class="device-item" hidden>
|
|
475
719
|
<span class="device-name"></span>
|
|
476
720
|
<button type="button" class="btn btn-danger btn-small">删除</button>
|
|
477
721
|
</li>
|
|
478
|
-
<
|
|
479
|
-
<button type="button" class="btn btn-outline" id="manageAddDeviceBtn">➕ 添加新设备</button>
|
|
480
|
-
</div>
|
|
481
|
-
<div id="webauthnManageMessage" class="message" hidden></div>
|
|
722
|
+
<button type="button" class="btn btn-outline" id="manageAddDeviceBtn">➕ 添加新设备</button>
|
|
482
723
|
</div>
|
|
483
724
|
</div>
|
|
484
725
|
|
|
@@ -493,10 +734,10 @@
|
|
|
493
734
|
<div id="deleteAccountForm" hidden style="margin-top:15px;">
|
|
494
735
|
<form onsubmit="return false;">
|
|
495
736
|
<div class="form-group">
|
|
496
|
-
<input type="password" id="deleteAccountPassword" placeholder="
|
|
737
|
+
<input type="password" id="deleteAccountPassword" placeholder="请输入登录密码" maxlength="72"
|
|
497
738
|
autocomplete="off">
|
|
498
739
|
</div>
|
|
499
|
-
<div
|
|
740
|
+
<div class="flex-buttons">
|
|
500
741
|
<button type="button" class="btn btn-danger" id="deleteAccountBtn">永久注销账户</button>
|
|
501
742
|
<button type="button" class="btn btn-secondary" id="cancelDeleteBtn">取消</button>
|
|
502
743
|
</div>
|
|
@@ -505,7 +746,7 @@
|
|
|
505
746
|
</div>
|
|
506
747
|
</div>
|
|
507
748
|
</div>
|
|
508
|
-
<div id="printBackupContainer"></div>
|
|
749
|
+
<div id="printBackupContainer" hidden></div>
|
|
509
750
|
|
|
510
751
|
<!-- 公共逻辑 -->
|
|
511
752
|
<script src="/static/themeModule.js" defer></script><!-- 引入主题自适应模块 -->
|
|
@@ -514,46 +755,44 @@
|
|
|
514
755
|
<script src="/static/topImg.js" defer></script> <!-- 引入返回顶部图标模块 -->
|
|
515
756
|
<script src="/static/utils/browser.js"></script> <!-- 引入@flun前端硬件验证模块 -->
|
|
516
757
|
<script>
|
|
517
|
-
//
|
|
758
|
+
// 全局变量
|
|
518
759
|
let currentUser = null, targetEmail = null, emailCheckInterval = null, isEmailPollingActive = false,
|
|
519
760
|
latestBackupCodes = [], currentWebAuthnCredentials = [];
|
|
520
761
|
|
|
521
|
-
//
|
|
522
|
-
const [
|
|
523
|
-
usernameEl, emailEl, createdAtEl,
|
|
762
|
+
// DOM 元素常量
|
|
763
|
+
const [usernameEl, emailEl, emailVerifiedBadge, createdAtEl, editProfileBtn, showChangePasswordBtn, logoutBtn,
|
|
524
764
|
editProfileCard, newUsernameInput, newEmailInput, profileCurrentPasswordInput,
|
|
525
|
-
|
|
765
|
+
updateProfileBtn, cancelEditBtn, updateProfileMessage,
|
|
526
766
|
changePasswordCard, currentPasswordInput, newPasswordInput, confirmPasswordInput,
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
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
|
|
537
776
|
] = [
|
|
538
|
-
'username', 'email', 'createdAt',
|
|
777
|
+
'username', 'email', 'emailVerifiedBadge', 'createdAt', 'editProfileBtn', 'showChangePasswordBtn', 'logoutBtn',
|
|
539
778
|
'editProfileCard', 'newUsername', 'newEmail', 'profileCurrentPassword',
|
|
540
|
-
'
|
|
779
|
+
'updateProfileBtn', 'cancelEditBtn', 'updateProfileMessage',
|
|
541
780
|
'changePasswordCard', 'currentPassword', 'newPassword', 'confirmPassword',
|
|
542
|
-
'
|
|
543
|
-
'
|
|
544
|
-
'
|
|
545
|
-
'
|
|
546
|
-
'
|
|
547
|
-
'
|
|
548
|
-
'
|
|
549
|
-
'
|
|
550
|
-
'
|
|
551
|
-
'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'
|
|
552
790
|
].map(id => document.getElementById(id)),
|
|
553
|
-
//
|
|
791
|
+
// 基础工具
|
|
554
792
|
showMessage = (element, type, text) => {
|
|
555
793
|
element.className = `message ${type}`, element.textContent = text, element.hidden = false;
|
|
556
|
-
},
|
|
794
|
+
},
|
|
795
|
+
hideMessage = element => (element.hidden = true, element.textContent = '', element.className = 'message'),
|
|
557
796
|
requestApi = async (url, options, msgElement = null, btnElement = null, suppressSuccessMsg = false) => {
|
|
558
797
|
if (btnElement && typeof btnElement === 'boolean') suppressSuccessMsg = btnElement, btnElement = null;
|
|
559
798
|
if (btnElement) btnElement.disabled = true;
|
|
@@ -591,13 +830,124 @@
|
|
|
591
830
|
if (isEmailPollingActive) stopEmailPolling(), timeoutCallback?.();
|
|
592
831
|
}, 900000);
|
|
593
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
|
+
// 个人资料模块
|
|
594
889
|
resetEditProfileForm = () => {
|
|
595
890
|
profileCurrentPasswordInput.value = '', newUsernameInput.value = currentUser?.username;
|
|
596
891
|
newEmailInput.value = currentUser?.email;
|
|
597
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
|
+
// 密码修改模块
|
|
598
909
|
resetPasswordForm = () => {
|
|
599
910
|
currentPasswordInput.value = '', newPasswordInput.value = '', confirmPasswordInput.value = '';
|
|
600
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
|
+
},
|
|
601
951
|
saveBackupCodesToFile = (backupCodes, prefix = '我的账户备份码') => {
|
|
602
952
|
if (backupCodes?.length === 0) return alert('没有可保存的备份码');
|
|
603
953
|
const now = new Date(), time = now.toLocaleString(), fileTime = time.replace(/[\/:]/g, '-').replace(/ /g, '_');
|
|
@@ -615,56 +965,161 @@
|
|
|
615
965
|
printBackupCodes = (backupCodes, title = '双因素认证备用码') => {
|
|
616
966
|
if (!backupCodes || backupCodes.length === 0) return alert('没有可打印的备份码');
|
|
617
967
|
printBackupContainer.innerHTML = `
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
</div>
|
|
624
|
-
<div class="warning">⚠️ 每个备用码仅能使用一次,请妥善保管;</div>
|
|
625
|
-
<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('')}
|
|
626
973
|
</div>
|
|
627
|
-
|
|
974
|
+
<div class="warning">⚠️ 每个备用码仅能使用一次,请妥善保管;</div>
|
|
975
|
+
<div class="footer">共计 ${backupCodes.length} 个备用码</div>
|
|
976
|
+
</div>`;
|
|
628
977
|
printBackupCodesBtn.disabled = true, window.print(), setTimeout(() => printBackupContainer.innerHTML = '', 500);
|
|
629
978
|
},
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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);
|
|
638
992
|
}
|
|
993
|
+
deleteBackupCodesBtn.disabled = false;
|
|
639
994
|
},
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
+
});
|
|
646
1067
|
});
|
|
647
|
-
backupCodesPanel.hidden = false, syncBackupButtonState();
|
|
648
1068
|
},
|
|
649
|
-
|
|
650
|
-
|
|
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 = '';
|
|
651
1078
|
},
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
+
}
|
|
658
1106
|
}
|
|
659
1107
|
},
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
+
}
|
|
666
1119
|
};
|
|
667
1120
|
|
|
1121
|
+
// ==================== 事件绑定 ====================
|
|
1122
|
+
// 1. 个人资料
|
|
668
1123
|
editProfileBtn.addEventListener('click', openEditProfileCard);
|
|
669
1124
|
cancelEditBtn.addEventListener('click', () => { closeEditProfileCard(), editProfileBtn.focus() });
|
|
670
1125
|
updateProfileBtn.addEventListener('click', async () => {
|
|
@@ -679,8 +1134,7 @@
|
|
|
679
1134
|
let skipFinallyRestore = false;
|
|
680
1135
|
try {
|
|
681
1136
|
const { ok } = await requestApi('/api/update-profile', {
|
|
682
|
-
method: 'POST',
|
|
683
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1137
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
684
1138
|
body: JSON.stringify({ username, email, currentPassword: pwd })
|
|
685
1139
|
}, updateProfileMessage, updateProfileBtn);
|
|
686
1140
|
if (ok) {
|
|
@@ -700,18 +1154,7 @@
|
|
|
700
1154
|
}
|
|
701
1155
|
});
|
|
702
1156
|
|
|
703
|
-
//
|
|
704
|
-
const closeChangePasswordCard = () => {
|
|
705
|
-
if (!changePasswordCard.hidden) {
|
|
706
|
-
changePasswordCard.hidden = true, resetPasswordForm(), showChangePasswordBtn.disabled = false;
|
|
707
|
-
hideMessage(changePasswordMessage);
|
|
708
|
-
}
|
|
709
|
-
},
|
|
710
|
-
openChangePasswordCard = () => {
|
|
711
|
-
closeEditProfileCard(), changePasswordCard.hidden = false, showChangePasswordBtn.disabled = true;
|
|
712
|
-
resetPasswordForm(), setTimeout(() => currentPasswordInput.focus(), 50), hideMessage(changePasswordMessage);
|
|
713
|
-
};
|
|
714
|
-
|
|
1157
|
+
// 2. 修改密码
|
|
715
1158
|
showChangePasswordBtn.addEventListener('click', openChangePasswordCard);
|
|
716
1159
|
cancelChangePasswordBtn.addEventListener('click', () => {
|
|
717
1160
|
closeChangePasswordCard(), showChangePasswordBtn.focus();
|
|
@@ -721,37 +1164,45 @@
|
|
|
721
1164
|
.map(input => input.value.trim());
|
|
722
1165
|
|
|
723
1166
|
if (!current || !newPwd || !confirm) return showMessage(changePasswordMessage, 'error', '请填写所有字段 ');
|
|
724
|
-
if (newPwd.length < 6) return showMessage(changePasswordMessage, 'error', '新密码至少6位');
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}, 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);
|
|
731
1173
|
if (ok) setTimeout(() => (closeChangePasswordCard(), window.location.href = '/login'), 1000);
|
|
732
1174
|
});
|
|
733
1175
|
|
|
734
|
-
//
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1176
|
+
// 3. 备份码管理
|
|
1177
|
+
toggleBackupPanelBtn.addEventListener('click', async () => {
|
|
1178
|
+
saveBackupFileBtn.disabled = false, printBackupCodesBtn.disabled = false, toggleBackupPanelBtn.disabled = true;
|
|
1179
|
+
toggleBackupPanelBtn.textContent = '加载中...';
|
|
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);
|
|
754
1204
|
|
|
1205
|
+
// 4. 2FA认证
|
|
755
1206
|
toggle2faBtn.addEventListener('click', async () => {
|
|
756
1207
|
const isEnabled = currentUser?.twoFactorEnabled;
|
|
757
1208
|
if (!isEnabled) {
|
|
@@ -759,9 +1210,14 @@
|
|
|
759
1210
|
else hideEnable2faPanel();
|
|
760
1211
|
} else {
|
|
761
1212
|
if (!confirm('确定关闭双因素认证吗?关闭后账户安全性将降低;')) return;
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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 }
|
|
765
1221
|
}
|
|
766
1222
|
});
|
|
767
1223
|
cancelEnable2faBtn.addEventListener('click', hideEnable2faPanel);
|
|
@@ -769,149 +1225,25 @@
|
|
|
769
1225
|
const token = verifyTokenInput.value.trim();
|
|
770
1226
|
if (!token) return showMessage(confirmMessage, 'error', '请输入6位验证码');
|
|
771
1227
|
const { ok, data } = await requestApi('/api/confirm-2fa', {
|
|
772
|
-
method: 'POST',
|
|
773
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1228
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
774
1229
|
body: JSON.stringify({ token })
|
|
775
1230
|
}, confirmMessage, confirm2faBtn);
|
|
1231
|
+
|
|
776
1232
|
if (ok && data.backupCodes) {
|
|
777
1233
|
await loadUser(), renderBackupCodes(data.backupCodes), hideEnable2faPanel();
|
|
778
1234
|
setTimeout(() => hideMessage(confirmMessage), 3000);
|
|
779
|
-
} else
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
saveBackupFileBtn.disabled = false, printBackupCodesBtn.disabled = false;
|
|
783
|
-
showBackupBtn.disabled = true, showBackupBtn.textContent = '加载中...', hideMessage(backupManageMessage);
|
|
784
|
-
try {
|
|
785
|
-
const isPanelVisible = !backupCodesPanel.hidden;
|
|
786
|
-
if (isPanelVisible) return hideBackupPanel(), showBackupBtn.disabled = false;
|
|
787
|
-
if (!confirm('重新生成备用码,将会使旧备用码失效,是否继续?')) return showBackupBtn.disabled = false;
|
|
788
|
-
backupCodesPanel.hidden = false;
|
|
789
|
-
const result = await requestApi('/api/regenerate-backup-codes', { method: 'POST' }, backupManageMessage, showBackupBtn);
|
|
790
|
-
if (result.ok && result.data.backupCodes) renderBackupCodes(result.data.backupCodes);
|
|
791
|
-
else backupCodesPanel.hidden = true, backupCodesList.innerHTML = '', syncBackupButtonState();
|
|
792
|
-
} catch (err) {
|
|
793
|
-
backupCodesPanel.hidden = true, backupCodesList.innerHTML = '', syncBackupButtonState();
|
|
1235
|
+
} else if (ok) {
|
|
1236
|
+
await loadUser(), hideEnable2faPanel(), showMessage(confirmMessage, 'success', '2FA 已启用');
|
|
1237
|
+
setTimeout(() => hideMessage(confirmMessage), 3000);
|
|
794
1238
|
}
|
|
1239
|
+
else verifyTokenInput.value = '', verifyTokenInput.focus();
|
|
795
1240
|
});
|
|
796
|
-
saveBackupFileBtn.addEventListener('click', () => {
|
|
797
|
-
if (latestBackupCodes.length) saveBackupCodesToFile(latestBackupCodes, `${currentUser?.username}*2FA备用码`);
|
|
798
|
-
else alert('没有备份码可保存');
|
|
799
|
-
});
|
|
800
|
-
printBackupCodesBtn.addEventListener('click', () => {
|
|
801
|
-
if (latestBackupCodes.length) printBackupCodes(latestBackupCodes, `${currentUser?.username}*2FA备用码`);
|
|
802
|
-
else alert('没有备份码可打印');
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
// ==================== WebAuthn 硬件验证模块 ====================
|
|
806
|
-
const loadWebAuthnData = async () => {
|
|
807
|
-
try {
|
|
808
|
-
const { ok, data } = await requestApi('/api/webauthn/credentials', { method: 'GET' }, null, null, true);
|
|
809
|
-
if (ok) currentWebAuthnCredentials = data.credentials || [];
|
|
810
|
-
else throw new Error(data.message);
|
|
811
|
-
} catch (err) {
|
|
812
|
-
currentWebAuthnCredentials = [];
|
|
813
|
-
const targetMsg = manageWebAuthnPanel.hidden ? webauthnMessage : webauthnManageMessage;
|
|
814
|
-
showMessage(targetMsg, 'error', '加载硬件凭证失败,请刷新页面重试');
|
|
815
|
-
} finally { refreshWebAuthnUI(); }
|
|
816
|
-
},
|
|
817
|
-
renderCredentialsList = () => {
|
|
818
|
-
if (currentWebAuthnCredentials.length === 0) return noDeviceMsg.style.display = 'block';
|
|
819
1241
|
|
|
820
|
-
|
|
821
|
-
currentWebAuthnCredentials.forEach((cred, idx) => {
|
|
822
|
-
const li = deviceItemExample.cloneNode(true), nameSpan = li.querySelector('.device-name'),
|
|
823
|
-
delBtn = li.querySelector('button'), time = new Date(cred.createdAt).toLocaleString();
|
|
824
|
-
|
|
825
|
-
nameSpan.textContent = `🔑 ${cred.deviceName} 硬件${idx + 1}(添加于${time})`;
|
|
826
|
-
li.style.display = 'flex', webauthnDeviceList.append(li);
|
|
827
|
-
delBtn.addEventListener('click', async e => {
|
|
828
|
-
e.preventDefault();
|
|
829
|
-
if (!confirm('确定删除此设备吗?删除后将无法用于登录验证;')) return;
|
|
830
|
-
const { ok } = await requestApi('/api/webauthn/credentials/delete',
|
|
831
|
-
{
|
|
832
|
-
method: 'POST',
|
|
833
|
-
headers: { 'Content-Type': 'application/json' },
|
|
834
|
-
body: JSON.stringify({ credentialId: cred.id })
|
|
835
|
-
}, webauthnManageMessage, delBtn);
|
|
836
|
-
if (ok) await loadUser(), await loadWebAuthnData();
|
|
837
|
-
});
|
|
838
|
-
});
|
|
839
|
-
},
|
|
840
|
-
refreshWebAuthnUI = () => {
|
|
841
|
-
hideMessage(webauthnMessage), hideMessage(webauthnManageMessage);
|
|
842
|
-
const isEnabled = currentUser?.webauthnEnabled === true;
|
|
843
|
-
webauthnStatusIcon.innerHTML = isEnabled ? '✅' : '';
|
|
844
|
-
toggleWebAuthnBtn.textContent = isEnabled ? '关闭硬件验证' : '启用硬件验证';
|
|
845
|
-
toggleWebAuthnBtn.classList.toggle('btn-danger', isEnabled);
|
|
846
|
-
toggleWebAuthnBtn.disabled = false, manageAddDeviceBtn.disabled = false;
|
|
847
|
-
if (isEnabled) webauthnMessage.hidden = true, manageWebAuthnPanel.hidden = false, renderCredentialsList();
|
|
848
|
-
else {
|
|
849
|
-
manageWebAuthnPanel.hidden = true, webauthnMessage.hidden = false;
|
|
850
|
-
webauthnDeviceList.innerHTML = '', noDeviceMsg.style.display = 'block';
|
|
851
|
-
}
|
|
852
|
-
},
|
|
853
|
-
startAddDeviceAndEnableIfNeeded = async () => {
|
|
854
|
-
hideMessage(webauthnMessage), hideMessage(webauthnManageMessage);
|
|
855
|
-
try {
|
|
856
|
-
const beginResult = await requestApi('/api/webauthn/register/begin', { method: 'POST' }, webauthnMessage, null, true);
|
|
857
|
-
if (!beginResult.ok) return;
|
|
858
|
-
const options = beginResult.data, attResp = await flunWebAuthnBrowser.startRegistration({ optionsJSON: options }),
|
|
859
|
-
completeResult = await requestApi('/api/webauthn/register/complete', {
|
|
860
|
-
method: 'POST',
|
|
861
|
-
headers: { 'Content-Type': 'application/json' },
|
|
862
|
-
body: JSON.stringify({ attestationResponse: attResp })
|
|
863
|
-
}, webauthnMessage);
|
|
864
|
-
if (!completeResult.ok) return;
|
|
865
|
-
await loadUser(), await loadWebAuthnData();
|
|
866
|
-
if (!currentUser?.webauthnEnabled) {
|
|
867
|
-
const { ok, data } = await requestApi('/api/webauthn/toggle', { method: 'POST' }, webauthnMessage);
|
|
868
|
-
if (ok) await loadUser();
|
|
869
|
-
}
|
|
870
|
-
} catch (err) {
|
|
871
|
-
if (err.name === 'AbortError' || err.name === 'NotAllowedError') return;
|
|
872
|
-
let errorMsg = '添加失败:';
|
|
873
|
-
if (err.name === 'NotAllowedError') errorMsg += '操作被拒绝或超时,请确保使用 HTTPS 且设备已配置';
|
|
874
|
-
else errorMsg += '未知错误';
|
|
875
|
-
showMessage(webauthnMessage, 'error', errorMsg);
|
|
876
|
-
}
|
|
877
|
-
},
|
|
878
|
-
toggleWebAuthn = async () => {
|
|
879
|
-
const targetState = !currentUser?.webauthnEnabled;
|
|
880
|
-
if (!targetState) {
|
|
881
|
-
if (!confirm('确定关闭硬件验证吗?关闭后登录时不再需要硬件验证;')) return;
|
|
882
|
-
const oldUser = { ...currentUser };
|
|
883
|
-
if (currentUser) currentUser.webauthnEnabled = false;
|
|
884
|
-
refreshWebAuthnUI();
|
|
885
|
-
const { ok, data } = await requestApi('/api/webauthn/toggle', { method: 'POST' }, null, toggleWebAuthnBtn);
|
|
886
|
-
if (ok) await loadUser();
|
|
887
|
-
else {
|
|
888
|
-
if (currentUser) currentUser.webauthnEnabled = oldUser?.webauthnEnabled;
|
|
889
|
-
refreshWebAuthnUI(), alert(data.message);
|
|
890
|
-
}
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
if (currentWebAuthnCredentials.length > 0) {
|
|
894
|
-
const { ok } = await requestApi('/api/webauthn/toggle', { method: 'POST' }, webauthnMessage, toggleWebAuthnBtn);
|
|
895
|
-
if (ok) await loadUser();
|
|
896
|
-
} else {
|
|
897
|
-
try { await startAddDeviceAndEnableIfNeeded() }
|
|
898
|
-
finally { toggleWebAuthnBtn.disabled = false }
|
|
899
|
-
}
|
|
900
|
-
};
|
|
1242
|
+
// 5. WebAuthn 硬件验证
|
|
901
1243
|
toggleWebAuthnBtn.addEventListener('click', toggleWebAuthn);
|
|
902
|
-
manageAddDeviceBtn.addEventListener('click',
|
|
903
|
-
|
|
904
|
-
// ==================== 注销账户模块 ====================
|
|
905
|
-
const toggleDeleteForm = show => {
|
|
906
|
-
if (show) {
|
|
907
|
-
showDeleteAccountBtn.hidden = true, deleteAccountForm.hidden = false;
|
|
908
|
-
setTimeout(() => deleteAccountPassword.focus(), 50), hideMessage(deleteAccountMessage);
|
|
909
|
-
} else {
|
|
910
|
-
showDeleteAccountBtn.hidden = false, deleteAccountForm.hidden = true, deleteAccountPassword.value = '';
|
|
911
|
-
hideMessage(deleteAccountMessage), showDeleteAccountBtn.focus();
|
|
912
|
-
}
|
|
913
|
-
};
|
|
1244
|
+
manageAddDeviceBtn.addEventListener('click', () => addWebAuthnDevice());
|
|
914
1245
|
|
|
1246
|
+
// 6. 注销账户
|
|
915
1247
|
showDeleteAccountBtn.addEventListener('click', () => toggleDeleteForm(true));
|
|
916
1248
|
cancelDeleteBtn.addEventListener('click', () => toggleDeleteForm(false));
|
|
917
1249
|
deleteAccountBtn.addEventListener('click', async () => {
|
|
@@ -920,36 +1252,26 @@
|
|
|
920
1252
|
if (!confirm('您确定要永久注销账户吗?此操作不可撤销,所有数据将被删除!!!')) return;
|
|
921
1253
|
|
|
922
1254
|
const { ok } = await requestApi('/api/delete-account', {
|
|
923
|
-
method: 'POST',
|
|
924
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1255
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
925
1256
|
body: JSON.stringify({ password: pwd })
|
|
926
1257
|
}, deleteAccountMessage, deleteAccountBtn);
|
|
927
1258
|
if (ok) setTimeout(() => window.location.href = '/login', 2000);
|
|
928
1259
|
});
|
|
1260
|
+
|
|
1261
|
+
// 7. 退出登录
|
|
929
1262
|
logoutBtn.addEventListener('click', async () => {
|
|
930
1263
|
const { ok } = await requestApi('/api/logout', { method: 'POST' }, null, logoutBtn);
|
|
931
1264
|
if (ok) window.location.href = '/login';
|
|
932
1265
|
else alert('退出失败,请重试');
|
|
933
1266
|
});
|
|
934
1267
|
|
|
935
|
-
// ====================
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
usernameEl.textContent = username, emailEl.textContent = email ?? '未设置';
|
|
943
|
-
emailVerifiedBadge.textContent = emailVerified ? '✅' : '';
|
|
944
|
-
createdAtEl.textContent = new Date(createdAt).toLocaleString(), await Promise.all([refreshTwofaUI(), loadWebAuthnData()]);
|
|
945
|
-
} catch (err) { alert('加载用户信息失败,请刷新页面重试'); }
|
|
946
|
-
},
|
|
947
|
-
enterHandlers = [
|
|
948
|
-
{ input: profileCurrentPasswordInput, button: updateProfileBtn },
|
|
949
|
-
{ input: confirmPasswordInput, button: changePasswordBtn },
|
|
950
|
-
{ input: deleteAccountPassword, button: deleteAccountBtn },
|
|
951
|
-
{ input: verifyTokenInput, button: confirm2faBtn }
|
|
952
|
-
],
|
|
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
|
+
],
|
|
953
1275
|
clearMessageMappings = [
|
|
954
1276
|
{ messageEl: updateProfileMessage, inputs: [newUsernameInput, newEmailInput, profileCurrentPasswordInput] },
|
|
955
1277
|
{ messageEl: changePasswordMessage, inputs: [currentPasswordInput, newPasswordInput, confirmPasswordInput] },
|
|
@@ -970,8 +1292,7 @@
|
|
|
970
1292
|
if (e.key === 'Enter') e.preventDefault(), button.click();
|
|
971
1293
|
});
|
|
972
1294
|
});
|
|
973
|
-
|
|
974
|
-
window.addEventListener('beforeunload', stopEmailPolling), setupInputClearMessage(), loadUser();
|
|
1295
|
+
setupInputClearMessage(), window.addEventListener('beforeunload', stopEmailPolling), loadUser();
|
|
975
1296
|
</script>
|
|
976
1297
|
</body>
|
|
977
1298
|
|