@flun/html-template 4.0.10
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/.env +9 -0
- package/LICENSE +15 -0
- package/build.js +3 -0
- package/compile.js +349 -0
- package/copy-files.js +200 -0
- package/customize/account.js +726 -0
- package/customize/data.json +484 -0
- package/customize/functions.js +48 -0
- package/customize/hotReloadInjector.js +25 -0
- package/customize/routes.js +141 -0
- package/customize/users.json +44 -0
- package/customize/variables.js +70 -0
- package/dev-server.js +344 -0
- package/dev.js +4 -0
- package/f-CHANGELOG.md +4 -0
- package/f-README.md +485 -0
- package/index.d.ts +133 -0
- package/index.js +4 -0
- package/package.json +77 -0
- package/restoreDefaults.js +8 -0
- package/services/templateService.js +962 -0
- package/static/about.css +118 -0
- package/static/auth.js +27 -0
- package/static/constants.css +138 -0
- package/static/img/dark.png +0 -0
- package/static/img/favicon.ico +0 -0
- package/static/img/light.png +0 -0
- package/static/img/top.png +0 -0
- package/static/index.css +86 -0
- package/static/mouseOrTouch.js +156 -0
- package/static/public.css +288 -0
- package/static/script.css +318 -0
- package/static/script.js +392 -0
- package/static/styling.css +874 -0
- package/static/styling.js +933 -0
- package/static/themeImg.css +10 -0
- package/static/themeImg.js +19 -0
- package/static/themeModule.js +222 -0
- package/static/topImg.css +19 -0
- package/static/topImg.js +21 -0
- package/static/utils/browser13.js +270 -0
- package/static/utils/closebrackets.js +166 -0
- package/static/utils/css-lint.js +308 -0
- package/static/utils/custom-css-hint.js +876 -0
- package/static/utils/foldgutter.js +141 -0
- package/static/utils/match-highlighter.js +70 -0
- package/templates/about.html +236 -0
- package/templates/account/2fa.html +184 -0
- package/templates/account/forgot-password.html +226 -0
- package/templates/account/login.html +230 -0
- package/templates/account/profile.html +977 -0
- package/templates/account/register.html +224 -0
- package/templates/account/reset-password.html +205 -0
- package/templates/account/verify-email.html +163 -0
- package/templates/base.html +71 -0
- package/templates/footer-content.html +5 -0
- package/templates/index.html +140 -0
- package/templates/script.html +209 -0
- package/templates/test-include.html +11 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>忘记密码处理页</title>
|
|
8
|
+
|
|
9
|
+
<!-- 引入公共主题变量与全局样式 -->
|
|
10
|
+
<link rel="stylesheet" href="/static/constants.css" /> <!-- 样式常量 -->
|
|
11
|
+
<link rel="stylesheet" href="/static/public.css" /> <!-- 公共样式 -->
|
|
12
|
+
<link rel="stylesheet" href="/static/themeImg.css" /> <!-- 主题图标 -->
|
|
13
|
+
<link rel="stylesheet" href="/static/topImg.css" /> <!-- 返回顶部图标 -->
|
|
14
|
+
|
|
15
|
+
<style>
|
|
16
|
+
body {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: center;
|
|
20
|
+
padding: 20px;
|
|
21
|
+
/* 背景由公共变量接管,自动适配深浅色 */
|
|
22
|
+
background-color: var(--bg-color);
|
|
23
|
+
background-image: var(--body-bg);
|
|
24
|
+
/* 保留页面原有的字体偏好,覆盖 public.css 中的通用字体 */
|
|
25
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.card {
|
|
29
|
+
background: var(--container-bg);
|
|
30
|
+
border-radius: 12px;
|
|
31
|
+
box-shadow: 0 20px 40px var(--content-shadow);
|
|
32
|
+
width: 100%;
|
|
33
|
+
max-width: 400px;
|
|
34
|
+
padding: 40px 30px;
|
|
35
|
+
transition: background 0.5s ease, box-shadow 0.5s ease;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.card h2 {
|
|
39
|
+
color: var(--h2-color);
|
|
40
|
+
margin-bottom: 10px;
|
|
41
|
+
text-align: center;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
font-size: 28px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.card p {
|
|
47
|
+
text-align: center;
|
|
48
|
+
color: var(--p-color);
|
|
49
|
+
margin-bottom: 30px;
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.form-group {
|
|
54
|
+
margin-bottom: 20px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.form-group input {
|
|
58
|
+
width: 100%;
|
|
59
|
+
padding: 12px 16px;
|
|
60
|
+
border: 1px solid var(--content-border);
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
font-size: 16px;
|
|
63
|
+
background: var(--li-bg);
|
|
64
|
+
color: var(--text-color);
|
|
65
|
+
transition: border-color 0.2s, background 0.5s ease, color 0.5s ease;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.form-group input:focus {
|
|
69
|
+
border-color: var(--link-color);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.btn {
|
|
73
|
+
width: 100%;
|
|
74
|
+
padding: 14px;
|
|
75
|
+
background: var(--btn-bg);
|
|
76
|
+
color: var(--text-color);
|
|
77
|
+
border: none;
|
|
78
|
+
border-radius: 8px;
|
|
79
|
+
font-size: 16px;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
transition: background 0.2s;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.btn:hover {
|
|
86
|
+
background: var(--btn-hover);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* 禁用按钮保持中性灰,深浅主题下均适用 */
|
|
90
|
+
.btn:disabled,
|
|
91
|
+
.btn.disabled {
|
|
92
|
+
background: #b0b0b0;
|
|
93
|
+
cursor: not-allowed;
|
|
94
|
+
opacity: 0.65;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.btn:disabled:hover {
|
|
98
|
+
background: #b0b0b0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.error {
|
|
102
|
+
color: #e53e3e;
|
|
103
|
+
font-size: 14px;
|
|
104
|
+
margin-top: 10px;
|
|
105
|
+
text-align: center;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.success {
|
|
109
|
+
color: #38a169;
|
|
110
|
+
font-size: 14px;
|
|
111
|
+
margin-top: 10px;
|
|
112
|
+
text-align: center;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.links {
|
|
116
|
+
margin-top: 25px;
|
|
117
|
+
text-align: center;
|
|
118
|
+
font-size: 14px;
|
|
119
|
+
color: var(--text-color);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.links a {
|
|
123
|
+
color: var(--link-color);
|
|
124
|
+
text-decoration: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.links a:hover {
|
|
128
|
+
color: var(--link-hover);
|
|
129
|
+
text-decoration: underline;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.hidden {
|
|
133
|
+
display: none;
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
136
|
+
</head>
|
|
137
|
+
|
|
138
|
+
<body>
|
|
139
|
+
<div class="card">
|
|
140
|
+
<h2>忘记密码</h2>
|
|
141
|
+
<form onsubmit="return false;">
|
|
142
|
+
<p>请输入您的邮箱,我们将发送重置链接</p>
|
|
143
|
+
<div class="form-group">
|
|
144
|
+
<input type="email" id="email" placeholder="邮箱地址" autofocus autocomplete="email" maxlength="100">
|
|
145
|
+
</div>
|
|
146
|
+
<button class="btn" id="sendBtn">发送重置邮件</button>
|
|
147
|
+
</form>
|
|
148
|
+
<div id="message" class="error"></div>
|
|
149
|
+
<div id="manualLink" class="links hidden">
|
|
150
|
+
<a href="/login">返回登录</a>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- 公共逻辑 -->
|
|
155
|
+
<script src="/static/themeModule.js" defer></script><!-- 引入主题自适应模块 -->
|
|
156
|
+
<script src="/static/mouseOrTouch.js" defer></script><!-- 引入鼠标或触摸操作 -->
|
|
157
|
+
<script src="/static/themeImg.js" defer></script> <!-- 引入主题图标模块 -->
|
|
158
|
+
<script src="/static/topImg.js" defer></script> <!-- 引入返回顶部图标模块 -->
|
|
159
|
+
<script>
|
|
160
|
+
// ==================== 按钮状态管理 ====================
|
|
161
|
+
let pollInterval = null, resetTimeout = null, resetEmail = null;
|
|
162
|
+
const [sendBtn, emailEl, messageDiv, manualLink] = ['sendBtn', 'email', 'message', 'manualLink']
|
|
163
|
+
.map(id => document.getElementById(id)),
|
|
164
|
+
handleError = msg => {
|
|
165
|
+
messageDiv.className = 'error', messageDiv.textContent = msg, sendBtn.disabled = false;
|
|
166
|
+
},
|
|
167
|
+
// 停止所有轮询和超时定时器
|
|
168
|
+
stopPolling = () => {
|
|
169
|
+
clearInterval(pollInterval), clearTimeout(resetTimeout), pollInterval = null, resetTimeout = null;
|
|
170
|
+
},
|
|
171
|
+
// 开始轮询检查密码是否已重置(并设置5分钟超时)
|
|
172
|
+
startResetCheckPolling = email => {
|
|
173
|
+
resetEmail = email, stopPolling();
|
|
174
|
+
pollInterval = setInterval(async () => {
|
|
175
|
+
try {
|
|
176
|
+
const res = await fetch(`/api/check-password-reset?email=${encodeURIComponent(email)}`);
|
|
177
|
+
if (res.ok) {
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
if (data.reset) stopPolling(), window.location.href = '/login';
|
|
180
|
+
}
|
|
181
|
+
else console.error('检查重置状态失败');
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error('轮询检查密码重置状态失败', err);
|
|
184
|
+
}
|
|
185
|
+
}, 2000);
|
|
186
|
+
resetTimeout = setTimeout(() => {
|
|
187
|
+
if (pollInterval) {
|
|
188
|
+
stopPolling(), sendBtn.disabled = false, messageDiv.className = 'error';
|
|
189
|
+
messageDiv.textContent = '重置链接已过期,请重新发送';
|
|
190
|
+
}
|
|
191
|
+
}, 900000);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 监听发送按钮点击
|
|
195
|
+
sendBtn.addEventListener('click', async () => {
|
|
196
|
+
sendBtn.disabled = true, stopPolling(), manualLink.classList.add('hidden'), messageDiv.textContent = '';
|
|
197
|
+
const email = emailEl.value.trim();
|
|
198
|
+
|
|
199
|
+
if (!email) return handleError('请输入邮箱');
|
|
200
|
+
try {
|
|
201
|
+
const response = await fetch('/api/forgot-password', {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: { 'Content-Type': 'application/json' },
|
|
204
|
+
body: JSON.stringify({ email })
|
|
205
|
+
}), data = await response.json();
|
|
206
|
+
|
|
207
|
+
if (response.ok) {
|
|
208
|
+
messageDiv.className = 'success', messageDiv.textContent = data.message;
|
|
209
|
+
manualLink.classList.remove('hidden'), startResetCheckPolling(email);
|
|
210
|
+
}
|
|
211
|
+
else handleError(data.message);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
handleError('网络错误,请稍后重试'), stopPolling();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
emailEl.addEventListener('keypress', e => {
|
|
218
|
+
if (e.key === 'Enter') sendBtn.click();
|
|
219
|
+
});
|
|
220
|
+
emailEl.addEventListener('input', () => messageDiv.textContent = '');
|
|
221
|
+
window.addEventListener('beforeunload', stopPolling);
|
|
222
|
+
window.addEventListener('load', () => sendBtn.disabled = false);
|
|
223
|
+
</script>
|
|
224
|
+
</body>
|
|
225
|
+
|
|
226
|
+
</html>
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<link rel="icon" type="image/x-icon" href="/static/img/favicon.ico" />
|
|
8
|
+
<link rel="shortcut icon" type="image/x-icon" href="/static/img/favicon.ico" />
|
|
9
|
+
<title>登录页</title>
|
|
10
|
+
|
|
11
|
+
<!-- 引入公共主题变量与全局样式 -->
|
|
12
|
+
<link rel="stylesheet" href="/static/constants.css" /> <!-- 样式常量 -->
|
|
13
|
+
<link rel="stylesheet" href="/static/public.css" /> <!-- 公共样式 -->
|
|
14
|
+
<link rel="stylesheet" href="/static/themeImg.css" /> <!-- 主题图标 -->
|
|
15
|
+
<link rel="stylesheet" href="/static/topImg.css" /> <!-- 返回顶部图标 -->
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
/* body 布局调整:居中卡片 */
|
|
19
|
+
body {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
padding: 20px;
|
|
24
|
+
background: var(--body-bg);
|
|
25
|
+
background-color: var(--bg-color);
|
|
26
|
+
min-height: 100vh;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* 卡片容器 */
|
|
30
|
+
.card {
|
|
31
|
+
background: var(--container-bg);
|
|
32
|
+
border-radius: 12px;
|
|
33
|
+
box-shadow: 0 20px 40px var(--content-shadow);
|
|
34
|
+
width: 100%;
|
|
35
|
+
max-width: 400px;
|
|
36
|
+
padding: 40px 30px;
|
|
37
|
+
transition: background 0.5s ease, box-shadow 0.5s ease;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.card h2 {
|
|
41
|
+
color: var(--h1-color);
|
|
42
|
+
margin-bottom: 30px;
|
|
43
|
+
text-align: center;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
font-size: 28px;
|
|
46
|
+
border-bottom: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.form-group {
|
|
50
|
+
margin-bottom: 20px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.form-group label {
|
|
54
|
+
display: block;
|
|
55
|
+
margin-bottom: 8px;
|
|
56
|
+
color: var(--text-color);
|
|
57
|
+
font-weight: 500;
|
|
58
|
+
font-size: 14px;
|
|
59
|
+
opacity: 0.85;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.form-group input {
|
|
63
|
+
width: 100%;
|
|
64
|
+
padding: 12px 16px;
|
|
65
|
+
border: 1px solid var(--content-border);
|
|
66
|
+
border-radius: 8px;
|
|
67
|
+
font-size: 16px;
|
|
68
|
+
background: var(--li-bg);
|
|
69
|
+
color: var(--text-color);
|
|
70
|
+
transition: border-color 0.2s, background 0.5s ease, color 0.5s ease;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.form-group input:focus {
|
|
74
|
+
border-color: var(--link-color);
|
|
75
|
+
outline: none;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.btn {
|
|
79
|
+
width: 100%;
|
|
80
|
+
padding: 14px;
|
|
81
|
+
background: var(--btn-bg);
|
|
82
|
+
color: var(--text-color);
|
|
83
|
+
border: none;
|
|
84
|
+
border-radius: 8px;
|
|
85
|
+
font-size: 16px;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
box-shadow: 0 4px 12px var(--content-shadow);
|
|
89
|
+
transition: background 0.3s ease, transform 0.2s ease;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.btn:hover {
|
|
93
|
+
background: var(--btn-hover);
|
|
94
|
+
transform: translateY(-2px);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.btn:disabled {
|
|
98
|
+
background: #b0b0b0;
|
|
99
|
+
cursor: not-allowed;
|
|
100
|
+
opacity: 0.65;
|
|
101
|
+
transform: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.error {
|
|
105
|
+
color: #e53e3e;
|
|
106
|
+
font-size: 14px;
|
|
107
|
+
margin-top: 10px;
|
|
108
|
+
text-align: center;
|
|
109
|
+
min-height: 20px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.links {
|
|
113
|
+
margin-top: 25px;
|
|
114
|
+
text-align: center;
|
|
115
|
+
font-size: 14px;
|
|
116
|
+
color: var(--text-color);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.links a {
|
|
120
|
+
color: var(--link-color);
|
|
121
|
+
text-decoration: none;
|
|
122
|
+
margin: 0 10px;
|
|
123
|
+
transition: color 0.2s;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.links a:hover {
|
|
127
|
+
color: var(--link-hover);
|
|
128
|
+
text-decoration: underline;
|
|
129
|
+
}
|
|
130
|
+
</style>
|
|
131
|
+
</head>
|
|
132
|
+
|
|
133
|
+
<body>
|
|
134
|
+
<div class="card">
|
|
135
|
+
<h2>登录</h2>
|
|
136
|
+
<form onsubmit="return false;">
|
|
137
|
+
<div class="form-group">
|
|
138
|
+
<label for="username">用户名 / 邮箱</label>
|
|
139
|
+
<input type="text" id="username" placeholder="请输入用户名或邮箱" maxlength="50" autofocus
|
|
140
|
+
autocomplete="username">
|
|
141
|
+
</div>
|
|
142
|
+
<div class="form-group">
|
|
143
|
+
<label for="password">密码</label>
|
|
144
|
+
<input type="password" id="password" placeholder="请输入密码" maxlength="72" autocomplete="current-password">
|
|
145
|
+
</div>
|
|
146
|
+
<button class="btn" id="loginBtn">登录</button>
|
|
147
|
+
</form>
|
|
148
|
+
<div class="error" id="message"></div>
|
|
149
|
+
<div class="links">
|
|
150
|
+
<a href="/forgot-password">忘记密码?</a>
|
|
151
|
+
<a href="/register">注册新账号</a>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<!-- 公共逻辑 -->
|
|
156
|
+
<script src="/static/themeModule.js" defer></script><!-- 引入主题自适应模块 -->
|
|
157
|
+
<script src="/static/mouseOrTouch.js" defer></script><!-- 引入鼠标或触摸操作 -->
|
|
158
|
+
<script src="/static/themeImg.js" defer></script> <!-- 引入主题图标模块 -->
|
|
159
|
+
<script src="/static/topImg.js" defer></script> <!-- 引入返回顶部图标模块 -->
|
|
160
|
+
<script src="/static/utils/browser13.js"></script>
|
|
161
|
+
<script>
|
|
162
|
+
const [usernameEl, passwordEl, messageDiv, loginBtn] = ['username', 'password', 'message', 'loginBtn']
|
|
163
|
+
.map(id => document.getElementById(id)),
|
|
164
|
+
handleError = msg => {
|
|
165
|
+
messageDiv.textContent = msg, loginBtn.disabled = false;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
loginBtn.addEventListener('click', async () => {
|
|
169
|
+
loginBtn.disabled = true, messageDiv.textContent = '';
|
|
170
|
+
const [username, password] = [usernameEl, passwordEl].map(input => input.value.trim());
|
|
171
|
+
if (!username || !password) return handleError('请输入用户名和密码');
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
// 密码验证
|
|
175
|
+
const response = await fetch('/api/login', {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: { 'Content-Type': 'application/json' },
|
|
178
|
+
body: JSON.stringify({ username, password })
|
|
179
|
+
}), data = await response.json();
|
|
180
|
+
|
|
181
|
+
if (!response.ok) return handleError(data.message);
|
|
182
|
+
// 硬件验证
|
|
183
|
+
if (data.requireWebAuthn) {
|
|
184
|
+
let platformAvailable = false;
|
|
185
|
+
if (window.PublicKeyCredential) {
|
|
186
|
+
try {
|
|
187
|
+
platformAvailable = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
188
|
+
} catch (err) { console.warn(err) };
|
|
189
|
+
}
|
|
190
|
+
if (!platformAvailable)
|
|
191
|
+
return handleError('当前环境不支持硬件验证,请使用HTTPS或localhost访问,并确保浏览器支持指纹/人脸功能;');
|
|
192
|
+
try {
|
|
193
|
+
// 获取 WebAuthn 登录选项
|
|
194
|
+
const beginRes = await fetch('/api/webauthn/login/begin', {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { 'Content-Type': 'application/json' },
|
|
197
|
+
body: JSON.stringify({ username })
|
|
198
|
+
}), options = await beginRes.json();
|
|
199
|
+
if (!beginRes.ok) return handleError(options.message);
|
|
200
|
+
|
|
201
|
+
// 调用浏览器硬件验证
|
|
202
|
+
const assertionResp = await flunWebAuthnBrowser.startAuthentication(options),
|
|
203
|
+
completeRes = await fetch('/api/webauthn/login/complete', {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: { 'Content-Type': 'application/json' },
|
|
206
|
+
body: JSON.stringify({ assertionResponse: assertionResp })
|
|
207
|
+
}), completeData = await completeRes.json();
|
|
208
|
+
|
|
209
|
+
if (completeRes.ok) window.location.href = '/';
|
|
210
|
+
else return handleError(completeData.message);
|
|
211
|
+
} catch (webauthnErr) {
|
|
212
|
+
let msg = webauthnErr.message;
|
|
213
|
+
if (webauthnErr.name === 'NotAllowedError') msg = '认证操作已取消';
|
|
214
|
+
handleError(msg);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (data.require2FA) window.location.href = '/2fa';
|
|
218
|
+
else if (data.success) window.location.href = '/';
|
|
219
|
+
else handleError(data.message);
|
|
220
|
+
} catch (err) { handleError('网络错误,请稍后重试') }
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
passwordEl.addEventListener('keypress', e => {
|
|
224
|
+
if (e.key === 'Enter') loginBtn.click();
|
|
225
|
+
});
|
|
226
|
+
[usernameEl, passwordEl].forEach(el => el.addEventListener('input', () => messageDiv.textContent = ''));
|
|
227
|
+
</script>
|
|
228
|
+
</body>
|
|
229
|
+
|
|
230
|
+
</html>
|