feat: harden account security flows

This commit is contained in:
2026-08-04 14:29:51 +08:00
parent 630252e2ff
commit c71b1351d6
14 changed files with 892 additions and 56 deletions
+120 -1
View File
@@ -1 +1,120 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{ title }}</title><link href="{{ url_for('static', filename='css/app.css') }}" rel="stylesheet"></head><body class="min-h-screen bg-page"><main class="mx-auto flex min-h-screen max-w-md items-center px-5"><section class="w-full rounded-2xl border border-line bg-white p-7 shadow-panel"><h1 class="text-2xl font-extrabold">{{ title }}</h1><p class="mt-2 text-sm text-textSub">验证码已发送至 {{ email }},10 分钟内有效。</p>{% with messages=get_flashed_messages(with_categories=true) %}{% for c,m in messages %}<p class="mt-4 text-sm text-dangerText">{{ m }}</p>{% endfor %}{% endwith %}<form method="post" class="mt-6 space-y-4"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><label class="block text-sm font-semibold">6 位验证码<input name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required autofocus class="mt-1 w-full rounded-lg border border-line px-3 py-2 text-center text-xl tracking-[.5em]"></label><button class="ui-btn ui-btn-primary w-full">验证</button></form><form method="post" action="{{ url_for('main.resend_code', purpose=purpose) }}" class="mt-3"><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="text-sm font-semibold text-primary">重新发送验证码</button></form></section></main></body></html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }} | 供水管道健康评估系统</title>
<link href="{{ url_for('static', filename='css/app.css') }}" rel="stylesheet">
</head>
<body class="min-h-screen bg-page text-textMain">
<main class="mx-auto flex min-h-screen max-w-md items-center px-5">
<section class="w-full rounded-2xl border border-line bg-white p-7 shadow-panel">
<span class="material-symbols-outlined text-4xl text-primary">mark_email_read</span>
<h1 class="mt-3 text-2xl font-extrabold">{{ title }}</h1>
<p class="mt-2 text-sm leading-6 text-textSub">验证码已发送至 {{ email }},10 分钟内有效。</p>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<p class="mt-4 rounded-lg bg-dangerSoft p-3 text-sm text-dangerText">{{ message }}</p>
{% endfor %}
{% endwith %}
<form method="post" class="mt-7" id="verificationForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input id="verificationCode" type="hidden" name="code" value="">
<fieldset>
<legend class="text-sm font-semibold">请输入 6 位验证码</legend>
<div class="mt-3 grid grid-cols-6 gap-2" id="codeInputs">
{% for index in range(6) %}
<input
type="text"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="1"
aria-label="验证码第 {{ index + 1 }} 位"
class="h-12 min-w-0 rounded-lg border border-line text-center text-xl font-bold tracking-wide focus:border-primary focus:ring-primary"
data-code-digit
{% if index == 0 %}autofocus{% endif %}
>
{% endfor %}
</div>
</fieldset>
<button id="verifyButton" class="ui-btn ui-btn-primary mt-6 w-full" type="submit">验证并继续</button>
</form>
<form method="post" action="{{ url_for('main.resend_code', purpose=purpose) }}" class="mt-4 text-center">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button id="resendButton" class="text-sm font-semibold text-primary disabled:cursor-not-allowed disabled:text-slate-400" type="submit" disabled>
<span id="resendText">{{ resend_seconds }} 秒后可重新发送</span>
</button>
</form>
</section>
</main>
<script>
(() => {
const inputs = Array.from(document.querySelectorAll('[data-code-digit]'));
const hiddenCode = document.getElementById('verificationCode');
const form = document.getElementById('verificationForm');
const resendButton = document.getElementById('resendButton');
const resendText = document.getElementById('resendText');
let remainingSeconds = {{ resend_seconds|int }};
function updateCode() {
hiddenCode.value = inputs.map((input) => input.value).join('');
}
function fillDigits(value, startIndex = 0) {
const digits = value.replace(/\D/g, '').slice(0, inputs.length - startIndex);
for (let index = 0; index < digits.length; index += 1) {
inputs[startIndex + index].value = digits[index];
}
updateCode();
const nextIndex = Math.min(startIndex + digits.length, inputs.length - 1);
inputs[nextIndex].focus();
}
inputs.forEach((input, index) => {
input.addEventListener('input', () => {
if (input.value.length > 1) {
fillDigits(input.value, index);
return;
}
input.value = input.value.replace(/\D/g, '');
updateCode();
if (input.value && index < inputs.length - 1) inputs[index + 1].focus();
});
input.addEventListener('keydown', (event) => {
if (event.key === 'Backspace' && !input.value && index > 0) {
inputs[index - 1].focus();
}
});
input.addEventListener('paste', (event) => {
event.preventDefault();
fillDigits(event.clipboardData.getData('text'), index);
});
});
form.addEventListener('submit', (event) => {
updateCode();
if (hiddenCode.value.length !== inputs.length) {
event.preventDefault();
inputs.find((input) => !input.value)?.focus();
}
});
const timer = window.setInterval(() => {
remainingSeconds -= 1;
if (remainingSeconds <= 0) {
window.clearInterval(timer);
resendButton.disabled = false;
resendText.textContent = '重新发送验证码';
return;
}
resendText.textContent = `${remainingSeconds} 秒后可重新发送`;
}, 1000);
})();
</script>
</body>
</html>