feat: 迁移 pages 模块到 controllers 层

- 迁移 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 <noreply@anthropic.com>
This commit is contained in:
胡绍康andClaude Opus 4.6 committed 2026-02-25 00:49:47 +08:00
1 parent 3bab297aac
commit fa99c48ef6
3 files changed
+133 -6

No files matched your search

+1 -1
View File
@@ -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))
+123
View File
@@ -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})
+9 -5
View File
@@ -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