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>
This commit is contained in:
2025-12-11 21:58:49 +08:00
parent 648cf0adf0
commit 0c0a5a7770
5 changed files with 691 additions and 4 deletions

95
app.py
View File

@@ -846,6 +846,101 @@ def get_email_verify_status():
})
# ==================== 密码重置邮件方式API ====================
@app.route('/api/forgot-password', methods=['POST'])
@require_ip_not_locked
def forgot_password():
"""发送密码重置邮件"""
data = request.json
email = data.get('email', '').strip()
captcha_session = data.get('captcha_session', '')
captcha_code = data.get('captcha', '').strip()
if not email:
return jsonify({"error": "请输入邮箱"}), 400
# 获取客户端IP
client_ip = get_client_ip()
# 检查IP限流
allowed, error_msg = check_ip_rate_limit(client_ip)
if not allowed:
return jsonify({"error": error_msg}), 429
# 验证验证码
success, message = verify_and_consume_captcha(captcha_session, captcha_code, captcha_storage, MAX_CAPTCHA_ATTEMPTS)
if not success:
is_locked = record_failed_captcha(client_ip)
if is_locked:
return jsonify({"error": "验证码错误次数过多,IP已被锁定1小时"}), 429
return jsonify({"error": message}), 400
# 检查邮件功能是否启用
email_settings = email_service.get_email_settings()
if not email_settings.get('enabled', False):
return jsonify({"error": "邮件功能未启用,请联系管理员"}), 400
# 查找用户(防止用户枚举,统一返回成功消息)
user = database.get_user_by_email(email)
if user and user.get('status') == 'approved':
# 发送重置邮件
result = email_service.send_password_reset_email(
email=email,
username=user['username'],
user_id=user['id']
)
if not result['success']:
logger.error(f"密码重置邮件发送失败: {result['error']}")
# 即使失败也返回统一消息,防止信息泄露
# 统一返回成功消息
return jsonify({
"success": True,
"message": "如果该邮箱已注册,您将收到密码重置邮件"
})
@app.route('/reset-password/<token>')
def reset_password_page(token):
"""密码重置页面"""
result = email_service.verify_password_reset_token(token)
if result:
return render_template('reset_password.html', token=token, valid=True, error_message='')
else:
return render_template('reset_password.html', token=token, valid=False,
error_message='重置链接无效或已过期,请重新申请密码重置')
@app.route('/api/reset-password-confirm', methods=['POST'])
def reset_password_confirm():
"""确认密码重置"""
data = request.json
token = data.get('token', '').strip()
new_password = data.get('new_password', '').strip()
if not token or not new_password:
return jsonify({"error": "参数不完整"}), 400
# 验证密码强度
is_valid, error_msg = validate_password(new_password)
if not is_valid:
return jsonify({"error": error_msg}), 400
# 验证并消费token
result = email_service.confirm_password_reset(token)
if not result:
return jsonify({"error": "重置链接无效或已过期"}), 400
# 更新用户密码
user_id = result['user_id']
if database.admin_reset_user_password(user_id, new_password):
logger.info(f"用户密码重置成功: user_id={user_id}")
return jsonify({"success": True, "message": "密码重置成功"})
else:
return jsonify({"error": "密码重置失败"}), 500
# ==================== 验证码API ====================
import random
from task_checkpoint import get_checkpoint_manager, TaskStage