feat: 添加安全模块 + Dockerfile添加curl支持健康检查
主要更新: - 新增 security/ 安全模块 (风险评估、威胁检测、蜜罐等) - Dockerfile 添加 curl 以支持 Docker 健康检查 - 前端页面更新 (管理后台、用户端) - 数据库迁移和 schema 更新 - 新增 kdocs 上传服务 - 添加安全相关测试用例 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import posixpath
|
||||
import secrets
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
@@ -15,7 +17,9 @@ from app_logger import get_logger
|
||||
from app_security import (
|
||||
get_rate_limit_ip,
|
||||
is_safe_outbound_url,
|
||||
is_safe_path,
|
||||
require_ip_not_locked,
|
||||
sanitize_filename,
|
||||
validate_email,
|
||||
validate_password,
|
||||
)
|
||||
@@ -48,6 +52,36 @@ from services.time_utils import BEIJING_TZ, get_beijing_now
|
||||
logger = get_logger("app")
|
||||
config = get_config()
|
||||
|
||||
_server_cpu_percent_lock = threading.Lock()
|
||||
_server_cpu_percent_last: float | None = None
|
||||
_server_cpu_percent_last_ts = 0.0
|
||||
|
||||
|
||||
def _get_server_cpu_percent() -> float:
|
||||
import psutil
|
||||
|
||||
global _server_cpu_percent_last, _server_cpu_percent_last_ts
|
||||
|
||||
now = time.time()
|
||||
with _server_cpu_percent_lock:
|
||||
if _server_cpu_percent_last is not None and (now - _server_cpu_percent_last_ts) < 0.5:
|
||||
return _server_cpu_percent_last
|
||||
|
||||
try:
|
||||
if _server_cpu_percent_last is None:
|
||||
cpu_percent = float(psutil.cpu_percent(interval=0.1))
|
||||
else:
|
||||
cpu_percent = float(psutil.cpu_percent(interval=None))
|
||||
except Exception:
|
||||
cpu_percent = float(_server_cpu_percent_last or 0.0)
|
||||
|
||||
if cpu_percent < 0:
|
||||
cpu_percent = 0.0
|
||||
|
||||
_server_cpu_percent_last = cpu_percent
|
||||
_server_cpu_percent_last_ts = now
|
||||
return cpu_percent
|
||||
|
||||
|
||||
def _admin_reauth_required() -> bool:
|
||||
try:
|
||||
@@ -61,6 +95,24 @@ def _require_admin_reauth():
|
||||
return jsonify({"error": "需要二次确认", "code": "reauth_required"}), 401
|
||||
return None
|
||||
|
||||
def _get_upload_dir():
|
||||
rel_dir = getattr(config, "ANNOUNCEMENT_IMAGE_DIR", "static/announcements")
|
||||
if not is_safe_path(current_app.root_path, rel_dir):
|
||||
rel_dir = "static/announcements"
|
||||
abs_dir = os.path.join(current_app.root_path, rel_dir)
|
||||
os.makedirs(abs_dir, exist_ok=True)
|
||||
return abs_dir, rel_dir
|
||||
|
||||
|
||||
def _get_file_size(file_storage):
|
||||
try:
|
||||
file_storage.stream.seek(0, os.SEEK_END)
|
||||
size = file_storage.stream.tell()
|
||||
file_storage.stream.seek(0)
|
||||
return size
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@admin_api_bp.route("/debug-config", methods=["GET"])
|
||||
@admin_required
|
||||
@@ -199,6 +251,42 @@ def admin_reauth():
|
||||
# ==================== 公告管理API(管理员) ====================
|
||||
|
||||
|
||||
@admin_api_bp.route("/announcements/upload_image", methods=["POST"])
|
||||
@admin_required
|
||||
def admin_upload_announcement_image():
|
||||
"""上传公告图片(返回可访问URL)"""
|
||||
file = request.files.get("file")
|
||||
if not file or not file.filename:
|
||||
return jsonify({"error": "请选择图片"}), 400
|
||||
|
||||
filename = sanitize_filename(file.filename)
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
allowed_exts = getattr(config, "ALLOWED_ANNOUNCEMENT_IMAGE_EXTENSIONS", {".png", ".jpg", ".jpeg"})
|
||||
if not ext or ext not in allowed_exts:
|
||||
return jsonify({"error": "不支持的图片格式"}), 400
|
||||
if file.mimetype and not str(file.mimetype).startswith("image/"):
|
||||
return jsonify({"error": "文件类型无效"}), 400
|
||||
|
||||
size = _get_file_size(file)
|
||||
max_size = int(getattr(config, "MAX_ANNOUNCEMENT_IMAGE_SIZE", 5 * 1024 * 1024))
|
||||
if size is not None and size > max_size:
|
||||
max_mb = max_size // 1024 // 1024
|
||||
return jsonify({"error": f"图片大小不能超过{max_mb}MB"}), 400
|
||||
|
||||
abs_dir, rel_dir = _get_upload_dir()
|
||||
token = secrets.token_hex(6)
|
||||
name = f"announcement_{int(time.time())}_{token}{ext}"
|
||||
save_path = os.path.join(abs_dir, name)
|
||||
file.save(save_path)
|
||||
|
||||
static_root = os.path.join(current_app.root_path, "static")
|
||||
rel_to_static = os.path.relpath(abs_dir, static_root)
|
||||
if rel_to_static.startswith(".."):
|
||||
rel_to_static = "announcements"
|
||||
url_path = posixpath.join(rel_to_static.replace(os.sep, "/"), name)
|
||||
return jsonify({"success": True, "url": url_for("serve_static", filename=url_path)})
|
||||
|
||||
|
||||
@admin_api_bp.route("/announcements", methods=["GET"])
|
||||
@admin_required
|
||||
def admin_get_announcements():
|
||||
@@ -221,9 +309,13 @@ def admin_create_announcement():
|
||||
data = request.json or {}
|
||||
title = (data.get("title") or "").strip()
|
||||
content = (data.get("content") or "").strip()
|
||||
image_url = (data.get("image_url") or "").strip()
|
||||
is_active = bool(data.get("is_active", True))
|
||||
|
||||
announcement_id = database.create_announcement(title, content, is_active=is_active)
|
||||
if image_url and len(image_url) > 1000:
|
||||
return jsonify({"error": "图片地址过长"}), 400
|
||||
|
||||
announcement_id = database.create_announcement(title, content, image_url=image_url, is_active=is_active)
|
||||
if not announcement_id:
|
||||
return jsonify({"error": "标题和内容不能为空"}), 400
|
||||
|
||||
@@ -317,6 +409,71 @@ def get_system_stats():
|
||||
return jsonify(stats)
|
||||
|
||||
|
||||
@admin_api_bp.route("/browser_pool/stats", methods=["GET"])
|
||||
@admin_required
|
||||
def get_browser_pool_stats():
|
||||
"""获取截图线程池状态"""
|
||||
try:
|
||||
from browser_pool_worker import get_browser_worker_pool
|
||||
|
||||
pool = get_browser_worker_pool()
|
||||
stats = pool.get_stats() or {}
|
||||
|
||||
worker_details = []
|
||||
for w in stats.get("workers") or []:
|
||||
last_ts = float(w.get("last_active_ts") or 0)
|
||||
last_active_at = None
|
||||
if last_ts > 0:
|
||||
try:
|
||||
last_active_at = datetime.fromtimestamp(last_ts, tz=BEIJING_TZ).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
last_active_at = None
|
||||
|
||||
created_ts = w.get("browser_created_at")
|
||||
created_at = None
|
||||
if created_ts:
|
||||
try:
|
||||
created_at = datetime.fromtimestamp(float(created_ts), tz=BEIJING_TZ).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
created_at = None
|
||||
|
||||
worker_details.append(
|
||||
{
|
||||
"worker_id": w.get("worker_id"),
|
||||
"idle": bool(w.get("idle")),
|
||||
"has_browser": bool(w.get("has_browser")),
|
||||
"total_tasks": int(w.get("total_tasks") or 0),
|
||||
"failed_tasks": int(w.get("failed_tasks") or 0),
|
||||
"browser_use_count": int(w.get("browser_use_count") or 0),
|
||||
"browser_created_at": created_at,
|
||||
"browser_created_ts": created_ts,
|
||||
"last_active_at": last_active_at,
|
||||
"last_active_ts": last_ts,
|
||||
"thread_alive": bool(w.get("thread_alive")),
|
||||
}
|
||||
)
|
||||
|
||||
total_workers = len(worker_details) if worker_details else int(stats.get("pool_size") or 0)
|
||||
return jsonify(
|
||||
{
|
||||
"total_workers": total_workers,
|
||||
"active_workers": int(stats.get("busy_workers") or 0),
|
||||
"idle_workers": int(stats.get("idle_workers") or 0),
|
||||
"queue_size": int(stats.get("queue_size") or 0),
|
||||
"workers": worker_details,
|
||||
"summary": {
|
||||
"total_tasks": int(stats.get("total_tasks") or 0),
|
||||
"failed_tasks": int(stats.get("failed_tasks") or 0),
|
||||
"success_rate": stats.get("success_rate"),
|
||||
},
|
||||
"server_time_cst": get_beijing_now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"[AdminAPI] 获取截图线程池状态失败: {e}")
|
||||
return jsonify({"error": "获取截图线程池状态失败"}), 500
|
||||
|
||||
|
||||
@admin_api_bp.route("/docker_stats", methods=["GET"])
|
||||
@admin_required
|
||||
def get_docker_stats():
|
||||
@@ -510,9 +667,21 @@ def update_system_config_api():
|
||||
schedule_weekdays = data.get("schedule_weekdays")
|
||||
new_max_concurrent_per_account = data.get("max_concurrent_per_account")
|
||||
new_max_screenshot_concurrent = data.get("max_screenshot_concurrent")
|
||||
enable_screenshot = data.get("enable_screenshot")
|
||||
auto_approve_enabled = data.get("auto_approve_enabled")
|
||||
auto_approve_hourly_limit = data.get("auto_approve_hourly_limit")
|
||||
auto_approve_vip_days = data.get("auto_approve_vip_days")
|
||||
kdocs_enabled = data.get("kdocs_enabled")
|
||||
kdocs_doc_url = data.get("kdocs_doc_url")
|
||||
kdocs_default_unit = data.get("kdocs_default_unit")
|
||||
kdocs_sheet_name = data.get("kdocs_sheet_name")
|
||||
kdocs_sheet_index = data.get("kdocs_sheet_index")
|
||||
kdocs_unit_column = data.get("kdocs_unit_column")
|
||||
kdocs_image_column = data.get("kdocs_image_column")
|
||||
kdocs_admin_notify_enabled = data.get("kdocs_admin_notify_enabled")
|
||||
kdocs_admin_notify_email = data.get("kdocs_admin_notify_email")
|
||||
kdocs_row_start = data.get("kdocs_row_start")
|
||||
kdocs_row_end = data.get("kdocs_row_end")
|
||||
|
||||
if max_concurrent is not None:
|
||||
if not isinstance(max_concurrent, int) or max_concurrent < 1:
|
||||
@@ -524,7 +693,13 @@ def update_system_config_api():
|
||||
|
||||
if new_max_screenshot_concurrent is not None:
|
||||
if not isinstance(new_max_screenshot_concurrent, int) or new_max_screenshot_concurrent < 1:
|
||||
return jsonify({"error": "截图并发数必须大于0(建议根据服务器配置设置,每个浏览器约占用200MB内存)"}), 400
|
||||
return jsonify({"error": "截图并发数必须大于0(建议根据服务器配置设置,wkhtmltoimage 资源占用较低)"}), 400
|
||||
|
||||
if enable_screenshot is not None:
|
||||
if isinstance(enable_screenshot, bool):
|
||||
enable_screenshot = 1 if enable_screenshot else 0
|
||||
if enable_screenshot not in (0, 1):
|
||||
return jsonify({"error": "截图开关必须是0或1"}), 400
|
||||
|
||||
if schedule_time is not None:
|
||||
import re
|
||||
@@ -554,6 +729,82 @@ def update_system_config_api():
|
||||
if not isinstance(auto_approve_vip_days, int) or auto_approve_vip_days < 0:
|
||||
return jsonify({"error": "注册赠送VIP天数不能为负数"}), 400
|
||||
|
||||
if kdocs_enabled is not None:
|
||||
if isinstance(kdocs_enabled, bool):
|
||||
kdocs_enabled = 1 if kdocs_enabled else 0
|
||||
if kdocs_enabled not in (0, 1):
|
||||
return jsonify({"error": "表格上传开关必须是0或1"}), 400
|
||||
|
||||
if kdocs_doc_url is not None:
|
||||
kdocs_doc_url = str(kdocs_doc_url or "").strip()
|
||||
if kdocs_doc_url and not is_safe_outbound_url(kdocs_doc_url):
|
||||
return jsonify({"error": "文档链接格式不正确"}), 400
|
||||
|
||||
if kdocs_default_unit is not None:
|
||||
kdocs_default_unit = str(kdocs_default_unit or "").strip()
|
||||
if len(kdocs_default_unit) > 50:
|
||||
return jsonify({"error": "默认县区长度不能超过50"}), 400
|
||||
|
||||
if kdocs_sheet_name is not None:
|
||||
kdocs_sheet_name = str(kdocs_sheet_name or "").strip()
|
||||
if len(kdocs_sheet_name) > 50:
|
||||
return jsonify({"error": "Sheet名称长度不能超过50"}), 400
|
||||
|
||||
if kdocs_sheet_index is not None:
|
||||
try:
|
||||
kdocs_sheet_index = int(kdocs_sheet_index)
|
||||
except Exception:
|
||||
return jsonify({"error": "Sheet序号必须是数字"}), 400
|
||||
if kdocs_sheet_index < 0:
|
||||
return jsonify({"error": "Sheet序号不能为负数"}), 400
|
||||
|
||||
if kdocs_unit_column is not None:
|
||||
kdocs_unit_column = str(kdocs_unit_column or "").strip().upper()
|
||||
if not kdocs_unit_column:
|
||||
return jsonify({"error": "县区列不能为空"}), 400
|
||||
import re
|
||||
|
||||
if not re.match(r"^[A-Z]{1,3}$", kdocs_unit_column):
|
||||
return jsonify({"error": "县区列格式错误"}), 400
|
||||
|
||||
if kdocs_image_column is not None:
|
||||
kdocs_image_column = str(kdocs_image_column or "").strip().upper()
|
||||
if not kdocs_image_column:
|
||||
return jsonify({"error": "图片列不能为空"}), 400
|
||||
import re
|
||||
|
||||
if not re.match(r"^[A-Z]{1,3}$", kdocs_image_column):
|
||||
return jsonify({"error": "图片列格式错误"}), 400
|
||||
|
||||
if kdocs_admin_notify_enabled is not None:
|
||||
if isinstance(kdocs_admin_notify_enabled, bool):
|
||||
kdocs_admin_notify_enabled = 1 if kdocs_admin_notify_enabled else 0
|
||||
if kdocs_admin_notify_enabled not in (0, 1):
|
||||
return jsonify({"error": "管理员通知开关必须是0或1"}), 400
|
||||
|
||||
if kdocs_admin_notify_email is not None:
|
||||
kdocs_admin_notify_email = str(kdocs_admin_notify_email or "").strip()
|
||||
if kdocs_admin_notify_email:
|
||||
is_valid, error_msg = validate_email(kdocs_admin_notify_email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
if kdocs_row_start is not None:
|
||||
try:
|
||||
kdocs_row_start = int(kdocs_row_start)
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "起始行必须是数字"}), 400
|
||||
if kdocs_row_start < 0:
|
||||
return jsonify({"error": "起始行不能为负数"}), 400
|
||||
|
||||
if kdocs_row_end is not None:
|
||||
try:
|
||||
kdocs_row_end = int(kdocs_row_end)
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "结束行必须是数字"}), 400
|
||||
if kdocs_row_end < 0:
|
||||
return jsonify({"error": "结束行不能为负数"}), 400
|
||||
|
||||
old_config = database.get_system_config() or {}
|
||||
|
||||
if not database.update_system_config(
|
||||
@@ -564,9 +815,21 @@ def update_system_config_api():
|
||||
schedule_weekdays=schedule_weekdays,
|
||||
max_concurrent_per_account=new_max_concurrent_per_account,
|
||||
max_screenshot_concurrent=new_max_screenshot_concurrent,
|
||||
enable_screenshot=enable_screenshot,
|
||||
auto_approve_enabled=auto_approve_enabled,
|
||||
auto_approve_hourly_limit=auto_approve_hourly_limit,
|
||||
auto_approve_vip_days=auto_approve_vip_days,
|
||||
kdocs_enabled=kdocs_enabled,
|
||||
kdocs_doc_url=kdocs_doc_url,
|
||||
kdocs_default_unit=kdocs_default_unit,
|
||||
kdocs_sheet_name=kdocs_sheet_name,
|
||||
kdocs_sheet_index=kdocs_sheet_index,
|
||||
kdocs_unit_column=kdocs_unit_column,
|
||||
kdocs_image_column=kdocs_image_column,
|
||||
kdocs_admin_notify_enabled=kdocs_admin_notify_enabled,
|
||||
kdocs_admin_notify_email=kdocs_admin_notify_email,
|
||||
kdocs_row_start=kdocs_row_start,
|
||||
kdocs_row_end=kdocs_row_end,
|
||||
):
|
||||
return jsonify({"error": "更新失败"}), 400
|
||||
|
||||
@@ -577,6 +840,14 @@ def update_system_config_api():
|
||||
max_global=int(new_config.get("max_concurrent_global", old_config.get("max_concurrent_global", 2))),
|
||||
max_per_user=int(new_config.get("max_concurrent_per_account", old_config.get("max_concurrent_per_account", 1))),
|
||||
)
|
||||
if new_max_screenshot_concurrent is not None:
|
||||
try:
|
||||
from browser_pool_worker import resize_browser_worker_pool
|
||||
|
||||
if resize_browser_worker_pool(int(new_config.get("max_screenshot_concurrent", new_max_screenshot_concurrent))):
|
||||
logger.info(f"截图线程池并发已更新为: {new_config.get('max_screenshot_concurrent')}")
|
||||
except Exception as pool_error:
|
||||
logger.warning(f"截图线程池并发更新失败: {pool_error}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -590,6 +861,70 @@ def update_system_config_api():
|
||||
return jsonify({"message": "系统配置已更新"})
|
||||
|
||||
|
||||
@admin_api_bp.route("/kdocs/status", methods=["GET"])
|
||||
@admin_required
|
||||
def get_kdocs_status_api():
|
||||
"""获取金山文档上传状态"""
|
||||
try:
|
||||
from services.kdocs_uploader import get_kdocs_uploader
|
||||
|
||||
uploader = get_kdocs_uploader()
|
||||
status = uploader.get_status()
|
||||
live = str(request.args.get("live", "")).lower() in ("1", "true", "yes")
|
||||
if live:
|
||||
live_status = uploader.refresh_login_status()
|
||||
if live_status.get("success"):
|
||||
logged_in = bool(live_status.get("logged_in"))
|
||||
status["logged_in"] = logged_in
|
||||
status["last_login_ok"] = logged_in
|
||||
status["login_required"] = not logged_in
|
||||
if live_status.get("error"):
|
||||
status["last_error"] = live_status.get("error")
|
||||
else:
|
||||
status["logged_in"] = True if status.get("last_login_ok") else False if status.get("last_login_ok") is False else None
|
||||
if status.get("last_login_ok") is True and status.get("last_error") == "操作超时":
|
||||
status["last_error"] = None
|
||||
return jsonify(status)
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"获取状态失败: {e}"}), 500
|
||||
|
||||
|
||||
@admin_api_bp.route("/kdocs/qr", methods=["POST"])
|
||||
@admin_required
|
||||
def get_kdocs_qr_api():
|
||||
"""获取金山文档登录二维码"""
|
||||
try:
|
||||
from services.kdocs_uploader import get_kdocs_uploader
|
||||
|
||||
uploader = get_kdocs_uploader()
|
||||
data = request.get_json(silent=True) or {}
|
||||
force = bool(data.get("force"))
|
||||
if not force:
|
||||
force = str(request.args.get("force", "")).lower() in ("1", "true", "yes")
|
||||
result = uploader.request_qr(force=force)
|
||||
if not result.get("success"):
|
||||
return jsonify({"error": result.get("error", "获取二维码失败")}), 400
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"获取二维码失败: {e}"}), 500
|
||||
|
||||
|
||||
@admin_api_bp.route("/kdocs/clear-login", methods=["POST"])
|
||||
@admin_required
|
||||
def clear_kdocs_login_api():
|
||||
"""清除金山文档登录态"""
|
||||
try:
|
||||
from services.kdocs_uploader import get_kdocs_uploader
|
||||
|
||||
uploader = get_kdocs_uploader()
|
||||
result = uploader.clear_login()
|
||||
if not result.get("success"):
|
||||
return jsonify({"error": result.get("error", "清除失败")}), 400
|
||||
return jsonify({"success": True})
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"清除失败: {e}"}), 500
|
||||
|
||||
|
||||
@admin_api_bp.route("/schedule/execute", methods=["POST"])
|
||||
@admin_required
|
||||
def execute_schedule_now():
|
||||
@@ -673,7 +1008,7 @@ def get_server_info_api():
|
||||
"""获取服务器信息"""
|
||||
import psutil
|
||||
|
||||
cpu_percent = psutil.cpu_percent(interval=1)
|
||||
cpu_percent = _get_server_cpu_percent()
|
||||
|
||||
memory = psutil.virtual_memory()
|
||||
memory_total = f"{memory.total / (1024**3):.1f}GB"
|
||||
@@ -776,30 +1111,44 @@ def get_running_tasks_api():
|
||||
@admin_required
|
||||
def get_task_logs_api():
|
||||
"""获取任务日志列表(支持分页和多种筛选)"""
|
||||
limit = int(request.args.get("limit", 20))
|
||||
offset = int(request.args.get("offset", 0))
|
||||
try:
|
||||
limit = int(request.args.get("limit", 20))
|
||||
limit = max(1, min(limit, 200)) # 限制 1-200 条
|
||||
except (ValueError, TypeError):
|
||||
limit = 20
|
||||
|
||||
try:
|
||||
offset = int(request.args.get("offset", 0))
|
||||
offset = max(0, offset)
|
||||
except (ValueError, TypeError):
|
||||
offset = 0
|
||||
|
||||
date_filter = request.args.get("date")
|
||||
status_filter = request.args.get("status")
|
||||
source_filter = request.args.get("source")
|
||||
user_id_filter = request.args.get("user_id")
|
||||
account_filter = request.args.get("account")
|
||||
account_filter = (request.args.get("account") or "").strip()
|
||||
|
||||
if user_id_filter:
|
||||
try:
|
||||
user_id_filter = int(user_id_filter)
|
||||
except ValueError:
|
||||
except (ValueError, TypeError):
|
||||
user_id_filter = None
|
||||
|
||||
result = database.get_task_logs(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
date_filter=date_filter,
|
||||
status_filter=status_filter,
|
||||
source_filter=source_filter,
|
||||
user_id_filter=user_id_filter,
|
||||
account_filter=account_filter,
|
||||
)
|
||||
return jsonify(result)
|
||||
try:
|
||||
result = database.get_task_logs(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
date_filter=date_filter,
|
||||
status_filter=status_filter,
|
||||
source_filter=source_filter,
|
||||
user_id_filter=user_id_filter,
|
||||
account_filter=account_filter if account_filter else None,
|
||||
)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
logger.error(f"获取任务日志失败: {e}")
|
||||
return jsonify({"logs": [], "total": 0, "error": "查询失败"})
|
||||
|
||||
|
||||
@admin_api_bp.route("/task/logs/clear", methods=["POST"])
|
||||
@@ -910,32 +1259,6 @@ def admin_reset_password_route(user_id):
|
||||
return jsonify({"error": "重置失败,用户不存在"}), 400
|
||||
|
||||
|
||||
@admin_api_bp.route("/password_resets", methods=["GET"])
|
||||
@admin_required
|
||||
def get_password_resets_route():
|
||||
"""获取所有待审核的密码重置申请"""
|
||||
resets = database.get_pending_password_resets()
|
||||
return jsonify(resets)
|
||||
|
||||
|
||||
@admin_api_bp.route("/password_resets/<int:request_id>/approve", methods=["POST"])
|
||||
@admin_required
|
||||
def approve_password_reset_route(request_id):
|
||||
"""批准密码重置申请"""
|
||||
if database.approve_password_reset(request_id):
|
||||
return jsonify({"message": "密码重置申请已批准"})
|
||||
return jsonify({"error": "批准失败"}), 400
|
||||
|
||||
|
||||
@admin_api_bp.route("/password_resets/<int:request_id>/reject", methods=["POST"])
|
||||
@admin_required
|
||||
def reject_password_reset_route(request_id):
|
||||
"""拒绝密码重置申请"""
|
||||
if database.reject_password_reset(request_id):
|
||||
return jsonify({"message": "密码重置申请已拒绝"})
|
||||
return jsonify({"error": "拒绝失败"}), 400
|
||||
|
||||
|
||||
@admin_api_bp.route("/feedbacks", methods=["GET"])
|
||||
@admin_required
|
||||
def get_all_feedbacks():
|
||||
@@ -1067,6 +1390,7 @@ def update_email_settings_api():
|
||||
enabled = data.get("enabled", False)
|
||||
failover_enabled = data.get("failover_enabled", True)
|
||||
register_verify_enabled = data.get("register_verify_enabled")
|
||||
login_alert_enabled = data.get("login_alert_enabled")
|
||||
base_url = data.get("base_url")
|
||||
task_notify_enabled = data.get("task_notify_enabled")
|
||||
|
||||
@@ -1074,6 +1398,7 @@ def update_email_settings_api():
|
||||
enabled=enabled,
|
||||
failover_enabled=failover_enabled,
|
||||
register_verify_enabled=register_verify_enabled,
|
||||
login_alert_enabled=login_alert_enabled,
|
||||
base_url=base_url,
|
||||
task_notify_enabled=task_notify_enabled,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user