feat: 删除SFTP上传工具,修复OSS配置bug
主要变更: - 删除管理员工具栏及上传工具相关功能(后端API + 前端UI) - 删除upload-tool目录及相关文件 - 修复OSS配置测试连接bug(testUser缺少has_oss_config标志) - 新增backend/utils加密和缓存工具模块 - 更新.gitignore排除测试报告文件 技术改进: - 统一使用OSS存储,废弃SFTP上传方式 - 修复OSS配置保存和测试连接时的错误处理 - 完善代码库文件管理,排除临时报告文件
This commit is contained in:
@@ -7,6 +7,18 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// 引入加密工具(用于敏感数据加密存储)
|
||||
const { encryptSecret, decryptSecret, validateEncryption } = require('./utils/encryption');
|
||||
|
||||
// 验证加密系统在启动时正常工作
|
||||
try {
|
||||
validateEncryption();
|
||||
} catch (error) {
|
||||
console.error('[安全] 加密系统验证失败,服务无法启动');
|
||||
console.error('[安全] 请检查 ENCRYPTION_KEY 配置');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 数据库路径配置
|
||||
// 优先使用环境变量 DATABASE_PATH,默认为 ./data/database.db
|
||||
const DEFAULT_DB_PATH = path.join(__dirname, 'data', 'database.db');
|
||||
@@ -26,9 +38,147 @@ console.log(`[数据库] 路径: ${dbPath}`);
|
||||
// 创建或连接数据库
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// 启用外键约束
|
||||
// ===== 性能优化配置(P0 优先级修复) =====
|
||||
|
||||
// 1. 启用 WAL 模式(Write-Ahead Logging)
|
||||
// 优势:支持并发读写,大幅提升数据库性能
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
// 2. 配置同步模式为 NORMAL
|
||||
// 性能提升:在安全性和性能之间取得平衡,比 FULL 模式快很多
|
||||
db.pragma('synchronous = NORMAL');
|
||||
|
||||
// 3. 增加缓存大小到 64MB
|
||||
// 性能提升:减少磁盘 I/O,缓存更多数据页和索引页
|
||||
// 负值表示 KB,-64000 = 64MB
|
||||
db.pragma('cache_size = -64000');
|
||||
|
||||
// 4. 临时表存储在内存中
|
||||
// 性能提升:避免临时表写入磁盘,加速排序和分组操作
|
||||
db.pragma('temp_store = MEMORY');
|
||||
|
||||
// 5. 启用外键约束
|
||||
db.pragma('foreign_keys = ON');
|
||||
|
||||
console.log('[数据库性能优化] ✓ WAL 模式已启用');
|
||||
console.log('[数据库性能优化] ✓ 同步模式: NORMAL');
|
||||
console.log('[数据库性能优化] ✓ 缓存大小: 64MB');
|
||||
console.log('[数据库性能优化] ✓ 临时表存储: 内存');
|
||||
|
||||
// ===== 第二轮修复:WAL 文件定期清理机制 =====
|
||||
|
||||
/**
|
||||
* 执行数据库检查点(Checkpoint)
|
||||
* 将 WAL 文件中的内容写入主数据库文件,并清理 WAL
|
||||
* @param {Database} database - 数据库实例
|
||||
* @returns {boolean} 是否成功执行
|
||||
*/
|
||||
function performCheckpoint(database = db) {
|
||||
try {
|
||||
// 执行 checkpoint(将 WAL 内容合并到主数据库)
|
||||
database.pragma('wal_checkpoint(PASSIVE)');
|
||||
|
||||
// 获取 WAL 文件大小信息
|
||||
const walInfo = database.pragma('wal_checkpoint(TRUNCATE)', { simple: true });
|
||||
|
||||
console.log('[WAL清理] ✓ 检查点完成');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[WAL清理] ✗ 检查点失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WAL 文件大小
|
||||
* @param {Database} database - 数据库实例
|
||||
* @returns {number} WAL 文件大小(字节)
|
||||
*/
|
||||
function getWalFileSize(database = db) {
|
||||
try {
|
||||
const dbPath = database.name;
|
||||
const walPath = `${dbPath}-wal`;
|
||||
|
||||
if (fs.existsSync(walPath)) {
|
||||
const stats = fs.statSync(walPath);
|
||||
return stats.size;
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (error) {
|
||||
console.error('[WAL清理] 获取 WAL 文件大小失败:', error.message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动时检查 WAL 文件大小,如果超过阈值则执行清理
|
||||
* @param {number} threshold - 阈值(字节),默认 100MB
|
||||
*/
|
||||
function checkWalOnStartup(threshold = 100 * 1024 * 1024) {
|
||||
try {
|
||||
const walSize = getWalFileSize();
|
||||
|
||||
if (walSize > threshold) {
|
||||
console.warn(`[WAL清理] ⚠ 启动时检测到 WAL 文件过大: ${(walSize / 1024 / 1024).toFixed(2)}MB`);
|
||||
console.log('[WAL清理] 正在执行自动清理...');
|
||||
|
||||
const success = performCheckpoint();
|
||||
|
||||
if (success) {
|
||||
const newSize = getWalFileSize();
|
||||
console.log(`[WAL清理] ✓ 清理完成: ${walSize} → ${newSize} 字节`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[WAL清理] ✓ WAL 文件大小正常: ${(walSize / 1024 / 1024).toFixed(2)}MB`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WAL清理] 启动检查失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置定期 WAL 检查点
|
||||
* 每隔指定时间自动执行一次检查点,防止 WAL 文件无限增长
|
||||
* @param {number} intervalHours - 间隔时间(小时),默认 24 小时
|
||||
* @returns {NodeJS.Timeout} 定时器 ID,可用于取消
|
||||
*/
|
||||
function schedulePeriodicCheckpoint(intervalHours = 24) {
|
||||
const intervalMs = intervalHours * 60 * 60 * 1000;
|
||||
|
||||
const timerId = setInterval(() => {
|
||||
const walSize = getWalFileSize();
|
||||
|
||||
console.log(`[WAL清理] 定期检查点执行中... (当前 WAL: ${(walSize / 1024 / 1024).toFixed(2)}MB)`);
|
||||
|
||||
performCheckpoint();
|
||||
}, intervalMs);
|
||||
|
||||
console.log(`[WAL清理] ✓ 定期检查点已启用: 每 ${intervalHours} 小时执行一次`);
|
||||
|
||||
return timerId;
|
||||
}
|
||||
|
||||
// 立即执行启动时检查
|
||||
checkWalOnStartup(100 * 1024 * 1024); // 100MB 阈值
|
||||
|
||||
// 启动定期检查点(24 小时)
|
||||
let walCheckpointTimer = null;
|
||||
if (process.env.WAL_CHECKPOINT_ENABLED !== 'false') {
|
||||
const interval = parseInt(process.env.WAL_CHECKPOINT_INTERVAL_HOURS || '24', 10);
|
||||
walCheckpointTimer = schedulePeriodicCheckpoint(interval);
|
||||
} else {
|
||||
console.log('[WAL清理] 定期检查点已禁用(WAL_CHECKPOINT_ENABLED=false)');
|
||||
}
|
||||
|
||||
// 导出 WAL 管理函数
|
||||
const WalManager = {
|
||||
performCheckpoint,
|
||||
getWalFileSize,
|
||||
checkWalOnStartup,
|
||||
schedulePeriodicCheckpoint
|
||||
};
|
||||
|
||||
// 初始化数据库表
|
||||
function initDatabase() {
|
||||
// 用户表
|
||||
@@ -95,14 +245,36 @@ function initDatabase() {
|
||||
|
||||
// 创建索引
|
||||
db.exec(`
|
||||
-- 基础索引
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_upload_api_key ON users(upload_api_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_shares_code ON shares(share_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_shares_user ON shares(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_shares_expires ON shares(expires_at);
|
||||
|
||||
-- ===== 性能优化:复合索引(P0 优先级修复) =====
|
||||
|
||||
-- 1. 分享链接复合索引:share_code + expires_at
|
||||
-- 优势:加速分享码查询(最常见的操作),同时过滤过期链接
|
||||
-- 使用场景:ShareDB.findByCode, 分享访问验证
|
||||
CREATE INDEX IF NOT EXISTS idx_shares_code_expires ON shares(share_code, expires_at);
|
||||
|
||||
-- 注意:system_logs 表的复合索引在表创建后创建(第372行之后)
|
||||
-- 2. 活动日志复合索引:user_id + created_at
|
||||
-- 优势:快速查询用户最近的活动记录,支持时间范围过滤
|
||||
-- 使用场景:用户活动历史、审计日志查询
|
||||
-- CREATE INDEX IF NOT EXISTS idx_logs_user_created ON system_logs(user_id, created_at);
|
||||
|
||||
-- 3. 文件复合索引:user_id + parent_path
|
||||
-- 注意:当前系统使用 OSS,不直接存储文件元数据到数据库
|
||||
-- 如果未来需要文件系统功能,此索引将优化目录浏览性能
|
||||
-- CREATE INDEX IF NOT EXISTS idx_files_user_parent ON files(user_id, parent_path);
|
||||
`);
|
||||
|
||||
console.log('[数据库性能优化] ✓ 基础索引已创建');
|
||||
console.log(' - idx_shares_code_expires: 分享码+过期时间');
|
||||
|
||||
// 数据库迁移:添加upload_api_key字段(如果不存在)
|
||||
try {
|
||||
const columns = db.prepare("PRAGMA table_info(users)").all();
|
||||
@@ -197,8 +369,30 @@ function initDatabase() {
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_category ON system_logs(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_level ON system_logs(level);
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_user_id ON system_logs(user_id);
|
||||
|
||||
-- ===== 性能优化:复合索引(P0 优先级修复) =====
|
||||
-- 活动日志复合索引:user_id + created_at
|
||||
-- 优势:快速查询用户最近的活动记录,支持时间范围过滤
|
||||
-- 使用场景:用户活动历史、审计日志查询
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_user_created ON system_logs(user_id, created_at);
|
||||
`);
|
||||
|
||||
console.log('[数据库性能优化] ✓ 日志表复合索引已创建');
|
||||
console.log(' - idx_logs_user_created: 用户+创建时间');
|
||||
|
||||
// 数据库迁移:添加 storage_used 字段(P0 性能优化)
|
||||
try {
|
||||
const columns = db.prepare("PRAGMA table_info(users)").all();
|
||||
const hasStorageUsed = columns.some(col => col.name === 'storage_used');
|
||||
|
||||
if (!hasStorageUsed) {
|
||||
db.exec(`ALTER TABLE users ADD COLUMN storage_used INTEGER DEFAULT 0`);
|
||||
console.log('[数据库迁移] ✓ storage_used 字段已添加');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[数据库迁移] storage_used 字段添加失败:', error);
|
||||
}
|
||||
|
||||
console.log('数据库初始化完成');
|
||||
}
|
||||
|
||||
@@ -296,53 +490,273 @@ const UserDB = {
|
||||
return bcrypt.compareSync(plainPassword, hashedPassword);
|
||||
},
|
||||
|
||||
/**
|
||||
* 字段类型验证函数
|
||||
* 确保所有字段值类型符合数据库要求
|
||||
* @param {string} fieldName - 字段名
|
||||
* @param {*} value - 字段值
|
||||
* @returns {boolean} 是否有效
|
||||
* @private
|
||||
*/
|
||||
_validateFieldValue(fieldName, value) {
|
||||
// 字段类型白名单(根据数据库表结构定义)
|
||||
const FIELD_TYPES = {
|
||||
// 文本类型字段
|
||||
'username': 'string',
|
||||
'email': 'string',
|
||||
'password': 'string',
|
||||
'oss_provider': 'string',
|
||||
'oss_region': 'string',
|
||||
'oss_access_key_id': 'string',
|
||||
'oss_access_key_secret': 'string',
|
||||
'oss_bucket': 'string',
|
||||
'oss_endpoint': 'string',
|
||||
'upload_api_key': 'string',
|
||||
'verification_token': 'string',
|
||||
'verification_expires_at': 'string',
|
||||
'storage_permission': 'string',
|
||||
'current_storage_type': 'string',
|
||||
'theme_preference': 'string',
|
||||
|
||||
// 数值类型字段
|
||||
'is_admin': 'number',
|
||||
'is_active': 'number',
|
||||
'is_banned': 'is_banned',
|
||||
'has_oss_config': 'number',
|
||||
'is_verified': 'number',
|
||||
'local_storage_quota': 'number',
|
||||
'local_storage_used': 'number'
|
||||
};
|
||||
|
||||
const expectedType = FIELD_TYPES[fieldName];
|
||||
|
||||
// 如果字段不在类型定义中,允许通过(向后兼容)
|
||||
if (!expectedType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查类型匹配
|
||||
if (expectedType === 'string') {
|
||||
return typeof value === 'string';
|
||||
} else if (expectedType === 'number') {
|
||||
// 允许数值或可转换为数值的字符串
|
||||
return typeof value === 'number' || (typeof value === 'string' && !isNaN(Number(value)));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 验证字段映射完整性
|
||||
* 确保 FIELD_MAP 中定义的所有字段都在数据库表中存在
|
||||
* @returns {Object} 验证结果 { valid: boolean, missing: string[], extra: string[] }
|
||||
* @private
|
||||
*/
|
||||
_validateFieldMapping() {
|
||||
// 字段映射白名单:防止别名攻击(如 toString、valueOf 等原型方法)
|
||||
const FIELD_MAP = {
|
||||
// 基础字段
|
||||
'username': 'username',
|
||||
'email': 'email',
|
||||
'password': 'password',
|
||||
|
||||
// OSS 配置字段
|
||||
'oss_provider': 'oss_provider',
|
||||
'oss_region': 'oss_region',
|
||||
'oss_access_key_id': 'oss_access_key_id',
|
||||
'oss_access_key_secret': 'oss_access_key_secret',
|
||||
'oss_bucket': 'oss_bucket',
|
||||
'oss_endpoint': 'oss_endpoint',
|
||||
|
||||
// API 密钥和权限字段
|
||||
'upload_api_key': 'upload_api_key',
|
||||
'is_admin': 'is_admin',
|
||||
'is_active': 'is_active',
|
||||
'is_banned': 'is_banned',
|
||||
'has_oss_config': 'has_oss_config',
|
||||
|
||||
// 验证字段
|
||||
'is_verified': 'is_verified',
|
||||
'verification_token': 'verification_token',
|
||||
'verification_expires_at': 'verification_expires_at',
|
||||
|
||||
// 存储配置字段
|
||||
'storage_permission': 'storage_permission',
|
||||
'current_storage_type': 'current_storage_type',
|
||||
'local_storage_quota': 'local_storage_quota',
|
||||
'local_storage_used': 'local_storage_used',
|
||||
|
||||
// 偏好设置
|
||||
'theme_preference': 'theme_preference'
|
||||
};
|
||||
|
||||
try {
|
||||
// 获取数据库表的实际列信息
|
||||
const columns = db.prepare("PRAGMA table_info(users)").all();
|
||||
const dbFields = new Set(columns.map(col => col.name));
|
||||
|
||||
// 检查 FIELD_MAP 中的字段是否都在数据库中存在
|
||||
const mappedFields = new Set(Object.values(FIELD_MAP));
|
||||
const missingFields = [];
|
||||
const extraFields = [];
|
||||
|
||||
for (const field of mappedFields) {
|
||||
if (!dbFields.has(field)) {
|
||||
missingFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查数据库中是否有 FIELD_MAP 未定义的字段(可选)
|
||||
for (const dbField of dbFields) {
|
||||
if (!mappedFields.has(dbField) && !['id', 'created_at', 'updated_at'].includes(dbField)) {
|
||||
extraFields.push(dbField);
|
||||
}
|
||||
}
|
||||
|
||||
const isValid = missingFields.length === 0;
|
||||
|
||||
if (!isValid) {
|
||||
console.error(`[数据库错误] 字段映射验证失败,缺失字段: ${missingFields.join(', ')}`);
|
||||
}
|
||||
|
||||
if (extraFields.length > 0) {
|
||||
console.warn(`[数据库警告] 数据库存在 FIELD_MAP 未定义的字段: ${extraFields.join(', ')}`);
|
||||
}
|
||||
|
||||
return { valid: isValid, missing: missingFields, extra: extraFields };
|
||||
} catch (error) {
|
||||
console.error(`[数据库错误] 字段映射验证失败: ${error.message}`);
|
||||
return { valid: false, missing: [], extra: [], error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户
|
||||
// 安全修复:使用白名单验证字段名,防止 SQL 注入
|
||||
// 安全修复:使用字段映射白名单,防止 SQL 注入和原型污染攻击
|
||||
update(id, updates) {
|
||||
// 允许更新的字段白名单
|
||||
const ALLOWED_FIELDS = [
|
||||
'username', 'email', 'password',
|
||||
'oss_provider', 'oss_region', 'oss_access_key_id', 'oss_access_key_secret', 'oss_bucket', 'oss_endpoint',
|
||||
'upload_api_key', 'is_admin', 'is_active', 'is_banned', 'has_oss_config',
|
||||
'is_verified', 'verification_token', 'verification_expires_at',
|
||||
'storage_permission', 'current_storage_type', 'local_storage_quota', 'local_storage_used',
|
||||
'theme_preference'
|
||||
];
|
||||
// 字段映射白名单:防止别名攻击(如 toString、valueOf 等原型方法)
|
||||
const FIELD_MAP = {
|
||||
// 基础字段
|
||||
'username': 'username',
|
||||
'email': 'email',
|
||||
'password': 'password',
|
||||
|
||||
// OSS 配置字段
|
||||
'oss_provider': 'oss_provider',
|
||||
'oss_region': 'oss_region',
|
||||
'oss_access_key_id': 'oss_access_key_id',
|
||||
'oss_access_key_secret': 'oss_access_key_secret',
|
||||
'oss_bucket': 'oss_bucket',
|
||||
'oss_endpoint': 'oss_endpoint',
|
||||
|
||||
// API 密钥和权限字段
|
||||
'upload_api_key': 'upload_api_key',
|
||||
'is_admin': 'is_admin',
|
||||
'is_active': 'is_active',
|
||||
'is_banned': 'is_banned',
|
||||
'has_oss_config': 'has_oss_config',
|
||||
|
||||
// 验证字段
|
||||
'is_verified': 'is_verified',
|
||||
'verification_token': 'verification_token',
|
||||
'verification_expires_at': 'verification_expires_at',
|
||||
|
||||
// 存储配置字段
|
||||
'storage_permission': 'storage_permission',
|
||||
'current_storage_type': 'current_storage_type',
|
||||
'local_storage_quota': 'local_storage_quota',
|
||||
'local_storage_used': 'local_storage_used',
|
||||
|
||||
// 偏好设置
|
||||
'theme_preference': 'theme_preference'
|
||||
};
|
||||
|
||||
const fields = [];
|
||||
const values = [];
|
||||
const rejectedFields = []; // 记录被拒绝的字段(类型不符)
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
// 安全检查 1:确保是对象自身的属性(防止原型污染)
|
||||
// 使用 Object.prototype.hasOwnProperty.call() 避免原型链污染
|
||||
if (!Object.prototype.hasOwnProperty.call(updates, key)) {
|
||||
console.warn(`[安全警告] 跳过非自身属性: ${key} (类型: ${typeof key})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 安全检查 2:只允许白名单中的字段
|
||||
if (!ALLOWED_FIELDS.includes(key)) {
|
||||
// 安全检查 2:字段名必须是字符串类型
|
||||
if (typeof key !== 'string' || key.trim() === '') {
|
||||
console.warn(`[安全警告] 跳过无效字段名: ${key} (类型: ${typeof key})`);
|
||||
rejectedFields.push({ field: key, reason: '字段名不是有效字符串' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 安全检查 3:验证字段映射(防止别名攻击)
|
||||
const mappedField = FIELD_MAP[key];
|
||||
if (!mappedField) {
|
||||
console.warn(`[安全警告] 尝试更新非法字段: ${key}`);
|
||||
rejectedFields.push({ field: key, reason: '字段不在白名单中' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 安全检查 4:确保字段名不包含特殊字符或 SQL 关键字
|
||||
// 只允许字母、数字和下划线
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(mappedField)) {
|
||||
console.warn(`[安全警告] 字段名包含非法字符: ${mappedField}`);
|
||||
rejectedFields.push({ field: key, reason: '字段名包含非法字符' });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 安全检查 5:验证字段值类型(第二轮修复)
|
||||
if (!this._validateFieldValue(key, value)) {
|
||||
const expectedType = {
|
||||
'username': 'string', 'email': 'string', 'password': 'string',
|
||||
'oss_provider': 'string', 'oss_region': 'string',
|
||||
'oss_access_key_id': 'string', 'oss_access_key_secret': 'string',
|
||||
'oss_bucket': 'string', 'oss_endpoint': 'string',
|
||||
'upload_api_key': 'string', 'verification_token': 'string',
|
||||
'verification_expires_at': 'string', 'storage_permission': 'string',
|
||||
'current_storage_type': 'string', 'theme_preference': 'string',
|
||||
'is_admin': 'number', 'is_active': 'number', 'is_banned': 'number',
|
||||
'has_oss_config': 'number', 'is_verified': 'number',
|
||||
'local_storage_quota': 'number', 'local_storage_used': 'number'
|
||||
}[key];
|
||||
|
||||
console.warn(`[类型检查] 字段 ${key} 值类型不符: 期望 ${expectedType}, 实际 ${typeof value}, 值: ${JSON.stringify(value)}`);
|
||||
rejectedFields.push({ field: key, reason: `值类型不符 (期望: ${expectedType}, 实际: ${typeof value})` });
|
||||
continue;
|
||||
}
|
||||
|
||||
// 特殊处理密码字段(需要哈希)
|
||||
if (key === 'password') {
|
||||
fields.push(`${key} = ?`);
|
||||
fields.push(`${mappedField} = ?`);
|
||||
values.push(bcrypt.hashSync(value, 10));
|
||||
} else {
|
||||
fields.push(`${key} = ?`);
|
||||
fields.push(`${mappedField} = ?`);
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有有效字段,返回空结果
|
||||
if (fields.length === 0) {
|
||||
return { changes: 0 };
|
||||
// 记录被拒绝的字段(用于调试)
|
||||
if (rejectedFields.length > 0) {
|
||||
console.log(`[类型检查] 用户 ${id} 更新请求拒绝了 ${rejectedFields.length} 个字段:`, rejectedFields);
|
||||
}
|
||||
|
||||
// 如果没有有效字段,返回空结果
|
||||
if (fields.length === 0) {
|
||||
console.warn(`[安全警告] 没有有效字段可更新,用户ID: ${id}`);
|
||||
return { changes: 0, rejectedFields };
|
||||
}
|
||||
|
||||
// 添加 updated_at 时间戳
|
||||
fields.push('updated_at = CURRENT_TIMESTAMP');
|
||||
values.push(id);
|
||||
|
||||
// 使用参数化查询执行更新(防止 SQL 注入)
|
||||
const stmt = db.prepare(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`);
|
||||
return stmt.run(...values);
|
||||
const result = stmt.run(...values);
|
||||
|
||||
// 附加被拒绝字段信息到返回结果
|
||||
result.rejectedFields = rejectedFields;
|
||||
return result;
|
||||
},
|
||||
|
||||
// 获取所有用户
|
||||
@@ -468,9 +882,20 @@ const ShareDB = {
|
||||
|
||||
// 根据分享码查找
|
||||
// 增强: 检查分享者是否被封禁(被封禁用户的分享不可访问)
|
||||
// ===== 性能优化(P0 优先级修复):只查询必要字段,避免 N+1 查询 =====
|
||||
// 移除了敏感字段:oss_access_key_id, oss_access_key_secret(不需要传递给分享访问者)
|
||||
findByCode(shareCode) {
|
||||
const result = db.prepare(`
|
||||
SELECT s.*, u.username, u.oss_provider, u.oss_region, u.oss_access_key_id, u.oss_access_key_secret, u.oss_bucket, u.oss_endpoint, u.theme_preference, u.is_banned
|
||||
SELECT
|
||||
s.id, s.user_id, s.share_code, s.share_path, s.share_type,
|
||||
s.view_count, s.download_count, s.created_at, s.expires_at,
|
||||
u.username,
|
||||
-- OSS 配置(访问分享文件所需)
|
||||
u.oss_provider, u.oss_region, u.oss_bucket, u.oss_endpoint,
|
||||
-- 用户偏好(主题)
|
||||
u.theme_preference,
|
||||
-- 安全检查
|
||||
u.is_banned
|
||||
FROM shares s
|
||||
JOIN users u ON s.user_id = u.id
|
||||
WHERE s.share_code = ?
|
||||
@@ -559,6 +984,85 @@ const SettingsDB = {
|
||||
// 获取所有设置
|
||||
getAll() {
|
||||
return db.prepare('SELECT key, value FROM system_settings').all();
|
||||
},
|
||||
|
||||
// ===== 统一 OSS 配置管理(管理员配置,所有用户共享) =====
|
||||
|
||||
/**
|
||||
* 获取统一的 OSS 配置
|
||||
* @returns {Object|null} OSS 配置对象,如果未配置则返回 null
|
||||
*/
|
||||
getUnifiedOssConfig() {
|
||||
const config = {
|
||||
provider: this.get('oss_provider'),
|
||||
region: this.get('oss_region'),
|
||||
access_key_id: this.get('oss_access_key_id'),
|
||||
access_key_secret: this.get('oss_access_key_secret'),
|
||||
bucket: this.get('oss_bucket'),
|
||||
endpoint: this.get('oss_endpoint')
|
||||
};
|
||||
|
||||
// 检查是否所有必需字段都已配置
|
||||
if (!config.provider || !config.access_key_id || !config.access_key_secret || !config.bucket) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 安全修复:解密 OSS Access Key Secret
|
||||
try {
|
||||
if (config.access_key_secret) {
|
||||
config.access_key_secret = decryptSecret(config.access_key_secret);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[安全] 解密统一 OSS 配置失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置统一的 OSS 配置
|
||||
* @param {Object} ossConfig - OSS 配置对象
|
||||
* @param {string} ossConfig.provider - 服务商(aliyun/tencent/aws)
|
||||
* @param {string} ossConfig.region - 区域
|
||||
* @param {string} ossConfig.access_key_id - Access Key ID
|
||||
* @param {string} ossConfig.access_key_secret - Access Key Secret
|
||||
* @param {string} ossConfig.bucket - 存储桶名称
|
||||
* @param {string} [ossConfig.endpoint] - 自定义 Endpoint(可选)
|
||||
*/
|
||||
setUnifiedOssConfig(ossConfig) {
|
||||
this.set('oss_provider', ossConfig.provider);
|
||||
this.set('oss_region', ossConfig.region);
|
||||
this.set('oss_access_key_id', ossConfig.access_key_id);
|
||||
|
||||
// 安全修复:加密存储 OSS Access Key Secret
|
||||
try {
|
||||
const encryptedSecret = encryptSecret(ossConfig.access_key_secret);
|
||||
this.set('oss_access_key_secret', encryptedSecret);
|
||||
} catch (error) {
|
||||
console.error('[安全] 加密统一 OSS 配置失败:', error.message);
|
||||
throw new Error('保存 OSS 配置失败:加密错误');
|
||||
}
|
||||
|
||||
this.set('oss_bucket', ossConfig.bucket);
|
||||
this.set('oss_endpoint', ossConfig.endpoint || '');
|
||||
console.log('[系统设置] 统一 OSS 配置已更新(已加密)');
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除统一的 OSS 配置
|
||||
*/
|
||||
clearUnifiedOssConfig() {
|
||||
db.prepare('DELETE FROM system_settings WHERE key LIKE "oss_%"').run();
|
||||
console.log('[系统设置] 统一 OSS 配置已清除');
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查是否已配置统一的 OSS
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasUnifiedOssConfig() {
|
||||
return this.getUnifiedOssConfig() !== null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -937,5 +1441,6 @@ module.exports = {
|
||||
VerificationDB,
|
||||
PasswordResetTokenDB,
|
||||
SystemLogDB,
|
||||
TransactionDB
|
||||
TransactionDB,
|
||||
WalManager
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user