修复多项安全漏洞和Bug
1. 安全修复: - 修复密码重置接口用户枚举漏洞,统一返回消息防止信息泄露 - 统一密码强度验证为8位以上且包含字母和数字 - 添加第三方账号密码加密存储(Fernet对称加密) - 修复默认管理员弱密码问题,改用随机生成强密码 - 修复管理员回复XSS漏洞,添加HTML转义 - 将MD5哈希替换为SHA256 2. 并发Bug修复: - 修复日志缓存竞态条件,添加锁保护 - 修复截图信号量配置变更后不生效问题 3. 其他改进: - 添加API参数类型验证和边界检查 - 新增crypto_utils.py加密工具模块 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -68,8 +68,8 @@ class APIBrowser:
|
||||
cookies_dir = '/app/data/cookies'
|
||||
os.makedirs(cookies_dir, exist_ok=True)
|
||||
|
||||
# 用用户名的hash作为文件名
|
||||
filename = hashlib.md5(username.encode()).hexdigest() + '.json'
|
||||
# 安全修复:使用SHA256代替MD5作为文件名哈希
|
||||
filename = hashlib.sha256(username.encode()).hexdigest()[:32] + '.json'
|
||||
cookies_path = os.path.join(cookies_dir, filename)
|
||||
|
||||
try:
|
||||
|
||||
119
app.py
119
app.py
@@ -43,6 +43,7 @@ from app_security import (
|
||||
is_safe_path, sanitize_filename, get_client_ip
|
||||
)
|
||||
from app_utils import verify_and_consume_captcha
|
||||
from crypto_utils import encrypt_password as encrypt_account_password
|
||||
|
||||
|
||||
# ========== 时区辅助函数 (Bug #2 fix) ==========
|
||||
@@ -147,19 +148,25 @@ max_concurrent_global = config.MAX_CONCURRENT_GLOBAL
|
||||
user_semaphores = {} # {user_id: Semaphore}
|
||||
global_semaphore = threading.Semaphore(max_concurrent_global)
|
||||
|
||||
# 截图专用信号量:限制同时进行的截图任务数量为1(避免资源竞争)
|
||||
# <EFBFBD><EFBFBD><EFBFBD>图信号量将在首次使用时初始化
|
||||
# 截图专用信号量:限制同时进行的截图任务数量(避免资源竞争)
|
||||
# 信号量将在首次使用时初始化,并支持动态更新
|
||||
screenshot_semaphore = None
|
||||
screenshot_semaphore_lock = threading.Lock()
|
||||
screenshot_semaphore_size = 0 # 记录当前信号量大小
|
||||
|
||||
def get_screenshot_semaphore():
|
||||
"""获取截图信号量(懒加载,根据配置动态创建)"""
|
||||
global screenshot_semaphore
|
||||
"""获取截图信号量(懒加载,根据配置动态创建,支持配置更新)"""
|
||||
global screenshot_semaphore, screenshot_semaphore_size
|
||||
with screenshot_semaphore_lock:
|
||||
config = database.get_system_config()
|
||||
max_concurrent = config.get('max_screenshot_concurrent', 3)
|
||||
if screenshot_semaphore is None:
|
||||
|
||||
# 安全修复:支持配置变更后重新创建信号量
|
||||
if screenshot_semaphore is None or screenshot_semaphore_size != max_concurrent:
|
||||
screenshot_semaphore = threading.Semaphore(max_concurrent)
|
||||
screenshot_semaphore_size = max_concurrent
|
||||
print(f"[截图信号量] 已更新为 {max_concurrent} 并发")
|
||||
|
||||
return screenshot_semaphore, max_concurrent
|
||||
|
||||
|
||||
@@ -357,36 +364,32 @@ def log_to_client(message, user_id=None, account_id=None):
|
||||
|
||||
# 如果指定了user_id,则缓存到该用户的日志
|
||||
if user_id:
|
||||
# 安全修复:使用锁保护日志缓存操作,防止竞态条件
|
||||
global log_cache_total_count
|
||||
if user_id not in log_cache:
|
||||
log_cache[user_id] = []
|
||||
log_cache[user_id].append(log_data)
|
||||
log_cache_total_count += 1
|
||||
with log_cache_lock:
|
||||
if user_id not in log_cache:
|
||||
log_cache[user_id] = []
|
||||
log_cache[user_id].append(log_data)
|
||||
log_cache_total_count += 1
|
||||
|
||||
# 持久化到数据库 (已禁用,使用task_logs表代替)
|
||||
# try:
|
||||
# database.save_operation_log(user_id, message, account_id, 'INFO')
|
||||
# except Exception as e:
|
||||
# print(f"保存日志到数据库失败: {e}")
|
||||
# 单用户限制
|
||||
if len(log_cache[user_id]) > MAX_LOGS_PER_USER:
|
||||
log_cache[user_id].pop(0)
|
||||
log_cache_total_count -= 1
|
||||
|
||||
# 单用户限制
|
||||
if len(log_cache[user_id]) > MAX_LOGS_PER_USER:
|
||||
log_cache[user_id].pop(0)
|
||||
log_cache_total_count -= 1
|
||||
|
||||
# 全局限制 - 如果超过总数限制,清理日志最多的用户
|
||||
while log_cache_total_count > MAX_TOTAL_LOGS:
|
||||
if log_cache:
|
||||
max_user = max(log_cache.keys(), key=lambda u: len(log_cache[u]))
|
||||
if log_cache[max_user]:
|
||||
log_cache[max_user].pop(0)
|
||||
log_cache_total_count -= 1
|
||||
# 全局限制 - 如果超过总数限制,清理日志最多的用户
|
||||
while log_cache_total_count > MAX_TOTAL_LOGS:
|
||||
if log_cache:
|
||||
max_user = max(log_cache.keys(), key=lambda u: len(log_cache[u]))
|
||||
if log_cache[max_user]:
|
||||
log_cache[max_user].pop(0)
|
||||
log_cache_total_count -= 1
|
||||
else:
|
||||
break
|
||||
else:
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
# 发送到该用户的room
|
||||
# 发送到该用户的room(在锁外执行,避免死锁)
|
||||
socketio.emit('log', log_data, room=f'user_{user_id}')
|
||||
|
||||
# 控制台日志:添加账号短标识便于区分
|
||||
@@ -1135,8 +1138,10 @@ def admin_reset_password_route(user_id):
|
||||
if not new_password:
|
||||
return jsonify({"error": "新密码不能为空"}), 400
|
||||
|
||||
if len(new_password) < 6:
|
||||
return jsonify({"error": "密码长度不能少于6位"}), 400
|
||||
# 安全修复:统一密码强度要求为8位以上且包含字母和数字
|
||||
is_valid, error_msg = validate_password(new_password)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
if database.admin_reset_user_password(user_id, new_password):
|
||||
return jsonify({"message": "密码重置成功"})
|
||||
@@ -1184,24 +1189,27 @@ def request_password_reset():
|
||||
if not username or not new_password:
|
||||
return jsonify({"error": "用户名和新密码不能为空"}), 400
|
||||
|
||||
if len(new_password) < 6:
|
||||
return jsonify({"error": "密码长度不能少于6位"}), 400
|
||||
# 安全修复:统一密码强度要求为8位以上且包含字母和数字
|
||||
is_valid, error_msg = validate_password(new_password)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
# 验证用户存在
|
||||
# 安全修复:防止用户枚举,统一返回成功消息
|
||||
# 无论用户是否存在都返回相同消息
|
||||
user = database.get_user_by_username(username)
|
||||
if not user:
|
||||
return jsonify({"error": "用户不存在"}), 404
|
||||
|
||||
# 如果提供了邮箱,验证邮箱是否匹配
|
||||
if email and user.get('email') != email:
|
||||
return jsonify({"error": "邮箱不匹配"}), 400
|
||||
# 如果用户存在且邮箱匹配(或未提供邮箱),则创建重置申请
|
||||
if user:
|
||||
# 如果提供了邮箱,验证邮箱是否匹配
|
||||
if email and user.get('email') != email:
|
||||
# 邮箱不匹配,但不透露具体原因
|
||||
pass
|
||||
else:
|
||||
# 创建重置申请
|
||||
database.create_password_reset_request(user['id'], new_password)
|
||||
|
||||
# 创建重置申请
|
||||
request_id = database.create_password_reset_request(user['id'], new_password)
|
||||
if request_id:
|
||||
return jsonify({"message": "密码重置申请已提交,请等待管理员审核"})
|
||||
else:
|
||||
return jsonify({"error": "申请提交失败"}), 500
|
||||
# 无论成功与否都返回相同消息,防止用户枚举
|
||||
return jsonify({"message": "如果账号存在,密码重置申请已提交,请等待管理员审核"})
|
||||
|
||||
|
||||
# ==================== 账号管理API (用户隔离) ====================
|
||||
@@ -1274,8 +1282,16 @@ def get_my_feedbacks():
|
||||
def get_all_feedbacks():
|
||||
"""管理员获取所有反馈"""
|
||||
status = request.args.get('status')
|
||||
limit = int(request.args.get('limit', 100))
|
||||
offset = int(request.args.get('offset', 0))
|
||||
|
||||
# 安全修复:参数类型验证,防止类型转换异常
|
||||
try:
|
||||
limit = int(request.args.get('limit', 100))
|
||||
offset = int(request.args.get('offset', 0))
|
||||
# 限制最大值防止DoS
|
||||
limit = min(max(1, limit), 1000)
|
||||
offset = max(0, offset)
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "无效的分页参数"}), 400
|
||||
|
||||
feedbacks = database.get_bug_feedbacks(limit=limit, offset=offset, status_filter=status)
|
||||
stats = database.get_feedback_stats()
|
||||
@@ -1409,6 +1425,9 @@ def update_account(account_id):
|
||||
if not new_password:
|
||||
return jsonify({"error": "密码不能为空"}), 400
|
||||
|
||||
# 安全修复:加密存储第三方账号密码
|
||||
encrypted_password = encrypt_account_password(new_password)
|
||||
|
||||
# 更新数据库
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -1416,15 +1435,15 @@ def update_account(account_id):
|
||||
UPDATE accounts
|
||||
SET password = ?, remember = ?
|
||||
WHERE id = ?
|
||||
''', (new_password, new_remember, account_id))
|
||||
''', (encrypted_password, new_remember, account_id))
|
||||
conn.commit()
|
||||
|
||||
# 重置账号登录状态(密码修改后恢复active状态)
|
||||
database.reset_account_login_status(account_id)
|
||||
logger.info(f"[账号更新] 用户 {user_id} 修改了账号 {account.username} 的密码,已重置登录状态")
|
||||
|
||||
# 更新内存中的账号信息
|
||||
account.password = new_password
|
||||
# 更新内存中的账号信息(内存中保存明文,用于自动化登录)
|
||||
account._password = new_password
|
||||
account.remember = new_remember
|
||||
|
||||
log_to_client(f"账号 {account.username} 信息已更新,登录状态已重置", user_id)
|
||||
|
||||
169
crypto_utils.py
Normal file
169
crypto_utils.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
加密工具模块
|
||||
用于加密存储敏感信息(如第三方账号密码)
|
||||
使用Fernet对称加密
|
||||
"""
|
||||
|
||||
import os
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
|
||||
|
||||
# 加密密钥文件路径
|
||||
ENCRYPTION_KEY_FILE = 'data/encryption_key.bin'
|
||||
ENCRYPTION_SALT_FILE = 'data/encryption_salt.bin'
|
||||
|
||||
|
||||
def _get_or_create_salt():
|
||||
"""获取或创建盐值"""
|
||||
salt_path = Path(ENCRYPTION_KEY_FILE).parent / 'encryption_salt.bin'
|
||||
if salt_path.exists():
|
||||
with open(salt_path, 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
# 生成新的盐值
|
||||
salt = os.urandom(16)
|
||||
os.makedirs(salt_path.parent, exist_ok=True)
|
||||
with open(salt_path, 'wb') as f:
|
||||
f.write(salt)
|
||||
return salt
|
||||
|
||||
|
||||
def _derive_key(password: bytes, salt: bytes) -> bytes:
|
||||
"""从密码派生加密密钥"""
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=480000, # OWASP推荐的迭代次数
|
||||
)
|
||||
return base64.urlsafe_b64encode(kdf.derive(password))
|
||||
|
||||
|
||||
def get_encryption_key():
|
||||
"""获取加密密钥(优先环境变量,否则从文件读取或生成)"""
|
||||
# 优先从环境变量读取
|
||||
env_key = os.environ.get('ENCRYPTION_KEY')
|
||||
if env_key:
|
||||
# 使用环境变量中的密钥派生Fernet密钥
|
||||
salt = _get_or_create_salt()
|
||||
return _derive_key(env_key.encode(), salt)
|
||||
|
||||
# 从文件读取
|
||||
key_path = Path(ENCRYPTION_KEY_FILE)
|
||||
if key_path.exists():
|
||||
with open(key_path, 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
# 生成新的密钥
|
||||
key = Fernet.generate_key()
|
||||
os.makedirs(key_path.parent, exist_ok=True)
|
||||
with open(key_path, 'wb') as f:
|
||||
f.write(key)
|
||||
print(f"[安全] 已生成新的加密密钥并保存到 {ENCRYPTION_KEY_FILE}")
|
||||
return key
|
||||
|
||||
|
||||
# 全局Fernet实例
|
||||
_fernet = None
|
||||
|
||||
|
||||
def _get_fernet():
|
||||
"""获取Fernet加密器(懒加载)"""
|
||||
global _fernet
|
||||
if _fernet is None:
|
||||
key = get_encryption_key()
|
||||
_fernet = Fernet(key)
|
||||
return _fernet
|
||||
|
||||
|
||||
def encrypt_password(plain_password: str) -> str:
|
||||
"""
|
||||
加密密码
|
||||
|
||||
Args:
|
||||
plain_password: 明文密码
|
||||
|
||||
Returns:
|
||||
str: 加密后的密码(base64编码)
|
||||
"""
|
||||
if not plain_password:
|
||||
return ''
|
||||
|
||||
fernet = _get_fernet()
|
||||
encrypted = fernet.encrypt(plain_password.encode('utf-8'))
|
||||
return encrypted.decode('utf-8')
|
||||
|
||||
|
||||
def decrypt_password(encrypted_password: str) -> str:
|
||||
"""
|
||||
解密密码
|
||||
|
||||
Args:
|
||||
encrypted_password: 加密的密码
|
||||
|
||||
Returns:
|
||||
str: 明文密码
|
||||
"""
|
||||
if not encrypted_password:
|
||||
return ''
|
||||
|
||||
try:
|
||||
fernet = _get_fernet()
|
||||
decrypted = fernet.decrypt(encrypted_password.encode('utf-8'))
|
||||
return decrypted.decode('utf-8')
|
||||
except Exception as e:
|
||||
# 解密失败,可能是旧的明文密码
|
||||
print(f"[警告] 密码解密失败,可能是未加密的旧数据: {e}")
|
||||
return encrypted_password
|
||||
|
||||
|
||||
def is_encrypted(password: str) -> bool:
|
||||
"""
|
||||
检查密码是否已加密
|
||||
Fernet加密的数据以'gAAAAA'开头
|
||||
|
||||
Args:
|
||||
password: 要检查的密码
|
||||
|
||||
Returns:
|
||||
bool: 是否已加密
|
||||
"""
|
||||
if not password:
|
||||
return False
|
||||
# Fernet加密的数据是base64编码,以'gAAAAA'开头
|
||||
return password.startswith('gAAAAA')
|
||||
|
||||
|
||||
def migrate_password(password: str) -> str:
|
||||
"""
|
||||
迁移密码:如果是明文则加密,如果已加密则保持不变
|
||||
|
||||
Args:
|
||||
password: 密码(可能是明文或已加密)
|
||||
|
||||
Returns:
|
||||
str: 加密后的密码
|
||||
"""
|
||||
if is_encrypted(password):
|
||||
return password
|
||||
return encrypt_password(password)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试加密解密
|
||||
test_password = "test_password_123"
|
||||
|
||||
print(f"原始密码: {test_password}")
|
||||
encrypted = encrypt_password(test_password)
|
||||
print(f"加密后: {encrypted}")
|
||||
decrypted = decrypt_password(encrypted)
|
||||
print(f"解密后: {decrypted}")
|
||||
print(f"加密解密成功: {test_password == decrypted}")
|
||||
print(f"是否已加密: {is_encrypted(encrypted)}")
|
||||
print(f"明文是否加密: {is_encrypted(test_password)}")
|
||||
58
database.py
58
database.py
@@ -26,6 +26,7 @@ from password_utils import (
|
||||
verify_password_sha256
|
||||
)
|
||||
from app_config import get_config
|
||||
from crypto_utils import encrypt_password, decrypt_password, migrate_password
|
||||
|
||||
# 获取配置
|
||||
config = get_config()
|
||||
@@ -473,7 +474,14 @@ def _migrate_to_v5(conn):
|
||||
# ==================== 管理员相关 ====================
|
||||
|
||||
def ensure_default_admin():
|
||||
"""确保存在默认管理员账号 admin/admin"""
|
||||
"""确保存在默认管理员账号
|
||||
|
||||
安全修复:使用随机生成的强密码代替弱密码'admin'
|
||||
首次运行时会将密码打印到控制台,请及时修改
|
||||
"""
|
||||
import secrets
|
||||
import string
|
||||
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
@@ -482,14 +490,22 @@ def ensure_default_admin():
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result['count'] == 0:
|
||||
# 创建默认管理员 admin/admin
|
||||
default_password_hash = hash_password_bcrypt('admin')
|
||||
# 安全修复:生成随机强密码(12位,包含大小写字母和数字)
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
random_password = ''.join(secrets.choice(alphabet) for _ in range(12))
|
||||
|
||||
default_password_hash = hash_password_bcrypt(random_password)
|
||||
cursor.execute(
|
||||
'INSERT INTO admins (username, password_hash) VALUES (?, ?)',
|
||||
('admin', default_password_hash)
|
||||
)
|
||||
conn.commit()
|
||||
print("✓ 已创建默认管理员账号 (admin/admin)")
|
||||
print("=" * 60)
|
||||
print("安全提醒:已创建默认管理员账号")
|
||||
print(f"用户名: admin")
|
||||
print(f"密码: {random_password}")
|
||||
print("请立即登录后修改密码!")
|
||||
print("=" * 60)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -812,32 +828,45 @@ def delete_user(user_id):
|
||||
# ==================== 账号相关 ====================
|
||||
|
||||
def create_account(user_id, account_id, username, password, remember=True, remark=''):
|
||||
"""创建账号"""
|
||||
"""创建账号(密码加密存储)"""
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
# 安全修复:加密存储第三方账号密码
|
||||
encrypted_password = encrypt_password(password)
|
||||
cursor.execute('''
|
||||
INSERT INTO accounts (id, user_id, username, password, remember, remark)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
''', (account_id, user_id, username, password, 1 if remember else 0, remark))
|
||||
''', (account_id, user_id, username, encrypted_password, 1 if remember else 0, remark))
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
|
||||
def get_user_accounts(user_id):
|
||||
"""获取用户的所有账号"""
|
||||
"""获取用户的所有账号(自动解密密码)"""
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT * FROM accounts WHERE user_id = ? ORDER BY created_at DESC', (user_id,))
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
accounts = []
|
||||
for row in cursor.fetchall():
|
||||
account = dict(row)
|
||||
# 安全修复:解密第三方账号密码(兼容旧数据)
|
||||
account['password'] = decrypt_password(account.get('password', ''))
|
||||
accounts.append(account)
|
||||
return accounts
|
||||
|
||||
|
||||
def get_account(account_id):
|
||||
"""获取单个账号"""
|
||||
"""获取单个账号(自动解密密码)"""
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT * FROM accounts WHERE id = ?', (account_id,))
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
if row:
|
||||
account = dict(row)
|
||||
# 安全修复:解密第三方账号密码(兼容旧数据)
|
||||
account['password'] = decrypt_password(account.get('password', ''))
|
||||
return account
|
||||
return None
|
||||
|
||||
|
||||
def update_account_remark(account_id, remark):
|
||||
@@ -1483,17 +1512,22 @@ def get_feedback_by_id(feedback_id):
|
||||
|
||||
|
||||
def reply_feedback(feedback_id, admin_reply):
|
||||
"""管理员回复反馈"""
|
||||
"""管理员回复反馈(带XSS防护)"""
|
||||
from app_security import escape_html
|
||||
|
||||
with db_pool.get_db() as conn:
|
||||
cursor = conn.cursor()
|
||||
cst_tz = pytz.timezone("Asia/Shanghai")
|
||||
cst_time = datetime.now(cst_tz).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 安全修复:转义管理员回复,防止存储型XSS攻击
|
||||
safe_reply = escape_html(admin_reply) if admin_reply else ''
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE bug_feedbacks
|
||||
SET admin_reply = ?, status = 'replied', replied_at = ?
|
||||
WHERE id = ?
|
||||
''', (admin_reply, cst_time, feedback_id))
|
||||
''', (safe_reply, cst_time, feedback_id))
|
||||
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
@@ -166,9 +166,9 @@ class PlaywrightAutomation:
|
||||
"""获取cookies文件路径"""
|
||||
import os
|
||||
os.makedirs(self.COOKIES_DIR, exist_ok=True)
|
||||
# 用用户名的hash作为文件名,避免特殊字符问题
|
||||
# 安全修复:使用SHA256代替MD5作为文件名哈希
|
||||
import hashlib
|
||||
filename = hashlib.md5(username.encode()).hexdigest() + '.json'
|
||||
filename = hashlib.sha256(username.encode()).hexdigest()[:32] + '.json'
|
||||
return os.path.join(self.COOKIES_DIR, filename)
|
||||
|
||||
def save_cookies(self, username: str):
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
<form id="resetPasswordForm" onsubmit="handleResetPassword(event)">
|
||||
<div class="form-group"><label>用户名</label><input type="text" id="resetUsername" placeholder="请输入用户名" required></div>
|
||||
<div class="form-group"><label>邮箱(可选)</label><input type="email" id="resetEmail" placeholder="用于验证身份"></div>
|
||||
<div class="form-group"><label>新密码</label><input type="password" id="resetNewPassword" placeholder="至少6位" required></div>
|
||||
<div class="form-group"><label>新密码</label><input type="password" id="resetNewPassword" placeholder="至少8位,包含字母和数字" required></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -245,7 +245,8 @@
|
||||
const successDiv = document.getElementById('modalSuccessMessage');
|
||||
errorDiv.style.display = 'none'; successDiv.style.display = 'none';
|
||||
if (!username || !newPassword) { errorDiv.textContent = '用户名和新密码不能为空'; errorDiv.style.display = 'block'; return; }
|
||||
if (newPassword.length < 6) { errorDiv.textContent = '密码长度至少6位'; errorDiv.style.display = 'block'; return; }
|
||||
if (newPassword.length < 8) { errorDiv.textContent = '密码长度至少8位'; errorDiv.style.display = 'block'; return; }
|
||||
if (!/[a-zA-Z]/.test(newPassword) || !/\d/.test(newPassword)) { errorDiv.textContent = '密码必须包含字母和数字'; errorDiv.style.display = 'block'; return; }
|
||||
try {
|
||||
const response = await fetch('/api/reset_password_request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, email, new_password: newPassword }) });
|
||||
const data = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user