fix: admin auth UX, password policy, deps, db pool

This commit is contained in:
2025-12-17 23:53:11 +08:00
parent 9028f7e272
commit 5851120f87
6 changed files with 46 additions and 18 deletions

View File

@@ -7,8 +7,26 @@ import threading
import queue import queue
import time import time
from typing import Callable, Optional, Dict, Any from typing import Callable, Optional, Dict, Any
import nest_asyncio import nest_asyncio
nest_asyncio.apply()
_NEST_ASYNCIO_APPLIED = False
_NEST_ASYNCIO_LOCK = threading.Lock()
def _apply_nest_asyncio_once() -> None:
"""按需应用 nest_asyncio避免 import 时产生全局副作用。"""
global _NEST_ASYNCIO_APPLIED
if _NEST_ASYNCIO_APPLIED:
return
with _NEST_ASYNCIO_LOCK:
if _NEST_ASYNCIO_APPLIED:
return
try:
nest_asyncio.apply()
except Exception:
pass
_NEST_ASYNCIO_APPLIED = True
# 安全修复: 将魔法数字提取为可配置常量 # 安全修复: 将魔法数字提取为可配置常量
BROWSER_IDLE_TIMEOUT = int(os.environ.get('BROWSER_IDLE_TIMEOUT', '300')) # 空闲超时(秒)默认5分钟 BROWSER_IDLE_TIMEOUT = int(os.environ.get('BROWSER_IDLE_TIMEOUT', '300')) # 空闲超时(秒)默认5分钟
@@ -204,13 +222,15 @@ class BrowserWorkerPool:
else: else:
print(f"[浏览器池] {message}") print(f"[浏览器池] {message}")
def initialize(self): def initialize(self):
"""初始化工作线程池(按需模式,启动时不创建浏览器)""" """初始化工作线程池(按需模式,启动时不创建浏览器)"""
with self.lock: with self.lock:
if self.initialized: if self.initialized:
return return
self.log(f"正在初始化工作线程池({self.pool_size}个worker按需启动浏览器...") _apply_nest_asyncio_once()
self.log(f"正在初始化工作线程池({self.pool_size}个worker按需启动浏览器...")
for i in range(self.pool_size): for i in range(self.pool_size):
worker = BrowserWorker( worker = BrowserWorker(

View File

@@ -111,6 +111,7 @@ class ConnectionPool:
if self._pool.qsize() < self.pool_size: if self._pool.qsize() < self.pool_size:
try: try:
new_conn = self._create_connection() new_conn = self._create_connection()
self._created_connections += 1
self._pool.put(new_conn, block=False) self._pool.put(new_conn, block=False)
except Full: except Full:
# 在获取锁期间池被填满了,关闭新建的连接 # 在获取锁期间池被填满了,关闭新建的连接
@@ -170,6 +171,7 @@ class PooledConnection:
if self._cursor: if self._cursor:
self._cursor.close() self._cursor.close()
self._cursor = None
except Exception as e: except Exception as e:
print(f"关闭游标失败: {e}") print(f"关闭游标失败: {e}")
finally: finally:
@@ -180,7 +182,8 @@ class PooledConnection:
def cursor(self): def cursor(self):
"""获取游标""" """获取游标"""
self._cursor = self._conn.cursor() if self._cursor is None:
self._cursor = self._conn.cursor()
return self._cursor return self._cursor
def commit(self): def commit(self):

View File

@@ -3,7 +3,6 @@ flask-socketio==5.3.5
flask-login==0.6.3 flask-login==0.6.3
python-socketio==5.10.0 python-socketio==5.10.0
playwright==1.40.0 playwright==1.40.0
eventlet==0.33.3
schedule==1.2.0 schedule==1.2.0
psutil==5.9.6 psutil==5.9.6
pytz==2024.1 pytz==2024.1

View File

@@ -43,6 +43,10 @@ def register():
if not username or not password: if not username or not password:
return jsonify({"error": "用户名和密码不能为空"}), 400 return jsonify({"error": "用户名和密码不能为空"}), 400
is_valid, error_msg = validate_password(password)
if not is_valid:
return jsonify({"error": error_msg}), 400
client_ip = get_client_ip() client_ip = get_client_ip()
allowed, error_msg = check_ip_rate_limit(client_ip) allowed, error_msg = check_ip_rate_limit(client_ip)

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import database import database
import email_service import email_service
from app_logger import get_logger from app_logger import get_logger
from app_security import require_ip_not_locked, validate_email from app_security import require_ip_not_locked, validate_email, validate_password
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
from flask_login import current_user, login_required from flask_login import current_user, login_required
from routes.pages import render_app_spa_or_legacy from routes.pages import render_app_spa_or_legacy
@@ -119,8 +119,9 @@ def change_user_password():
if not current_password or not new_password: if not current_password or not new_password:
return jsonify({"error": "请填写完整信息"}), 400 return jsonify({"error": "请填写完整信息"}), 400
if len(new_password) < 6: is_valid, error_msg = validate_password(new_password)
return jsonify({"error": "新密码至少6位"}), 400 if not is_valid:
return jsonify({"error": error_msg}), 400
user = database.get_user_by_id(current_user.id) user = database.get_user_by_id(current_user.id)
if not user: if not user:
@@ -290,4 +291,3 @@ def get_run_stats():
"today_attachments": stats.get("total_attachments", 0), "today_attachments": stats.get("total_attachments", 0),
} }
) )

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from functools import wraps from functools import wraps
from flask import jsonify, request, session from flask import jsonify, redirect, request, session, url_for
from services.runtime import get_logger from services.runtime import get_logger
@@ -18,9 +18,11 @@ def admin_required(f):
logger.debug(f"[admin_required] 检查会话admin_id存在: {'admin_id' in session}") logger.debug(f"[admin_required] 检查会话admin_id存在: {'admin_id' in session}")
if "admin_id" not in session: if "admin_id" not in session:
logger.warning(f"[admin_required] 拒绝访问 {request.path} - session中无admin_id") logger.warning(f"[admin_required] 拒绝访问 {request.path} - session中无admin_id")
return jsonify({"error": "需要管理员权限"}), 403 is_api = request.blueprint == "admin_api" or request.path.startswith("/yuyx/api")
if is_api:
return jsonify({"error": "需要管理员权限"}), 403
return redirect(url_for("pages.admin_login_page"))
logger.info(f"[admin_required] 管理员 {session.get('admin_username')} 访问 {request.path}") logger.info(f"[admin_required] 管理员 {session.get('admin_username')} 访问 {request.path}")
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function