From fa99c48ef6c4a6394555cffbcc6298af68a7623f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=BB=8D=E5=BA=B7?= <3052147989@qq.com> Date: Wed, 25 Feb 2026 00:49:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=BF=81=E7=A7=BB=20pages=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=88=B0=20controllers=20=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 迁移 4 个 pages 相关路由到 controllers/pages.py: - login (GET/POST) - logout - index - get_csrf_token - 更新 routes/pages.py 移除 impl 参数 - 保留 csrf_exempt 装饰器支持 Co-Authored-By: Claude Opus 4.6 --- outlook_web/app.py | 2 +- outlook_web/controllers/pages.py | 123 +++++++++++++++++++++++++++++++ outlook_web/routes/pages.py | 14 ++-- 3 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 outlook_web/controllers/pages.py diff --git a/outlook_web/app.py b/outlook_web/app.py index b2e496f..a9131ad 100644 --- a/outlook_web/app.py +++ b/outlook_web/app.py @@ -69,7 +69,7 @@ def create_app(*, autostart_scheduler: Optional[bool] = None): app.register_error_handler(Exception, legacy.handle_exception) # Blueprint 路由注册(URL 不变) - app.register_blueprint(pages.create_blueprint(impl=legacy, csrf_exempt=csrf_exempt)) + app.register_blueprint(pages.create_blueprint(csrf_exempt=csrf_exempt)) app.register_blueprint(groups.create_blueprint()) app.register_blueprint(tags.create_blueprint()) app.register_blueprint(accounts.create_blueprint(impl=legacy)) diff --git a/outlook_web/controllers/pages.py b/outlook_web/controllers/pages.py new file mode 100644 index 0000000..97e8736 --- /dev/null +++ b/outlook_web/controllers/pages.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from typing import Any + +from flask import g, jsonify, redirect, render_template, request, session, url_for + +from outlook_web.errors import build_error_payload +from outlook_web.repositories import settings as settings_repo +from outlook_web.security.auth import ( + check_rate_limit, + get_client_ip, + login_required, + record_login_failure, + reset_login_attempts, +) +from outlook_web.security.crypto import verify_password + + +# ==================== 页面路由 ==================== + + +def login() -> Any: + """登录页面""" + if request.method == 'POST': + try: + # 获取客户端 IP + client_ip = get_client_ip() + + # 检查速率限制 + allowed, remaining_time = check_rate_limit(client_ip) + if not allowed: + trace_id_value = None + try: + trace_id_value = getattr(g, 'trace_id', None) + except Exception: + trace_id_value = None + error_payload = build_error_payload( + code="LOGIN_RATE_LIMITED", + message=f"登录失败次数过多,请在 {remaining_time} 秒后重试", + err_type="RateLimitError", + status=429, + details=f"ip={client_ip}", + trace_id=trace_id_value + ) + return jsonify({'success': False, 'error': error_payload}), 429 + + data = request.json if request.is_json else request.form + password = data.get('password', '') + + # 从数据库获取密码哈希 + stored_password = settings_repo.get_login_password() + + # 验证密码 + if verify_password(password, stored_password): + # 登录成功,重置失败记录 + reset_login_attempts(client_ip) + session['logged_in'] = True + session.permanent = True + session.modified = True # 确保 Flask-Session 保存 session + return jsonify({'success': True, 'message': '登录成功'}) + else: + # 登录失败,记录失败次数 + record_login_failure(client_ip) + trace_id_value = None + try: + trace_id_value = getattr(g, 'trace_id', None) + except Exception: + trace_id_value = None + error_payload = build_error_payload( + code="LOGIN_INVALID_PASSWORD", + message="密码错误", + err_type="AuthError", + status=401, + details=f"ip={client_ip}", + trace_id=trace_id_value + ) + return jsonify({'success': False, 'error': error_payload}), 401 + except Exception as e: + trace_id_value = None + try: + trace_id_value = getattr(g, 'trace_id', None) + except Exception: + trace_id_value = None + from flask import current_app + try: + current_app.logger.exception("Login error trace_id=%s", trace_id_value or "unknown") + except Exception: + pass + error_payload = build_error_payload( + code="LOGIN_FAILED", + message="登录处理失败", + err_type="AuthError", + status=500, + details=str(e), + trace_id=trace_id_value + ) + return jsonify({'success': False, 'error': error_payload}), 500 + + # GET 请求返回登录页面 + return render_template('login.html') + + +def logout() -> Any: + """退出登录""" + session.pop('logged_in', None) + return redirect(url_for('pages.login')) + + +@login_required +def index() -> Any: + """主页""" + return render_template('index.html') + + +def get_csrf_token() -> Any: + """获取CSRF Token""" + from outlook_web.security.csrf import CSRF_AVAILABLE, generate_csrf + + if CSRF_AVAILABLE: + token = generate_csrf() + return jsonify({'csrf_token': token}) + else: + return jsonify({'csrf_token': None, 'csrf_disabled': True}) diff --git a/outlook_web/routes/pages.py b/outlook_web/routes/pages.py index 0c32cbd..ad4c5dd 100644 --- a/outlook_web/routes/pages.py +++ b/outlook_web/routes/pages.py @@ -4,19 +4,23 @@ from typing import Callable, Optional from flask import Blueprint +from outlook_web.controllers import pages as pages_controller -def create_blueprint(*, impl, csrf_exempt: Optional[Callable] = None) -> Blueprint: + +def create_blueprint(csrf_exempt: Optional[Callable] = None) -> Blueprint: + """创建 pages Blueprint""" bp = Blueprint("pages", __name__) - login_view = impl.login - csrf_token_view = impl.get_csrf_token + # 应用 csrf_exempt 装饰器(如果提供) + login_view = pages_controller.login + csrf_token_view = pages_controller.get_csrf_token if csrf_exempt is not None: login_view = csrf_exempt(login_view) csrf_token_view = csrf_exempt(csrf_token_view) bp.add_url_rule("/login", view_func=login_view, methods=["GET", "POST"]) - bp.add_url_rule("/logout", view_func=impl.logout, methods=["GET"]) - bp.add_url_rule("/", view_func=impl.index, methods=["GET"]) + bp.add_url_rule("/logout", view_func=pages_controller.logout, methods=["GET"]) + bp.add_url_rule("/", view_func=pages_controller.index, methods=["GET"]) bp.add_url_rule("/api/csrf-token", view_func=csrf_token_view, methods=["GET"]) return bp