Files
zsglpt/templates/reset_password.html
yuyx 0c0a5a7770 feat: 添加邮件功能第三阶段 - 密码重置
实现通过邮件自助重置密码功能:
- 新增发送密码重置邮件API (/api/forgot-password)
- 新增密码重置页面路由 (/reset-password/<token>)
- 新增确认密码重置API (/api/reset-password-confirm)

新增文件:
- templates/email/reset_password.html - 密码重置邮件模板
- templates/reset_password.html - 密码重置页面

修改文件:
- email_service.py - 添加密码重置相关函数
  - send_password_reset_email()
  - verify_password_reset_token()
  - confirm_password_reset()
- app.py - 添加密码重置相关API
- templates/login.html - 忘记密码支持两种方式:
  - 启用邮件功能:通过邮件自助重置
  - 未启用邮件:提交申请等待管理员审核

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 21:58:49 +08:00

267 lines
8.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>重置密码 - 知识管理平台</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.card {
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 40px;
text-align: center;
max-width: 450px;
width: 100%;
}
.icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #e74c3c, #c0392b);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 25px;
}
.icon svg {
width: 40px;
height: 40px;
fill: white;
}
h1 {
color: #333;
font-size: 24px;
margin-bottom: 10px;
}
p {
color: #666;
font-size: 14px;
line-height: 1.6;
margin-bottom: 25px;
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: bold;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.form-group small {
color: #999;
font-size: 12px;
margin-top: 5px;
display: block;
}
.btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 30px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.message {
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
display: none;
}
.message.error {
background: #ffe6e6;
color: #d63031;
border: 1px solid #ffcdd2;
}
.message.success {
background: #e6ffe6;
color: #27ae60;
border: 1px solid #c8e6c9;
}
.back-link {
margin-top: 20px;
}
.back-link a {
color: #667eea;
text-decoration: none;
font-size: 14px;
}
.back-link a:hover {
text-decoration: underline;
}
.expired {
display: none;
}
.expired .icon {
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
}
@media (max-width: 480px) {
body { padding: 12px; }
.card { padding: 30px 20px; }
h1 { font-size: 20px; }
}
</style>
</head>
<body>
<div class="card" id="resetForm">
<div class="icon">
<svg viewBox="0 0 24 24">
<path d="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/>
</svg>
</div>
<h1>重置密码</h1>
<p>请输入您的新密码</p>
<div id="errorMessage" class="message error"></div>
<div id="successMessage" class="message success"></div>
<form onsubmit="handleResetPassword(event)">
<div class="form-group">
<label for="newPassword">新密码</label>
<input type="password" id="newPassword" placeholder="请输入新密码" required minlength="8">
<small>至少8位包含字母和数字</small>
</div>
<div class="form-group">
<label for="confirmPassword">确认密码</label>
<input type="password" id="confirmPassword" placeholder="请再次输入新密码" required>
</div>
<button type="submit" class="btn" id="submitBtn">确认重置</button>
</form>
<div class="back-link">
<a href="/login">返回登录</a>
</div>
</div>
<div class="card expired" id="expiredCard">
<div class="icon">
<svg viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
</div>
<h1>链接已失效</h1>
<p>{{ error_message }}</p>
<div class="back-link">
<a href="/login">返回登录</a>
</div>
</div>
<script>
const token = '{{ token }}';
const isValid = {{ 'true' if valid else 'false' }};
if (!isValid) {
document.getElementById('resetForm').style.display = 'none';
document.getElementById('expiredCard').style.display = 'block';
}
async function handleResetPassword(event) {
event.preventDefault();
const newPassword = document.getElementById('newPassword').value;
const confirmPassword = document.getElementById('confirmPassword').value;
const errorDiv = document.getElementById('errorMessage');
const successDiv = document.getElementById('successMessage');
const submitBtn = document.getElementById('submitBtn');
errorDiv.style.display = 'none';
successDiv.style.display = 'none';
// 验证密码
if (newPassword.length < 8) {
errorDiv.textContent = '密码长度至少8位';
errorDiv.style.display = 'block';
return;
}
if (!/[a-zA-Z]/.test(newPassword) || !/\d/.test(newPassword)) {
errorDiv.textContent = '密码必须包含字母和数字';
errorDiv.style.display = 'block';
return;
}
if (newPassword !== confirmPassword) {
errorDiv.textContent = '两次输入的密码不一致';
errorDiv.style.display = 'block';
return;
}
submitBtn.disabled = true;
submitBtn.textContent = '处理中...';
try {
const response = await fetch('/api/reset-password-confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, new_password: newPassword })
});
const data = await response.json();
if (response.ok) {
successDiv.textContent = '密码重置成功3秒后跳转到登录页面...';
successDiv.style.display = 'block';
setTimeout(() => {
window.location.href = '/login';
}, 3000);
} else {
errorDiv.textContent = data.error || '重置失败';
errorDiv.style.display = 'block';
submitBtn.disabled = false;
submitBtn.textContent = '确认重置';
}
} catch (error) {
errorDiv.textContent = '网络错误,请稍后重试';
errorDiv.style.display = 'block';
submitBtn.disabled = false;
submitBtn.textContent = '确认重置';
}
}
</script>
</body>
</html>