From 648cf0adf0d3ecd4365bd12e18485aa74f492464 Mon Sep 17 00:00:00 2001 From: yuyx <237899745@qq.com> Date: Thu, 11 Dec 2025 21:51:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=82=AE=E4=BB=B6?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E7=AC=AC=E4=BA=8C=E9=98=B6=E6=AE=B5=20-=20?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E9=82=AE=E7=AE=B1=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现注册时的邮箱验证功能: - 修改注册API支持邮箱验证流程 - 新增邮箱验证API (/api/verify-email/) - 新增重发验证邮件API (/api/resend-verify-email) - 新增邮箱验证状态查询API (/api/email/verify-status) 新增文件: - templates/email/register.html - 注册验证邮件模板 - templates/verify_success.html - 验证成功页面 - templates/verify_failed.html - 验证失败页面 修改文件: - email_service.py - 添加发送注册验证邮件函数 - app.py - 添加邮箱验证相关API - database.py - 添加get_user_by_email函数 - app_config.py - 添加BASE_URL配置 - templates/register.html - 支持邮箱必填切换 - templates/login.html - 添加重发验证邮件功能 - templates/admin.html - 添加注册验证开关和BASE_URL设置 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app.py | 140 +++++++++++++++++++++++++- app_config.py | 4 + database.py | 9 ++ email_service.py | 182 ++++++++++++++++++++++++++++++++-- templates/admin.html | 24 ++++- templates/email/register.html | 76 ++++++++++++++ templates/login.html | 101 ++++++++++++++++++- templates/register.html | 52 +++++++++- templates/verify_failed.html | 117 ++++++++++++++++++++++ templates/verify_success.html | 109 ++++++++++++++++++++ 10 files changed, 794 insertions(+), 20 deletions(-) create mode 100644 templates/email/register.html create mode 100644 templates/verify_failed.html create mode 100644 templates/verify_success.html diff --git a/app.py b/app.py index 9674f35..40f48d7 100755 --- a/app.py +++ b/app.py @@ -693,22 +693,55 @@ def register(): return jsonify({"error": "验证码错误次数过多,IP已被锁定1小时"}), 429 return jsonify({"error": message}), 400 + # 检查邮箱验证是否启用 + email_settings = email_service.get_email_settings() + email_verify_enabled = email_settings.get('register_verify_enabled', False) and email_settings.get('enabled', False) + + # 如果启用了邮箱验证,邮箱必填 + if email_verify_enabled and not email: + return jsonify({"error": "启用邮箱验证后,邮箱为必填项"}), 400 + + # 简单邮箱格式验证 + if email and '@' not in email: + return jsonify({"error": "邮箱格式不正确"}), 400 + # 获取自动审核配置 system_config = database.get_system_config() auto_approve_enabled = system_config.get('auto_approve_enabled', 0) == 1 auto_approve_hourly_limit = system_config.get('auto_approve_hourly_limit', 10) auto_approve_vip_days = system_config.get('auto_approve_vip_days', 7) - # 检查每小时注册限制 - if auto_approve_enabled: + # 检查每小时注册限制(同时适用于自动审核和邮箱验证) + if auto_approve_enabled or email_verify_enabled: hourly_count = database.get_hourly_registration_count() if hourly_count >= auto_approve_hourly_limit: return jsonify({"error": f"注册人数过多,请稍后再试(每小时限制{auto_approve_hourly_limit}人)"}), 429 user_id = database.create_user(username, password, email) if user_id: - # 自动审核处理 - if auto_approve_enabled: + # 优先级:邮箱验证 > 自动审核 > 手动审核 + if email_verify_enabled and email: + # 发送验证邮件 + result = email_service.send_register_verification_email( + email=email, + username=username, + user_id=user_id + ) + if result['success']: + return jsonify({ + "success": True, + "message": "注册成功!验证邮件已发送,请查收邮箱并点击链接完成验证", + "need_verify": True + }) + else: + # 邮件发送失败,但用户已创建,返回提示 + logger.error(f"注册验证邮件发送失败: {result['error']}") + return jsonify({ + "success": True, + "message": f"注册成功,但验证邮件发送失败({result['error']})。请稍后在登录页面重新发送验证邮件", + "need_verify": True + }) + elif auto_approve_enabled: # 自动审核通过 database.approve_user(user_id) # 赠送VIP天数 @@ -723,6 +756,96 @@ def register(): return jsonify({"error": "用户名已存在"}), 400 +@app.route('/api/verify-email/') +def verify_email(token): + """验证邮箱 - 用户点击邮件中的链接""" + result = email_service.verify_email_token(token, email_service.EMAIL_TYPE_REGISTER) + + if result: + user_id = result['user_id'] + email = result['email'] + + # 验证成功,激活用户 + database.approve_user(user_id) + + # 获取自动审核配置,检查是否赠送VIP + system_config = database.get_system_config() + auto_approve_vip_days = system_config.get('auto_approve_vip_days', 7) + if auto_approve_vip_days > 0: + database.set_user_vip(user_id, auto_approve_vip_days) + + logger.info(f"用户邮箱验���成功: user_id={user_id}, email={email}") + return render_template('verify_success.html') + else: + logger.warning(f"邮箱验证失败: token={token[:20]}...") + return render_template('verify_failed.html', error_message="验证链接无效或已过期,请重新注册或申请重发验证邮件") + + +@app.route('/api/resend-verify-email', methods=['POST']) +@require_ip_not_locked +def resend_verify_email(): + """重发验证邮件""" + 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(用于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 + + # 查找待验证的用户 + user = database.get_user_by_email(email) + if not user: + return jsonify({"error": "该邮箱未注册"}), 404 + + if user['status'] == 'approved': + return jsonify({"error": "该账号已验证通过,请直接登录"}), 400 + + # 发送验证邮件 + result = email_service.resend_register_verification_email( + user_id=user['id'], + email=email, + username=user['username'] + ) + + if result['success']: + return jsonify({"success": True, "message": "验证邮件已重新发送,请查收"}) + else: + return jsonify({"error": result['error']}), 500 + + +@app.route('/api/email/verify-status') +def get_email_verify_status(): + """获取邮箱验证功能状态(公开API)""" + try: + settings = email_service.get_email_settings() + return jsonify({ + 'email_enabled': settings.get('enabled', False), + 'register_verify_enabled': settings.get('register_verify_enabled', False) and settings.get('enabled', False) + }) + except Exception as e: + return jsonify({ + 'email_enabled': False, + 'register_verify_enabled': False + }) + + # ==================== 验证码API ==================== import random from task_checkpoint import get_checkpoint_manager, TaskStage @@ -3496,8 +3619,15 @@ def update_email_settings_api(): data = request.json enabled = data.get('enabled', False) failover_enabled = data.get('failover_enabled', True) + register_verify_enabled = data.get('register_verify_enabled') + base_url = data.get('base_url') - email_service.update_email_settings(enabled, failover_enabled) + email_service.update_email_settings( + enabled=enabled, + failover_enabled=failover_enabled, + register_verify_enabled=register_verify_enabled, + base_url=base_url + ) return jsonify({'success': True}) except Exception as e: logger.error(f"更新邮件设置失败: {e}") diff --git a/app_config.py b/app_config.py index 6c640d8..e56a837 100755 --- a/app_config.py +++ b/app_config.py @@ -126,6 +126,10 @@ class Config: # ==================== SocketIO配置 ==================== SOCKETIO_CORS_ALLOWED_ORIGINS = os.environ.get('SOCKETIO_CORS_ALLOWED_ORIGINS', '*') + # ==================== 网站基础URL配置 ==================== + # 用于生成邮件中的验证链接等 + BASE_URL = os.environ.get('BASE_URL', 'http://localhost:51233') + # ==================== 日志配置 ==================== # 安全修复: 生产环境默认使用INFO级别,避免泄露敏感调试信息 LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO') diff --git a/database.py b/database.py index 41fd05a..ecd992f 100755 --- a/database.py +++ b/database.py @@ -821,6 +821,15 @@ def get_user_by_username(username): return dict(user) if user else None +def get_user_by_email(email): + """根据邮箱获取用户""" + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM users WHERE email = ?', (email,)) + user = cursor.fetchone() + return dict(user) if user else None + + def get_all_users(): """获取所有用户""" with db_pool.get_db() as conn: diff --git a/email_service.py b/email_service.py index 2a1cf39..6882fac 100644 --- a/email_service.py +++ b/email_service.py @@ -107,6 +107,8 @@ def init_email_tables(): id INTEGER PRIMARY KEY DEFAULT 1, enabled INTEGER DEFAULT 0, failover_enabled INTEGER DEFAULT 1, + register_verify_enabled INTEGER DEFAULT 0, + base_url TEXT DEFAULT '', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) @@ -188,26 +190,67 @@ def get_email_settings() -> Dict[str, Any]: """获取全局邮件设置""" with db_pool.get_db() as conn: cursor = conn.cursor() - cursor.execute("SELECT enabled, failover_enabled, updated_at FROM email_settings WHERE id = 1") + # 先检查表结构,添加新字段(兼容旧版本数据库) + try: + cursor.execute("SELECT register_verify_enabled FROM email_settings LIMIT 1") + except: + cursor.execute("ALTER TABLE email_settings ADD COLUMN register_verify_enabled INTEGER DEFAULT 0") + conn.commit() + try: + cursor.execute("SELECT base_url FROM email_settings LIMIT 1") + except: + cursor.execute("ALTER TABLE email_settings ADD COLUMN base_url TEXT DEFAULT ''") + conn.commit() + + cursor.execute(""" + SELECT enabled, failover_enabled, register_verify_enabled, base_url, updated_at + FROM email_settings WHERE id = 1 + """) row = cursor.fetchone() if row: return { 'enabled': bool(row[0]), 'failover_enabled': bool(row[1]), - 'updated_at': row[2] + 'register_verify_enabled': bool(row[2]) if row[2] is not None else False, + 'base_url': row[3] or '', + 'updated_at': row[4] } - return {'enabled': False, 'failover_enabled': True, 'updated_at': None} + return { + 'enabled': False, + 'failover_enabled': True, + 'register_verify_enabled': False, + 'base_url': '', + 'updated_at': None + } -def update_email_settings(enabled: bool, failover_enabled: bool) -> bool: +def update_email_settings( + enabled: bool, + failover_enabled: bool, + register_verify_enabled: bool = None, + base_url: str = None +) -> bool: """更新全局邮件设置""" with db_pool.get_db() as conn: cursor = conn.cursor() - cursor.execute(""" + + # 构建动态更新语句 + updates = ['enabled = ?', 'failover_enabled = ?', 'updated_at = CURRENT_TIMESTAMP'] + params = [int(enabled), int(failover_enabled)] + + if register_verify_enabled is not None: + updates.append('register_verify_enabled = ?') + params.append(int(register_verify_enabled)) + + if base_url is not None: + updates.append('base_url = ?') + params.append(base_url) + + cursor.execute(f""" UPDATE email_settings - SET enabled = ?, failover_enabled = ?, updated_at = CURRENT_TIMESTAMP + SET {', '.join(updates)} WHERE id = 1 - """, (int(enabled), int(failover_enabled))) + """, params) conn.commit() return True @@ -1057,6 +1100,131 @@ def cleanup_expired_tokens() -> int: return deleted +# ============ 注册验证邮件 ============ + +def send_register_verification_email( + email: str, + username: str, + user_id: int, + base_url: str = None +) -> Dict[str, Any]: + """ + 发送注册验证邮件 + + Args: + email: 用户邮箱 + username: 用户名 + user_id: 用户ID + base_url: 网站基础URL + + Returns: + {'success': bool, 'error': str, 'token': str} + """ + # 检查发送频率限制 + if not check_rate_limit(email, EMAIL_TYPE_REGISTER): + return { + 'success': False, + 'error': '发送太频繁,请稍后再试', + 'token': None + } + + # 生成验证Token + token = generate_email_token(email, EMAIL_TYPE_REGISTER, user_id) + + # 获取base_url + if not base_url: + settings = get_email_settings() + base_url = settings.get('base_url', '') + + if not base_url: + # 尝试从配置获取 + try: + from app_config import Config + base_url = Config.BASE_URL + except: + base_url = 'http://localhost:51233' + + # 生成验证链接 + verify_url = f"{base_url.rstrip('/')}/api/verify-email/{token}" + + # 读取邮件模板 + template_path = os.path.join(os.path.dirname(__file__), 'templates', 'email', 'register.html') + try: + with open(template_path, 'r', encoding='utf-8') as f: + html_template = f.read() + except FileNotFoundError: + # 使用简单的HTML模板 + html_template = """ + + +

邮箱验证

+

您好,{{ username }}!

+

请点击下面的链接验证您的邮箱地址:

+

{{ verify_url }}

+

此链接24小时内有效。

+ + + """ + + # 替换模板变量 + html_body = html_template.replace('{{ username }}', username) + html_body = html_body.replace('{{ verify_url }}', verify_url) + + # 纯文本版本 + text_body = f""" +您好,{username}! + +感谢您注册知识管理平台。请点击下面的链接验证您的邮箱地址: + +{verify_url} + +此链接24小时内有效。 + +如果您没有注册过账号,请忽略此邮件。 +""" + + # 发送邮件 + result = send_email( + to_email=email, + subject='【知识管理平台】邮箱验证', + body=text_body, + html_body=html_body, + email_type=EMAIL_TYPE_REGISTER, + user_id=user_id + ) + + if result['success']: + return {'success': True, 'error': '', 'token': token} + else: + return {'success': False, 'error': result['error'], 'token': None} + + +def resend_register_verification_email(user_id: int, email: str, username: str) -> Dict[str, Any]: + """ + 重发注册验证邮件 + + Args: + user_id: 用户ID + email: 用户邮箱 + username: 用户名 + + Returns: + {'success': bool, 'error': str} + """ + # 检查是否有未过期的token + with db_pool.get_db() as conn: + cursor = conn.cursor() + # 先使旧token失效 + cursor.execute(""" + UPDATE email_tokens SET used = 1 + WHERE user_id = ? AND token_type = ? AND used = 0 + """, (user_id, EMAIL_TYPE_REGISTER)) + conn.commit() + + # 发送新的验证邮件 + return send_register_verification_email(email, username, user_id) + + # ============ 异步发送队列 ============ class EmailQueue: diff --git a/templates/admin.html b/templates/admin.html index e6406be..a408858 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -1215,7 +1215,7 @@ 开启后,系统将支持邮箱验证、密码重置邮件、任务完成通知等功能 -
+
+
+ +
+ 开启后,新用户注册需通过邮箱验证才能激活账号(优先级高于自动审核) +
+
+
+ + +
+ 用于生成邮件中的验证链接,留空则使用默认配置 +
+
@@ -2780,6 +2796,8 @@ const data = await response.json(); document.getElementById('emailEnabled').checked = data.enabled; document.getElementById('failoverEnabled').checked = data.failover_enabled; + document.getElementById('registerVerifyEnabled').checked = data.register_verify_enabled || false; + document.getElementById('baseUrl').value = data.base_url || ''; } } catch (error) { console.error('加载邮件设置失败:', error); @@ -2794,7 +2812,9 @@ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: document.getElementById('emailEnabled').checked, - failover_enabled: document.getElementById('failoverEnabled').checked + failover_enabled: document.getElementById('failoverEnabled').checked, + register_verify_enabled: document.getElementById('registerVerifyEnabled').checked, + base_url: document.getElementById('baseUrl').value.trim() }) }); diff --git a/templates/email/register.html b/templates/email/register.html new file mode 100644 index 0000000..3bbc2e0 --- /dev/null +++ b/templates/email/register.html @@ -0,0 +1,76 @@ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+

知识管理平台

+

账号注册验证

+
+

+ 您好,{{ username }}! +

+

+ 感谢您注册知识管理平台。请点击下方按钮验证您的邮箱地址,完成账号激活。 +

+ + + + + + +
+ + 验证邮箱 + +
+ +

+ 如果按钮无法点击,请复制以下链接到浏览器打开: +

+

+ {{ verify_url }} +

+ +
+

+ 注意:此链接有效期为 24 小时,过期后需要重新注册。 +

+
+ +

+ 如果您没有注册过账号,请忽略此邮件。 +

+
+

+ 此邮件由系统自动发送,请勿直接回复。 +

+

+ © 知识管理平台 +

+
+
+ + diff --git a/templates/login.html b/templates/login.html index 00c4b55..14ea6e0 100644 --- a/templates/login.html +++ b/templates/login.html @@ -189,7 +189,10 @@ - + @@ -213,9 +216,49 @@ + + \ No newline at end of file diff --git a/templates/register.html b/templates/register.html index 6edb74a..bc4a4df 100644 --- a/templates/register.html +++ b/templates/register.html @@ -173,9 +173,9 @@
- + - 选填,用于接收审核通知 + 选填,用于接收审核通知
@@ -196,7 +196,29 @@ diff --git a/templates/verify_failed.html b/templates/verify_failed.html new file mode 100644 index 0000000..d3d4b22 --- /dev/null +++ b/templates/verify_failed.html @@ -0,0 +1,117 @@ + + + + + + 验证失败 - 知识管理平台 + + + +
+
+ + + +
+

验证失败

+

+ 很抱歉,邮箱验证未能成功。 +

+
+ {{ error_message }} +
+ +
+ + diff --git a/templates/verify_success.html b/templates/verify_success.html new file mode 100644 index 0000000..f5fcc58 --- /dev/null +++ b/templates/verify_success.html @@ -0,0 +1,109 @@ + + + + + + 邮箱验证成功 - 知识管理平台 + + + +
+
+ + + +
+

验证成功

+

+ 您的邮箱已验证成功!
+ 账号已激活,现在可以登录使用了。 +

+ 立即登录 +

+ 5 秒后自动跳转到登录页面... +

+
+ + + +