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:
1 parent
3bab297aac
commit
fa99c48ef6
3 files changed
+133
-6
No files matched your search
+1
-1
@@ -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))
|
||||
|
||||
@@ -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})
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user