feat: v3.1 - 用户密码管理和邮箱验证功能
新增功能: 1. 修改密码功能 - 用户可以修改自己的密码 - 需要验证旧密码 - 新密码至少6位且不能与旧密码相同 2. 邮箱绑定功能 - 用户可以绑定/修改邮箱 - 邮箱格式验证和唯一性检查 - 修改邮箱后需要重新验证 3. 邮箱验证功能 - 发送验证邮件(24小时有效) - 点击邮件链接完成验证 - 验证状态显示 技术实现: - 新增4个数据库字段(email_verified等) - 封装邮件发送工具(utils/email_sender.py) - 新增5个API接口 - 新增修改密码页面 - 集成邮箱管理到个人中心 文件变更: - 修改:app.py, models.py, base_new.html, profile.html - 新增:change_password.html, email_sender.py, migrate_email_verification.py - 文档:server-update.md, SERVER_RESTART_GUIDE.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
241
app.py
241
app.py
@@ -2,17 +2,19 @@ import os
|
||||
import markdown
|
||||
import random
|
||||
import string
|
||||
import secrets
|
||||
from io import BytesIO
|
||||
from flask import Flask, render_template, redirect, url_for, request, flash, jsonify, session, send_file
|
||||
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
||||
from flask_admin import Admin, AdminIndexView, expose
|
||||
from flask_admin.contrib.sqla import ModelView
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from config import config
|
||||
from models import db, Site, Tag, Admin as AdminModel, News, site_tags, PromptTemplate, User, Folder, Collection
|
||||
from utils.website_fetcher import WebsiteFetcher
|
||||
from utils.tag_generator import TagGenerator
|
||||
from utils.news_searcher import NewsSearcher
|
||||
from utils.email_sender import EmailSender
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
def create_app(config_name='default'):
|
||||
@@ -1130,6 +1132,16 @@ def create_app(config_name='default'):
|
||||
folders_count=folders_count,
|
||||
recent_collections=recent_collections)
|
||||
|
||||
@app.route('/user/change-password')
|
||||
@login_required
|
||||
def user_change_password_page():
|
||||
"""修改密码页面"""
|
||||
if not isinstance(current_user, User):
|
||||
flash('仅普通用户可访问', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return render_template('user/change_password.html')
|
||||
|
||||
@app.route('/user/collections')
|
||||
@login_required
|
||||
def user_collections():
|
||||
@@ -1201,6 +1213,233 @@ def create_app(config_name='default'):
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'更新失败:{str(e)}'}), 500
|
||||
|
||||
@app.route('/api/user/change-password', methods=['PUT'])
|
||||
@login_required
|
||||
def user_change_password():
|
||||
"""普通用户修改密码"""
|
||||
if not isinstance(current_user, User):
|
||||
return jsonify({'success': False, 'message': '仅普通用户可访问'}), 403
|
||||
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
old_password = data.get('old_password', '').strip()
|
||||
new_password = data.get('new_password', '').strip()
|
||||
confirm_password = data.get('confirm_password', '').strip()
|
||||
|
||||
# 验证旧密码
|
||||
if not old_password:
|
||||
return jsonify({'success': False, 'message': '请输入旧密码'}), 400
|
||||
|
||||
if not current_user.check_password(old_password):
|
||||
return jsonify({'success': False, 'message': '旧密码错误'}), 400
|
||||
|
||||
# 验证新密码
|
||||
if not new_password:
|
||||
return jsonify({'success': False, 'message': '请输入新密码'}), 400
|
||||
|
||||
if len(new_password) < 6:
|
||||
return jsonify({'success': False, 'message': '新密码长度至少6位'}), 400
|
||||
|
||||
if new_password != confirm_password:
|
||||
return jsonify({'success': False, 'message': '两次输入的新密码不一致'}), 400
|
||||
|
||||
if old_password == new_password:
|
||||
return jsonify({'success': False, 'message': '新密码不能与旧密码相同'}), 400
|
||||
|
||||
# 更新密码
|
||||
current_user.set_password(new_password)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '密码修改成功'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'修改失败:{str(e)}'}), 500
|
||||
|
||||
@app.route('/api/user/email', methods=['PUT'])
|
||||
@login_required
|
||||
def update_user_email():
|
||||
"""更新用户邮箱"""
|
||||
if not isinstance(current_user, User):
|
||||
return jsonify({'success': False, 'message': '仅普通用户可访问'}), 403
|
||||
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
email = data.get('email', '').strip()
|
||||
|
||||
# 验证邮箱格式
|
||||
if not email:
|
||||
return jsonify({'success': False, 'message': '请输入邮箱地址'}), 400
|
||||
|
||||
import re
|
||||
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
if not re.match(email_pattern, email):
|
||||
return jsonify({'success': False, 'message': '邮箱格式不正确'}), 400
|
||||
|
||||
# 检查邮箱是否已被其他用户使用
|
||||
existing_user = User.query.filter(
|
||||
User.email == email,
|
||||
User.id != current_user.id
|
||||
).first()
|
||||
|
||||
if existing_user:
|
||||
return jsonify({'success': False, 'message': '该邮箱已被其他用户使用'}), 400
|
||||
|
||||
# 更新邮箱
|
||||
current_user.email = email
|
||||
# 重置验证状态
|
||||
current_user.email_verified = False
|
||||
current_user.email_verified_at = None
|
||||
current_user.email_verify_token = None
|
||||
current_user.email_verify_token_expires = None
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '邮箱已更新,请验证新邮箱'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'更新失败:{str(e)}'}), 500
|
||||
|
||||
@app.route('/api/user/send-verify-email', methods=['POST'])
|
||||
@login_required
|
||||
def send_verify_email():
|
||||
"""发送邮箱验证邮件"""
|
||||
if not isinstance(current_user, User):
|
||||
return jsonify({'success': False, 'message': '仅普通用户可访问'}), 403
|
||||
|
||||
try:
|
||||
# 检查是否已绑定邮箱
|
||||
if not current_user.email:
|
||||
return jsonify({'success': False, 'message': '请先绑定邮箱'}), 400
|
||||
|
||||
# 检查是否已验证
|
||||
if current_user.email_verified:
|
||||
return jsonify({'success': False, 'message': '邮箱已验证,无需重复验证'}), 400
|
||||
|
||||
# 生成验证令牌(32位随机字符串)
|
||||
token = secrets.token_urlsafe(32)
|
||||
expires = datetime.now() + timedelta(hours=24)
|
||||
|
||||
# 保存令牌
|
||||
current_user.email_verify_token = token
|
||||
current_user.email_verify_token_expires = expires
|
||||
db.session.commit()
|
||||
|
||||
# 发送验证邮件
|
||||
verify_url = url_for('verify_email', token=token, _external=True)
|
||||
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
|
||||
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
|
||||
.header {{ background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }}
|
||||
.content {{ background: #f8fafc; padding: 30px; border-radius: 0 0 8px 8px; }}
|
||||
.button {{ display: inline-block; padding: 12px 30px; background: #0ea5e9; color: white; text-decoration: none; border-radius: 6px; margin: 20px 0; }}
|
||||
.footer {{ text-align: center; margin-top: 20px; color: #64748b; font-size: 12px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>验证您的邮箱</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>您好,{current_user.username}!</p>
|
||||
<p>感谢您注册 ZJPB。请点击下面的按钮验证您的邮箱地址:</p>
|
||||
<p style="text-align: center;">
|
||||
<a href="{verify_url}" class="button">验证邮箱</a>
|
||||
</p>
|
||||
<p>或复制以下链接到浏览器:</p>
|
||||
<p style="word-break: break-all; background: white; padding: 10px; border-radius: 4px;">{verify_url}</p>
|
||||
<p style="color: #64748b; font-size: 14px;">此链接将在24小时后失效。</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>如果您没有注册 ZJPB,请忽略此邮件。</p>
|
||||
<p>© 2025 ZJPB - 自己品吧</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
text_content = f"""
|
||||
验证您的邮箱
|
||||
|
||||
您好,{current_user.username}!
|
||||
|
||||
感谢您注册 ZJPB。请访问以下链接验证您的邮箱地址:
|
||||
|
||||
{verify_url}
|
||||
|
||||
此链接将在24小时后失效。
|
||||
|
||||
如果您没有注册 ZJPB,请忽略此邮件。
|
||||
"""
|
||||
|
||||
email_sender = EmailSender()
|
||||
success = email_sender.send_email(
|
||||
to_email=current_user.email,
|
||||
subject='验证您的邮箱 - ZJPB',
|
||||
html_content=html_content,
|
||||
text_content=text_content
|
||||
)
|
||||
|
||||
if success:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '验证邮件已发送,请查收'
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': '邮件发送失败,请稍后重试'
|
||||
}), 500
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'message': f'发送失败:{str(e)}'}), 500
|
||||
|
||||
@app.route('/verify-email/<token>')
|
||||
def verify_email(token):
|
||||
"""验证邮箱"""
|
||||
try:
|
||||
# 查找令牌对应的用户
|
||||
user = User.query.filter_by(email_verify_token=token).first()
|
||||
|
||||
if not user:
|
||||
flash('验证链接无效', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# 检查令牌是否过期
|
||||
if user.email_verify_token_expires < datetime.now():
|
||||
flash('验证链接已过期,请重新发送', 'error')
|
||||
return redirect(url_for('user_profile'))
|
||||
|
||||
# 验证成功
|
||||
user.email_verified = True
|
||||
user.email_verified_at = datetime.now()
|
||||
user.email_verify_token = None
|
||||
user.email_verify_token_expires = None
|
||||
db.session.commit()
|
||||
|
||||
flash('邮箱验证成功!', 'success')
|
||||
return redirect(url_for('user_profile'))
|
||||
|
||||
except Exception as e:
|
||||
flash(f'验证失败:{str(e)}', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/admin/change-password', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def change_password():
|
||||
|
||||
Reference in New Issue
Block a user