diff --git a/app.py b/app.py index 7a5d7c0..8e2bae7 100755 --- a/app.py +++ b/app.py @@ -2841,6 +2841,114 @@ def change_user_password(): return jsonify({"error": "密码更新失败"}), 500 +@app.route('/api/user/email', methods=['GET']) +@login_required +def get_user_email(): + """获取当前用户的邮箱信息""" + user = database.get_user_by_id(current_user.id) + if not user: + return jsonify({"error": "用户不存在"}), 404 + + return jsonify({ + "email": user.get('email', ''), + "email_verified": user.get('email_verified', False) + }) + + +@app.route('/api/user/bind-email', methods=['POST']) +@login_required +@require_ip_not_locked +def bind_user_email(): + """发送邮箱绑定验证邮件""" + data = request.get_json() + email = data.get('email', '').strip().lower() + + # 验证邮箱格式 + if not email or not validate_email(email): + return jsonify({"error": "请输入有效的邮箱地址"}), 400 + + # 检查邮件功能是否启用 + settings = email_service.get_email_settings() + if not settings.get('enabled', False): + return jsonify({"error": "邮件功能未启用,请联系管理员"}), 400 + + # 检查邮箱是否已被其他用户使用 + existing_user = database.get_user_by_email(email) + if existing_user and existing_user['id'] != current_user.id: + return jsonify({"error": "该邮箱已被其他用户绑定"}), 400 + + # 获取当前用户信息 + user = database.get_user_by_id(current_user.id) + if not user: + return jsonify({"error": "用户不存在"}), 404 + + # 如果已经绑定了相同邮箱且已验证,无需重复绑定 + if user.get('email') == email and user.get('email_verified'): + return jsonify({"error": "该邮箱已绑定并验证"}), 400 + + # 发送验证邮件 + result = email_service.send_bind_email_verification( + user_id=current_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/verify-bind-email/') +def verify_bind_email(token): + """验证邮箱绑定Token""" + result = email_service.verify_bind_email_token(token) + + if result: + user_id = result['user_id'] + email = result['email'] + + # 更新用户邮箱 + if database.update_user_email(user_id, email, verified=True): + # 返回成功页面 + return render_template('verify_success.html', + title='邮箱绑定成功', + message=f'邮箱 {email} 已成功绑定到您的账号!', + redirect_url='/' + ) + else: + return render_template('verify_failed.html', + title='绑定失败', + message='邮箱绑定失败,请重试' + ) + else: + return render_template('verify_failed.html', + title='链接无效', + message='验证链接已过期或无效,请重新发送验证邮件' + ) + + +@app.route('/api/user/unbind-email', methods=['POST']) +@login_required +def unbind_user_email(): + """解绑用户邮箱""" + user = database.get_user_by_id(current_user.id) + if not user: + return jsonify({"error": "用户不存在"}), 404 + + if not user.get('email'): + return jsonify({"error": "当前未绑定邮箱"}), 400 + + # 解绑邮箱 + if database.update_user_email(current_user.id, None, verified=False): + return jsonify({"success": True, "message": "邮箱已解绑"}) + else: + return jsonify({"error": "解绑失败"}), 500 + + @app.route('/api/run_stats', methods=['GET']) @login_required def get_run_stats(): diff --git a/database.py b/database.py index ecd992f..99a2bc5 100755 --- a/database.py +++ b/database.py @@ -830,6 +830,19 @@ def get_user_by_email(email): return dict(user) if user else None +def update_user_email(user_id, email, verified=False): + """更新用户邮箱""" + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE users + SET email = ?, email_verified = ? + WHERE id = ? + ''', (email, int(verified), user_id)) + conn.commit() + return cursor.rowcount > 0 + + def get_all_users(): """获取所有用户""" with db_pool.get_db() as conn: diff --git a/email_service.py b/email_service.py index c6bf9f3..a070cee 100644 --- a/email_service.py +++ b/email_service.py @@ -1404,6 +1404,122 @@ def confirm_password_reset(token: str) -> Optional[Dict[str, Any]]: return {'user_id': result['user_id'], 'email': result['email']} +# ============ 邮箱绑定验证 ============ + +def send_bind_email_verification( + user_id: int, + email: str, + username: str, + base_url: str = None +) -> Dict[str, Any]: + """ + 发送邮箱绑定验证邮件 + + Args: + user_id: 用户ID + email: 要绑定的邮箱 + username: 用户名 + base_url: 网站基础URL + + Returns: + {'success': bool, 'error': str, 'token': str} + """ + # 检查发送频率限制(绑定邮件限制1分钟) + if not check_rate_limit(email, EMAIL_TYPE_BIND): + return { + 'success': False, + 'error': '发送太频繁,请1分钟后再试', + 'token': None + } + + # 使旧的绑定token失效 + with db_pool.get_db() as conn: + cursor = conn.cursor() + cursor.execute(""" + UPDATE email_tokens SET used = 1 + WHERE user_id = ? AND token_type = ? AND used = 0 + """, (user_id, EMAIL_TYPE_BIND)) + conn.commit() + + # 生成新的验证Token + token = generate_email_token(email, EMAIL_TYPE_BIND, 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-bind-email/{token}" + + # 读取邮件模板 + template_path = os.path.join(os.path.dirname(__file__), 'templates', 'email', 'bind_email.html') + try: + with open(template_path, 'r', encoding='utf-8') as f: + html_template = f.read() + except FileNotFoundError: + html_template = """ + + +

邮箱绑定验证

+

您好,{{ username }}!

+

请点击下面的链接完成邮箱绑定:

+

{{ verify_url }}

+

此链接1小时内有效。

+ + + """ + + # 替换模板变量 + html_body = html_template.replace('{{ username }}', username) + html_body = html_body.replace('{{ verify_url }}', verify_url) + + # 纯文本版本 + text_body = f""" +您好,{username}! + +您正在绑定此邮箱到您的账号。请点击下面的链接完成验证: + +{verify_url} + +此链接1小时内有效。 + +如果这不是您的操作,请忽略此邮件。 +""" + + # 发送邮件 + result = send_email( + to_email=email, + subject='【知识管理平台】邮箱绑定验证', + body=text_body, + html_body=html_body, + email_type=EMAIL_TYPE_BIND, + user_id=user_id + ) + + if result['success']: + return {'success': True, 'error': '', 'token': token} + else: + return {'success': False, 'error': result['error'], 'token': None} + + +def verify_bind_email_token(token: str) -> Optional[Dict[str, Any]]: + """ + 验证邮箱绑定Token + + Returns: + 成功返回 {'user_id': int, 'email': str},失败返回 None + """ + return verify_email_token(token, EMAIL_TYPE_BIND) + + # ============ 异步发送队列 ============ class EmailQueue: diff --git a/templates/email/bind_email.html b/templates/email/bind_email.html new file mode 100644 index 0000000..6ebbe39 --- /dev/null +++ b/templates/email/bind_email.html @@ -0,0 +1,80 @@ + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+

知识管理平台

+

邮箱绑定验证

+
+

+ 您好,{{ username }}! +

+

+ 您正在绑定此邮箱到您的账号。请点击下方按钮完成验证: +

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

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

+

+ {{ verify_url }} +

+
+ + +
+

+ 安全提示:此链接1小时内有效。如果这不是您的操作,请忽略此邮件。 +

+
+ +

+ 绑定成功后,您将可以通过此邮箱接收任务完成通知。 +

+
+

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

+

+ © 知识管理平台 +

+
+
+ + diff --git a/templates/index.html b/templates/index.html index 63b1b3b..73a7714 100644 --- a/templates/index.html +++ b/templates/index.html @@ -742,27 +742,54 @@