更新加密验证器
This commit is contained in:
parent
f0603e40f8
commit
74405da203
@ -200,9 +200,11 @@ def generate_licenses():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
current_app.logger.error(f"生成卡密失败: {str(e)}")
|
current_app.logger.error(f"生成卡密失败: {str(e)}")
|
||||||
|
is_production = current_app.config.get('FLASK_ENV') == 'production' or not current_app.config.get('DEBUG', False)
|
||||||
|
error_message = '生成卡密失败,请稍后重试' if is_production else f'生成卡密失败: {str(e)}'
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': f'生成卡密失败: {str(e)}'
|
'message': error_message
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -657,9 +659,12 @@ def import_licenses():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"保存卡密失败: {str(e)}")
|
||||||
|
is_production = current_app.config.get('FLASK_ENV') == 'production' or not current_app.config.get('DEBUG', False)
|
||||||
|
error_message = '保存失败,请稍后重试' if is_production else f'保存失败: {str(e)}'
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': f'保存失败: {str(e)}'
|
'message': error_message
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|||||||
@ -9,6 +9,35 @@ import traceback
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from app.utils.logger import log_operation
|
from app.utils.logger import log_operation
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
|
# 允许的图片文件扩展名
|
||||||
|
ALLOWED_IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'}
|
||||||
|
# 允许的MIME类型
|
||||||
|
ALLOWED_IMAGE_MIMES = {
|
||||||
|
'image/png', 'image/jpeg', 'image/jpg', 'image/gif',
|
||||||
|
'image/webp', 'image/bmp', 'image/x-ms-bmp'
|
||||||
|
}
|
||||||
|
# 最大图片文件大小(5MB)
|
||||||
|
MAX_IMAGE_SIZE = 5 * 1024 * 1024
|
||||||
|
|
||||||
|
def allowed_image_file(filename):
|
||||||
|
"""检查文件扩展名是否允许"""
|
||||||
|
return '.' in filename and \
|
||||||
|
filename.rsplit('.', 1)[1].lower() in ALLOWED_IMAGE_EXTENSIONS
|
||||||
|
|
||||||
|
def validate_image_file(file, filename):
|
||||||
|
"""验证图片文件(扩展名和MIME类型)"""
|
||||||
|
# 检查扩展名
|
||||||
|
if not allowed_image_file(filename):
|
||||||
|
return False, '不支持的文件类型,只允许上传图片文件(PNG、JPG、JPEG、GIF、WEBP、BMP)'
|
||||||
|
|
||||||
|
# 检查MIME类型
|
||||||
|
if hasattr(file, 'content_type') and file.content_type:
|
||||||
|
if file.content_type not in ALLOWED_IMAGE_MIMES:
|
||||||
|
return False, f'不支持的文件MIME类型: {file.content_type}'
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
@api_bp.route('/products', methods=['GET'])
|
@api_bp.route('/products', methods=['GET'])
|
||||||
@require_login
|
@require_login
|
||||||
@ -125,41 +154,95 @@ def get_products():
|
|||||||
def create_product():
|
def create_product():
|
||||||
"""创建产品"""
|
"""创建产品"""
|
||||||
try:
|
try:
|
||||||
|
# 检查请求类型
|
||||||
|
is_multipart = request.content_type and 'multipart/form-data' in request.content_type
|
||||||
|
|
||||||
# 处理文件上传
|
# 处理文件上传
|
||||||
image_path = None
|
image_path = None
|
||||||
if 'image' in request.files:
|
if 'image' in request.files:
|
||||||
image_file = request.files['image']
|
image_file = request.files['image']
|
||||||
if image_file and image_file.filename:
|
if image_file and image_file.filename:
|
||||||
# 确保上传目录存在
|
# 验证文件类型(扩展名和MIME类型)
|
||||||
upload_folder = os.path.join(current_app.static_folder, 'producepic')
|
is_valid, error_msg = validate_image_file(image_file, image_file.filename)
|
||||||
|
if not is_valid:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': error_msg
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 验证文件大小(优先使用content_length,否则读取文件)
|
||||||
|
file_size = None
|
||||||
|
if request.content_length:
|
||||||
|
file_size = request.content_length
|
||||||
|
else:
|
||||||
|
# 备用方案:读取文件大小
|
||||||
|
image_file.seek(0, os.SEEK_END)
|
||||||
|
file_size = image_file.tell()
|
||||||
|
image_file.seek(0) # 重置文件指针
|
||||||
|
|
||||||
|
if file_size and file_size > MAX_IMAGE_SIZE:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'文件大小超过限制,最大允许 {MAX_IMAGE_SIZE / (1024*1024):.1f}MB'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 确保上传目录存在(使用绝对路径)
|
||||||
|
static_folder = os.path.abspath(current_app.static_folder)
|
||||||
|
upload_folder = os.path.join(static_folder, 'producepic')
|
||||||
|
# 验证路径安全性(确保在static目录下)
|
||||||
|
if not upload_folder.startswith(static_folder):
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '无效的上传路径'
|
||||||
|
}), 400
|
||||||
|
|
||||||
os.makedirs(upload_folder, exist_ok=True)
|
os.makedirs(upload_folder, exist_ok=True)
|
||||||
|
|
||||||
# 生成唯一文件名
|
# 生成安全的唯一文件名
|
||||||
import uuid
|
import uuid
|
||||||
filename = f"{uuid.uuid4().hex}_{image_file.filename}"
|
original_filename = secure_filename(image_file.filename)
|
||||||
|
file_ext = original_filename.rsplit('.', 1)[1].lower() if '.' in original_filename else 'jpg'
|
||||||
|
filename = f"{uuid.uuid4().hex}.{file_ext}"
|
||||||
file_path = os.path.join(upload_folder, filename)
|
file_path = os.path.join(upload_folder, filename)
|
||||||
|
|
||||||
|
# 再次验证路径安全性
|
||||||
|
real_path = os.path.abspath(file_path)
|
||||||
|
if not real_path.startswith(os.path.abspath(upload_folder)):
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '文件路径不安全'
|
||||||
|
}), 400
|
||||||
|
|
||||||
# 保存文件
|
# 保存文件
|
||||||
image_file.save(file_path)
|
image_file.save(file_path)
|
||||||
|
|
||||||
# 保存相对路径到数据库
|
# 保存相对路径到数据库
|
||||||
image_path = f"/static/producepic/{filename}"
|
image_path = f"/static/producepic/{filename}"
|
||||||
|
|
||||||
# 处理JSON数据
|
# 处理数据:如果是multipart请求,只从form获取;否则尝试JSON
|
||||||
data = request.form.get('data')
|
data = {}
|
||||||
if data:
|
if is_multipart or request.files:
|
||||||
|
# multipart/form-data 请求:从form中获取数据
|
||||||
|
data_str = request.form.get('data')
|
||||||
|
if data_str:
|
||||||
import json
|
import json
|
||||||
data = json.loads(data)
|
try:
|
||||||
else:
|
data = json.loads(data_str)
|
||||||
data = request.get_json() or {}
|
except json.JSONDecodeError:
|
||||||
|
current_app.logger.warning(f"无法解析form中的data字段: {data_str[:100]}")
|
||||||
|
data = {}
|
||||||
|
|
||||||
# 如果仍然没有数据,尝试从form中获取
|
# 如果仍然没有数据,尝试从form中直接获取各个字段
|
||||||
if not data:
|
if not data:
|
||||||
data = {
|
data = {
|
||||||
'product_name': request.form.get('product_name', ''),
|
'product_name': request.form.get('product_name', ''),
|
||||||
'description': request.form.get('description', ''),
|
'description': request.form.get('description', ''),
|
||||||
'product_id': request.form.get('product_id', '')
|
'product_id': request.form.get('product_id', ''),
|
||||||
|
'status': request.form.get('status', '1')
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
# application/json 请求:从JSON获取数据
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
if not data or not data.get('product_name', '').strip():
|
if not data or not data.get('product_name', '').strip():
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@ -170,6 +253,14 @@ def create_product():
|
|||||||
product_name = data.get('product_name', '').strip()
|
product_name = data.get('product_name', '').strip()
|
||||||
description = data.get('description', '').strip()
|
description = data.get('description', '').strip()
|
||||||
custom_id = data.get('product_id', '').strip()
|
custom_id = data.get('product_id', '').strip()
|
||||||
|
# 处理 status 字段:确保是整数类型
|
||||||
|
status = data.get('status', 1)
|
||||||
|
try:
|
||||||
|
status = int(status) if status else 1
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
status = 1
|
||||||
|
# 确保 status 只能是 0 或 1
|
||||||
|
status = 1 if status not in [0, 1] else status
|
||||||
|
|
||||||
if not product_name:
|
if not product_name:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@ -192,7 +283,7 @@ def create_product():
|
|||||||
product_name=product_name,
|
product_name=product_name,
|
||||||
description=description,
|
description=description,
|
||||||
image_path=image_path,
|
image_path=image_path,
|
||||||
status=1
|
status=status
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.add(product)
|
db.session.add(product)
|
||||||
@ -254,40 +345,91 @@ def update_product(product_id):
|
|||||||
'message': '产品不存在'
|
'message': '产品不存在'
|
||||||
}), 404
|
}), 404
|
||||||
|
|
||||||
|
# 检查请求类型
|
||||||
|
is_multipart = request.content_type and 'multipart/form-data' in request.content_type
|
||||||
|
|
||||||
# 处理文件上传
|
# 处理文件上传
|
||||||
if 'image' in request.files:
|
if 'image' in request.files:
|
||||||
image_file = request.files['image']
|
image_file = request.files['image']
|
||||||
if image_file and image_file.filename:
|
if image_file and image_file.filename:
|
||||||
# 确保上传目录存在
|
# 验证文件类型(扩展名和MIME类型)
|
||||||
upload_folder = os.path.join(current_app.static_folder, 'producepic')
|
is_valid, error_msg = validate_image_file(image_file, image_file.filename)
|
||||||
|
if not is_valid:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': error_msg
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 验证文件大小(优先使用content_length)
|
||||||
|
file_size = None
|
||||||
|
if request.content_length:
|
||||||
|
file_size = request.content_length
|
||||||
|
else:
|
||||||
|
image_file.seek(0, os.SEEK_END)
|
||||||
|
file_size = image_file.tell()
|
||||||
|
image_file.seek(0)
|
||||||
|
|
||||||
|
if file_size and file_size > MAX_IMAGE_SIZE:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'文件大小超过限制,最大允许 {MAX_IMAGE_SIZE / (1024*1024):.1f}MB'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 确保上传目录存在(使用绝对路径)
|
||||||
|
static_folder = os.path.abspath(current_app.static_folder)
|
||||||
|
upload_folder = os.path.join(static_folder, 'producepic')
|
||||||
|
if not upload_folder.startswith(static_folder):
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '无效的上传路径'
|
||||||
|
}), 400
|
||||||
|
|
||||||
os.makedirs(upload_folder, exist_ok=True)
|
os.makedirs(upload_folder, exist_ok=True)
|
||||||
|
|
||||||
# 生成唯一文件名
|
# 生成安全的唯一文件名
|
||||||
import uuid
|
import uuid
|
||||||
filename = f"{uuid.uuid4().hex}_{image_file.filename}"
|
original_filename = secure_filename(image_file.filename)
|
||||||
|
file_ext = original_filename.rsplit('.', 1)[1].lower() if '.' in original_filename else 'jpg'
|
||||||
|
filename = f"{uuid.uuid4().hex}.{file_ext}"
|
||||||
file_path = os.path.join(upload_folder, filename)
|
file_path = os.path.join(upload_folder, filename)
|
||||||
|
|
||||||
|
# 再次验证路径安全性
|
||||||
|
real_path = os.path.abspath(file_path)
|
||||||
|
if not real_path.startswith(os.path.abspath(upload_folder)):
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '文件路径不安全'
|
||||||
|
}), 400
|
||||||
|
|
||||||
# 保存文件
|
# 保存文件
|
||||||
image_file.save(file_path)
|
image_file.save(file_path)
|
||||||
|
|
||||||
# 保存相对路径到数据库
|
# 保存相对路径到数据库
|
||||||
product.image_path = f"/static/producepic/{filename}"
|
product.image_path = f"/static/producepic/{filename}"
|
||||||
|
|
||||||
# 处理JSON数据
|
# 处理数据:如果是multipart请求,只从form获取;否则尝试JSON
|
||||||
data = request.form.get('data')
|
data = {}
|
||||||
if data:
|
if is_multipart or request.files:
|
||||||
|
# multipart/form-data 请求:从form中获取数据
|
||||||
|
data_str = request.form.get('data')
|
||||||
|
if data_str:
|
||||||
import json
|
import json
|
||||||
data = json.loads(data)
|
try:
|
||||||
else:
|
data = json.loads(data_str)
|
||||||
data = request.get_json() or {}
|
except json.JSONDecodeError:
|
||||||
|
current_app.logger.warning(f"无法解析form中的data字段: {data_str[:100]}")
|
||||||
|
data = {}
|
||||||
|
|
||||||
# 如果仍然没有数据,尝试从form中获取
|
# 如果仍然没有数据,尝试从form中直接获取各个字段
|
||||||
if not data:
|
if not data:
|
||||||
data = {
|
data = {
|
||||||
'product_name': request.form.get('product_name', ''),
|
'product_name': request.form.get('product_name', ''),
|
||||||
'description': request.form.get('description', ''),
|
'description': request.form.get('description', ''),
|
||||||
'status': request.form.get('status', '')
|
'status': request.form.get('status', '')
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
# application/json 请求:从JSON获取数据
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@ -300,7 +442,14 @@ def update_product(product_id):
|
|||||||
if 'description' in data:
|
if 'description' in data:
|
||||||
product.description = data['description'].strip()
|
product.description = data['description'].strip()
|
||||||
if 'status' in data:
|
if 'status' in data:
|
||||||
product.status = data['status']
|
# 确保 status 是整数类型
|
||||||
|
try:
|
||||||
|
status = int(data['status']) if data['status'] else product.status
|
||||||
|
# 确保 status 只能是 0 或 1
|
||||||
|
product.status = 1 if status not in [0, 1] else status
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
# 如果转换失败,保持原值
|
||||||
|
pass
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|||||||
@ -227,11 +227,13 @@ def create_version():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
current_app.logger.error(f"创建版本失败: {str(e)}")
|
|
||||||
# 添加更详细的错误信息
|
|
||||||
import traceback
|
import traceback
|
||||||
|
current_app.logger.error(f"创建版本失败: {str(e)}")
|
||||||
current_app.logger.error(f"创建版本失败详细信息: {traceback.format_exc()}")
|
current_app.logger.error(f"创建版本失败详细信息: {traceback.format_exc()}")
|
||||||
return jsonify({'success': False, 'message': f'服务器内部错误: {str(e)}'}), 500
|
# 生产环境不泄露详细错误信息
|
||||||
|
is_production = current_app.config.get('FLASK_ENV') == 'production' or not current_app.config.get('DEBUG', False)
|
||||||
|
error_message = '服务器内部错误' if is_production else f'服务器内部错误: {str(e)}'
|
||||||
|
return jsonify({'success': False, 'message': error_message}), 500
|
||||||
|
|
||||||
@api_bp.route('/versions/<int:version_id>/publish', methods=['POST'])
|
@api_bp.route('/versions/<int:version_id>/publish', methods=['POST'])
|
||||||
@require_login
|
@require_login
|
||||||
@ -410,10 +412,13 @@ def upload_version_file():
|
|||||||
return jsonify({'success': False, 'message': '文件上传失败'}), 400
|
return jsonify({'success': False, 'message': '文件上传失败'}), 400
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"文件上传失败: {str(e)}")
|
|
||||||
import traceback
|
import traceback
|
||||||
|
current_app.logger.error(f"文件上传失败: {str(e)}")
|
||||||
current_app.logger.error(f"文件上传失败详细信息: {traceback.format_exc()}")
|
current_app.logger.error(f"文件上传失败详细信息: {traceback.format_exc()}")
|
||||||
return jsonify({'success': False, 'message': f'文件上传失败: {str(e)}'}), 500
|
# 生产环境不泄露详细错误信息
|
||||||
|
is_production = current_app.config.get('FLASK_ENV') == 'production' or not current_app.config.get('DEBUG', False)
|
||||||
|
error_message = '文件上传失败,请稍后重试' if is_production else f'文件上传失败: {str(e)}'
|
||||||
|
return jsonify({'success': False, 'message': error_message}), 500
|
||||||
|
|
||||||
def calculate_file_hash(file_path):
|
def calculate_file_hash(file_path):
|
||||||
"""计算文件SHA256哈希值"""
|
"""计算文件SHA256哈希值"""
|
||||||
|
|||||||
@ -19,22 +19,28 @@ def login():
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
current_app.logger.info(f"收到登录请求 - IP: {request.remote_addr}, User-Agent: {request.headers.get('User-Agent')}")
|
current_app.logger.info(f"收到登录请求 - IP: {request.remote_addr}, User-Agent: {request.headers.get('User-Agent')}")
|
||||||
|
|
||||||
# 检查CSRF令牌 - 生产环境部署时更宽松的验证
|
# 检查CSRF令牌 - 至少验证请求来源
|
||||||
csrf_token = request.form.get('csrf_token')
|
csrf_token = request.form.get('csrf_token')
|
||||||
|
origin = request.headers.get('Origin')
|
||||||
|
referer = request.headers.get('Referer')
|
||||||
|
x_requested_with = request.headers.get('X-Requested-With')
|
||||||
|
|
||||||
|
# 如果缺少CSRF令牌,至少验证请求来源
|
||||||
if not csrf_token:
|
if not csrf_token:
|
||||||
current_app.logger.warning(f"登录请求缺少CSRF令牌 - IP: {request.remote_addr}, User-Agent: {request.headers.get('User-Agent')}, Origin: {request.headers.get('Origin')}, Referer: {request.headers.get('Referer')}")
|
current_app.logger.warning(f"登录请求缺少CSRF令牌 - IP: {request.remote_addr}")
|
||||||
# 在生产环境中,如果缺少CSRF令牌,记录警告但继续处理
|
# 对于AJAX请求,至少检查X-Requested-With头
|
||||||
# 这有助于解决域名部署时的CSRF问题
|
|
||||||
if current_app.config.get('FLASK_ENV') == 'production':
|
|
||||||
current_app.logger.info("生产环境: 缺少CSRF令牌但继续处理登录请求")
|
|
||||||
else:
|
|
||||||
# 开发环境严格验证
|
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
|
# AJAX请求必须来自同源或可信源
|
||||||
|
if not origin and not referer:
|
||||||
|
current_app.logger.warning("可疑的AJAX请求:缺少Origin和Referer头")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': '缺少CSRF令牌,请刷新页面后重试'
|
'message': '请求验证失败,请刷新页面后重试'
|
||||||
}), 400
|
}), 400
|
||||||
flash('缺少CSRF令牌,请刷新页面后重试', 'error')
|
# 对于表单提交,检查Referer
|
||||||
|
elif not referer:
|
||||||
|
current_app.logger.warning("可疑的表单提交:缺少Referer头")
|
||||||
|
flash('请求验证失败,请刷新页面后重试', 'error')
|
||||||
return render_template('login.html')
|
return render_template('login.html')
|
||||||
|
|
||||||
username = request.form.get('username', '').strip()
|
username = request.form.get('username', '').strip()
|
||||||
|
|||||||
@ -393,8 +393,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
|
// 检查body是否是FormData,如果是则不设置Content-Type(让浏览器自动设置)
|
||||||
|
const isFormData = options.body instanceof FormData;
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
headers: {
|
headers: isFormData ? {} : {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
credentials: 'same-origin' // 重要: 使用session cookies
|
credentials: 'same-origin' // 重要: 使用session cookies
|
||||||
@ -463,6 +466,19 @@
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTML转义函数 - 防止XSS攻击
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (text == null) return '';
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return String(text).replace(/[&<>"']/g, m => map[m]);
|
||||||
|
}
|
||||||
|
|
||||||
// 显示通知 - 统一使用静态JS中的函数
|
// 显示通知 - 统一使用静态JS中的函数
|
||||||
function showNotification(message, type = 'info') {
|
function showNotification(message, type = 'info') {
|
||||||
// 创建通知元素
|
// 创建通知元素
|
||||||
|
|||||||
@ -144,13 +144,6 @@ function createProduct() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
})
|
})
|
||||||
.then(response => {
|
|
||||||
if (response.status === 401) {
|
|
||||||
window.location.href = '/login';
|
|
||||||
throw new Error('未授权访问');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showNotification('产品创建成功', 'success');
|
showNotification('产品创建成功', 'success');
|
||||||
@ -164,7 +157,10 @@ function createProduct() {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Failed to create product:', error);
|
console.error('Failed to create product:', error);
|
||||||
|
// apiRequest 已经处理了 401 等错误,这里只处理其他错误
|
||||||
|
if (error.message !== 'SESSION_EXPIRED') {
|
||||||
showNotification('网络错误,请稍后重试', 'error');
|
showNotification('网络错误,请稍后重试', 'error');
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
// 恢复按钮状态
|
// 恢复按钮状态
|
||||||
|
|||||||
@ -152,13 +152,6 @@ function updateProduct() {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: formData
|
body: formData
|
||||||
})
|
})
|
||||||
.then(response => {
|
|
||||||
if (response.status === 401) {
|
|
||||||
window.location.href = '/login';
|
|
||||||
throw new Error('未授权访问');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showNotification('产品更新成功', 'success');
|
showNotification('产品更新成功', 'success');
|
||||||
@ -172,7 +165,10 @@ function updateProduct() {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Failed to update product:', error);
|
console.error('Failed to update product:', error);
|
||||||
|
// apiRequest 已经处理了 401 等错误,这里只处理其他错误
|
||||||
|
if (error.message !== 'SESSION_EXPIRED') {
|
||||||
showNotification('网络错误,请稍后重试', 'error');
|
showNotification('网络错误,请稍后重试', 'error');
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
// 恢复按钮状态
|
// 恢复按钮状态
|
||||||
|
|||||||
@ -285,20 +285,20 @@ function renderProductList(products) {
|
|||||||
tbody.innerHTML = products.map(product => `
|
tbody.innerHTML = products.map(product => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" class="product-checkbox" data-product-id="${product.product_id}">
|
<input type="checkbox" class="product-checkbox" data-product-id="${escapeHtml(product.product_id)}">
|
||||||
</td>
|
</td>
|
||||||
<td><code>${product.product_id}</code></td>
|
<td><code>${escapeHtml(product.product_id)}</code></td>
|
||||||
<td>
|
<td>
|
||||||
<strong>${product.product_name}</strong>
|
<strong>${escapeHtml(product.product_name)}</strong>
|
||||||
${product.latest_version ? `<br><small class="text-muted">最新版本: ${product.latest_version}</small>` : ''}
|
${product.latest_version ? `<br><small class="text-muted">最新版本: ${escapeHtml(product.latest_version)}</small>` : ''}
|
||||||
</td>
|
</td>
|
||||||
<td>${product.description || '-'}</td>
|
<td>${escapeHtml(product.description || '-')}</td>
|
||||||
<td>
|
<td>
|
||||||
${product.image_path ? `<img src="${product.image_path}" alt="产品图片" style="max-width: 50px; max-height: 50px;">` : '-'}
|
${product.image_path ? `<img src="${escapeHtml(product.image_path)}" alt="产品图片" style="max-width: 50px; max-height: 50px;">` : '-'}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge ${product.status === 1 ? 'bg-success' : 'bg-secondary'}">
|
<span class="badge ${product.status === 1 ? 'bg-success' : 'bg-secondary'}">
|
||||||
${product.status_name}
|
${escapeHtml(product.status_name)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@ -63,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="bg-light p-3 rounded">
|
<div class="bg-light p-3 rounded">
|
||||||
{{ ticket.content|safe }}
|
{{ ticket.content|e|nl2br }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -336,19 +336,19 @@ function renderTicketList(tickets) {
|
|||||||
tbody.innerHTML = tickets.map(ticket => `
|
tbody.innerHTML = tickets.map(ticket => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" class="ticket-checkbox" data-ticket-id="${ticket.ticket_id}">
|
<input type="checkbox" class="ticket-checkbox" data-ticket-id="${escapeHtml(ticket.ticket_id)}">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code>${ticket.ticket_id}</code>
|
<code>${escapeHtml(ticket.ticket_id)}</code>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/tickets/${ticket.ticket_id}" class="text-decoration-none">
|
<a href="/tickets/${escapeHtml(ticket.ticket_id)}" class="text-decoration-none">
|
||||||
${ticket.title}
|
${escapeHtml(ticket.title)}
|
||||||
</a>
|
</a>
|
||||||
${ticket.reply_count > 0 ? `<span class="badge bg-primary ms-1">${ticket.reply_count}</span>` : ''}
|
${ticket.reply_count > 0 ? `<span class="badge bg-primary ms-1">${ticket.reply_count}</span>` : ''}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
${ticket.product_name || '-'}
|
${escapeHtml(ticket.product_name || '-')}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
${getPriorityBadge(ticket.priority)}
|
${getPriorityBadge(ticket.priority)}
|
||||||
|
|||||||
@ -5,7 +5,12 @@ from logging.handlers import TimedRotatingFileHandler
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""基础配置类"""
|
"""基础配置类"""
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
|
# SECRET_KEY必须从环境变量获取,生产环境不允许使用默认值
|
||||||
|
SECRET_KEY = os.environ.get('SECRET_KEY')
|
||||||
|
if not SECRET_KEY:
|
||||||
|
import sys
|
||||||
|
print("警告: SECRET_KEY未设置,使用不安全的默认值。生产环境必须设置SECRET_KEY环境变量!", file=sys.stderr)
|
||||||
|
SECRET_KEY = 'dev-secret-key-change-in-production'
|
||||||
|
|
||||||
# 数据库配置 - 优先使用环境变量中的DATABASE_URL
|
# 数据库配置 - 优先使用环境变量中的DATABASE_URL
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///instance/kamaxitong.db')
|
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///instance/kamaxitong.db')
|
||||||
|
|||||||
BIN
master.zip
BIN
master.zip
Binary file not shown.
Loading…
Reference in New Issue
Block a user