// 加载环境变量(确保在 server.js 之前也能读取) require('dotenv').config(); const Database = require('better-sqlite3'); const bcrypt = require('bcryptjs'); 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'); const dbPath = process.env.DATABASE_PATH ? path.resolve(__dirname, process.env.DATABASE_PATH) : DEFAULT_DB_PATH; // 确保数据库目录存在 const dbDir = path.dirname(dbPath); if (!fs.existsSync(dbDir)) { fs.mkdirSync(dbDir, { recursive: true }); console.log(`[数据库] 创建目录: ${dbDir}`); } 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() { // 用户表 db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, password TEXT NOT NULL, -- OSS配置(可选) oss_provider TEXT, oss_region TEXT, oss_access_key_id TEXT, oss_access_key_secret TEXT, oss_bucket TEXT, oss_endpoint TEXT, -- 上传工具API密钥 upload_api_key TEXT, -- 用户状态 is_admin INTEGER DEFAULT 0, is_active INTEGER DEFAULT 1, is_banned INTEGER DEFAULT 0, has_oss_config INTEGER DEFAULT 0, -- 时间戳 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // 分享链接表 db.exec(` CREATE TABLE IF NOT EXISTS shares ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, share_code TEXT UNIQUE NOT NULL, share_path TEXT NOT NULL, share_type TEXT DEFAULT 'file', share_password TEXT, -- 分享统计 view_count INTEGER DEFAULT 0, download_count INTEGER DEFAULT 0, -- 时间戳 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) `); // 系统设置表 db.exec(` CREATE TABLE IF NOT EXISTS system_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // 创建索引 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(); const hasUploadApiKey = columns.some(col => col.name === 'upload_api_key'); if (!hasUploadApiKey) { db.exec(`ALTER TABLE users ADD COLUMN upload_api_key TEXT`); console.log('数据库迁移:添加upload_api_key字段完成'); } } catch (error) { console.error('数据库迁移失败:', error); } // 数据库迁移:添加share_type字段(如果不存在) try { const shareColumns = db.prepare("PRAGMA table_info(shares)").all(); const hasShareType = shareColumns.some(col => col.name === 'share_type'); if (!hasShareType) { db.exec(`ALTER TABLE shares ADD COLUMN share_type TEXT DEFAULT 'file'`); console.log('数据库迁移:添加share_type字段完成'); } } catch (error) { console.error('数据库迁移(share_type)失败:', error); } // 数据库迁移:邮箱验证字段 try { const columns = db.prepare("PRAGMA table_info(users)").all(); const hasVerified = columns.some(col => col.name === 'is_verified'); const hasVerifyToken = columns.some(col => col.name === 'verification_token'); const hasVerifyExpires = columns.some(col => col.name === 'verification_expires_at'); if (!hasVerified) { db.exec(`ALTER TABLE users ADD COLUMN is_verified INTEGER DEFAULT 0`); } if (!hasVerifyToken) { db.exec(`ALTER TABLE users ADD COLUMN verification_token TEXT`); } if (!hasVerifyExpires) { db.exec(`ALTER TABLE users ADD COLUMN verification_expires_at DATETIME`); } // 注意:不再自动将未验证用户设为已验证 // 仅修复 is_verified 为 NULL 的旧数据(添加字段前创建的用户) // 这些用户没有 verification_token,说明是在邮箱验证功能上线前注册的 db.exec(`UPDATE users SET is_verified = 1 WHERE is_verified IS NULL AND verification_token IS NULL`); } catch (error) { console.error('数据库迁移(邮箱验证)失败:', error); } // 数据库迁移:密码重置Token表 try { db.exec(` CREATE TABLE IF NOT EXISTS password_reset_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, token TEXT UNIQUE NOT NULL, expires_at DATETIME NOT NULL, used INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) `); db.exec(`CREATE INDEX IF NOT EXISTS idx_reset_tokens_token ON password_reset_tokens(token);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_reset_tokens_user ON password_reset_tokens(user_id);`); } catch (error) { console.error('数据库迁移(密码重置Token)失败:', error); } // 系统日志表 db.exec(` CREATE TABLE IF NOT EXISTS system_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, level TEXT NOT NULL DEFAULT 'info', category TEXT NOT NULL, action TEXT NOT NULL, message TEXT NOT NULL, user_id INTEGER, username TEXT, ip_address TEXT, user_agent TEXT, details TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL ) `); // 日志表索引 db.exec(` CREATE INDEX IF NOT EXISTS idx_logs_created_at ON system_logs(created_at); 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('数据库初始化完成'); } // 创建默认管理员账号 function createDefaultAdmin() { const adminExists = db.prepare('SELECT id FROM users WHERE is_admin = 1').get(); if (!adminExists) { // 从环境变量读取管理员账号密码,如果没有则使用默认值 const adminUsername = process.env.ADMIN_USERNAME || 'admin'; const adminPassword = process.env.ADMIN_PASSWORD || 'admin123'; const hashedPassword = bcrypt.hashSync(adminPassword, 10); db.prepare(` INSERT INTO users ( username, email, password, is_admin, is_active, has_oss_config, is_verified ) VALUES (?, ?, ?, ?, ?, ?, ?) `).run( adminUsername, `${adminUsername}@example.com`, hashedPassword, 1, 1, 0, // 管理员不需要OSS配置 1 // 管理员默认已验证 ); console.log('默认管理员账号已创建'); console.log('用户名:', adminUsername); console.log('密码: ********'); console.log('⚠️ 请登录后立即修改密码!'); } } // 用户相关操作 const UserDB = { // 创建用户 create(userData) { const hashedPassword = bcrypt.hashSync(userData.password, 10); const hasOssConfig = userData.oss_provider && userData.oss_access_key_id && userData.oss_access_key_secret && userData.oss_bucket ? 1 : 0; // 对验证令牌进行哈希存储(与 VerificationDB.setVerification 保持一致) const hashedVerificationToken = userData.verification_token ? crypto.createHash('sha256').update(userData.verification_token).digest('hex') : null; const stmt = db.prepare(` INSERT INTO users ( username, email, password, oss_provider, oss_region, oss_access_key_id, oss_access_key_secret, oss_bucket, oss_endpoint, has_oss_config, is_verified, verification_token, verification_expires_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run( userData.username, userData.email, hashedPassword, userData.oss_provider || null, userData.oss_region || null, userData.oss_access_key_id || null, userData.oss_access_key_secret || null, userData.oss_bucket || null, userData.oss_endpoint || null, hasOssConfig, userData.is_verified !== undefined ? userData.is_verified : 0, hashedVerificationToken, userData.verification_expires_at || null ); return result.lastInsertRowid; }, // 根据用户名查找 findByUsername(username) { return db.prepare('SELECT * FROM users WHERE username = ?').get(username); }, // 根据邮箱查找 findByEmail(email) { return db.prepare('SELECT * FROM users WHERE email = ?').get(email); }, // 根据ID查找 findById(id) { return db.prepare('SELECT * FROM users WHERE id = ?').get(id); }, // 验证密码 verifyPassword(plainPassword, hashedPassword) { 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 注入和原型污染攻击 update(id, updates) { // 字段映射白名单:防止别名攻击(如 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 (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(`${mappedField} = ?`); values.push(bcrypt.hashSync(value, 10)); } else { fields.push(`${mappedField} = ?`); values.push(value); } } // 记录被拒绝的字段(用于调试) 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 = ?`); const result = stmt.run(...values); // 附加被拒绝字段信息到返回结果 result.rejectedFields = rejectedFields; return result; }, // 获取所有用户 getAll(filters = {}) { let query = 'SELECT * FROM users WHERE 1=1'; const params = []; if (filters.is_admin !== undefined) { query += ' AND is_admin = ?'; params.push(filters.is_admin); } if (filters.is_banned !== undefined) { query += ' AND is_banned = ?'; params.push(filters.is_banned); } query += ' ORDER BY created_at DESC'; return db.prepare(query).all(...params); }, // 删除用户 delete(id) { return db.prepare('DELETE FROM users WHERE id = ?').run(id); }, // 封禁/解封用户 setBanStatus(id, isBanned) { return db.prepare('UPDATE users SET is_banned = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?') .run(isBanned ? 1 : 0, id); } }; // 分享链接相关操作 const ShareDB = { // 生成随机分享码 generateShareCode(length = 8) { const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const bytes = crypto.randomBytes(length); let code = ''; for (let i = 0; i < length; i++) { code += chars[bytes[i] % chars.length]; } return code; }, // 创建分享链接 // 创建分享链接 create(userId, options = {}) { const { share_type = 'file', file_path = '', file_name = '', password = null, expiry_days = null } = options; let shareCode; let attempts = 0; // 尝试生成唯一的分享码 do { shareCode = this.generateShareCode(); attempts++; if (attempts > 10) { shareCode = this.generateShareCode(10); // 增加长度 } } while (this.findByCode(shareCode) && attempts < 20); // 计算过期时间 let expiresAt = null; if (expiry_days) { const expireDate = new Date(); expireDate.setDate(expireDate.getDate() + parseInt(expiry_days)); // 使用本地时区时间,而不是UTC时间 // 这样前端解析时会正确显示为本地时间 const year = expireDate.getFullYear(); const month = String(expireDate.getMonth() + 1).padStart(2, '0'); const day = String(expireDate.getDate()).padStart(2, '0'); const hours = String(expireDate.getHours()).padStart(2, '0'); const minutes = String(expireDate.getMinutes()).padStart(2, '0'); const seconds = String(expireDate.getSeconds()).padStart(2, '0'); expiresAt = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } const stmt = db.prepare(` INSERT INTO shares (user_id, share_code, share_path, share_type, share_password, expires_at) VALUES (?, ?, ?, ?, ?, ?) `); const hashedPassword = password ? bcrypt.hashSync(password, 10) : null; // 修复:正确处理不同类型的分享路径 let sharePath; if (share_type === 'file') { // 单文件分享:使用完整文件路径 sharePath = file_path; } else if (share_type === 'directory') { // 文件夹分享:使用文件夹路径 sharePath = file_path; } else { // all类型:分享根目录 sharePath = '/'; } const result = stmt.run( userId, shareCode, sharePath, share_type, hashedPassword, expiresAt ); return { id: result.lastInsertRowid, share_code: shareCode, share_type: share_type, expires_at: expiresAt, }; }, // 根据分享码查找 // 增强: 检查分享者是否被封禁(被封禁用户的分享不可访问) // ===== 性能优化(P0 优先级修复):只查询必要字段,避免 N+1 查询 ===== // 移除了敏感字段:oss_access_key_id, oss_access_key_secret(不需要传递给分享访问者) findByCode(shareCode) { const result = db.prepare(` 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 = ? AND (s.expires_at IS NULL OR s.expires_at > datetime('now', 'localtime')) AND u.is_banned = 0 `).get(shareCode); return result; }, // 根据ID查找 findById(id) { return db.prepare('SELECT * FROM shares WHERE id = ?').get(id); }, // 验证分享密码 verifyPassword(plainPassword, hashedPassword) { return bcrypt.compareSync(plainPassword, hashedPassword); }, // 获取用户的所有分享 getUserShares(userId) { return db.prepare(` SELECT * FROM shares WHERE user_id = ? ORDER BY created_at DESC `).all(userId); }, // 增加查看次数 incrementViewCount(shareCode) { return db.prepare(` UPDATE shares SET view_count = view_count + 1 WHERE share_code = ? `).run(shareCode); }, // 增加下载次数 incrementDownloadCount(shareCode) { return db.prepare(` UPDATE shares SET download_count = download_count + 1 WHERE share_code = ? `).run(shareCode); }, // 删除分享 delete(id, userId = null) { if (userId) { return db.prepare('DELETE FROM shares WHERE id = ? AND user_id = ?').run(id, userId); } return db.prepare('DELETE FROM shares WHERE id = ?').run(id); }, // 获取所有分享(管理员) getAll() { return db.prepare(` SELECT s.*, u.username FROM shares s JOIN users u ON s.user_id = u.id ORDER BY s.created_at DESC `).all(); } }; // 系统设置管理 const SettingsDB = { // 获取设置 get(key) { const row = db.prepare('SELECT value FROM system_settings WHERE key = ?').get(key); return row ? row.value : null; }, // 设置值 set(key, value) { db.prepare(` INSERT INTO system_settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP `).run(key, value); }, // 获取所有设置 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; } }; // 邮箱验证管理(增强安全:哈希存储) const VerificationDB = { setVerification(userId, token, expiresAtMs) { // 对令牌进行哈希后存储 const hashedToken = crypto.createHash('sha256').update(token).digest('hex'); db.prepare(` UPDATE users SET verification_token = ?, verification_expires_at = ?, is_verified = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run(hashedToken, expiresAtMs, userId); }, consumeVerificationToken(token) { // 对用户提供的令牌进行哈希 const hashedToken = crypto.createHash('sha256').update(token).digest('hex'); const row = db.prepare(` SELECT * FROM users WHERE verification_token = ? AND ( verification_expires_at IS NULL OR verification_expires_at = '' OR verification_expires_at > strftime('%s','now')*1000 -- 数值时间戳(ms) OR verification_expires_at > CURRENT_TIMESTAMP -- 兼容旧的字符串时间 ) AND is_verified = 0 `).get(hashedToken); if (!row) return null; db.prepare(` UPDATE users SET is_verified = 1, verification_token = NULL, verification_expires_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run(row.id); return row; } }; // 密码重置 Token 管理(增强安全:哈希存储) const PasswordResetTokenDB = { // 创建令牌时存储哈希值 create(userId, token, expiresAtMs) { // 对令牌进行哈希后存储(防止数据库泄露时令牌被直接使用) const hashedToken = crypto.createHash('sha256').update(token).digest('hex'); db.prepare(` INSERT INTO password_reset_tokens (user_id, token, expires_at, used) VALUES (?, ?, ?, 0) `).run(userId, hashedToken, expiresAtMs); }, // 验证令牌时先哈希再比较 use(token) { // 对用户提供的令牌进行哈希 const hashedToken = crypto.createHash('sha256').update(token).digest('hex'); const row = db.prepare(` SELECT * FROM password_reset_tokens WHERE token = ? AND used = 0 AND ( expires_at > strftime('%s','now')*1000 -- 数值时间戳 OR expires_at > CURRENT_TIMESTAMP -- 兼容旧的字符串时间 ) `).get(hashedToken); if (!row) return null; // 立即标记为已使用(防止重复使用) db.prepare(`UPDATE password_reset_tokens SET used = 1 WHERE id = ?`).run(row.id); return row; } }; // 初始化默认设置 function initDefaultSettings() { // 默认上传限制为10GB if (!SettingsDB.get('max_upload_size')) { SettingsDB.set('max_upload_size', '10737418240'); // 10GB in bytes } // 默认全局主题为暗色 if (!SettingsDB.get('global_theme')) { SettingsDB.set('global_theme', 'dark'); } } // 数据库迁移 - 主题偏好字段 function migrateThemePreference() { try { const columns = db.prepare("PRAGMA table_info(users)").all(); const hasThemePreference = columns.some(col => col.name === 'theme_preference'); if (!hasThemePreference) { console.log('[数据库迁移] 添加主题偏好字段...'); db.exec(`ALTER TABLE users ADD COLUMN theme_preference TEXT DEFAULT NULL`); console.log('[数据库迁移] ✓ 主题偏好字段已添加'); } } catch (error) { console.error('[数据库迁移] 主题偏好迁移失败:', error); } } // 数据库版本迁移 - v2.0 本地存储功能 function migrateToV2() { try { const columns = db.prepare("PRAGMA table_info(users)").all(); const hasStoragePermission = columns.some(col => col.name === 'storage_permission'); if (!hasStoragePermission) { console.log('[数据库迁移] 检测到旧版本数据库,开始升级到 v2.0...'); // 添加本地存储相关字段 db.exec(` ALTER TABLE users ADD COLUMN storage_permission TEXT DEFAULT 'sftp_only'; ALTER TABLE users ADD COLUMN current_storage_type TEXT DEFAULT 'sftp'; ALTER TABLE users ADD COLUMN local_storage_quota INTEGER DEFAULT 1073741824; ALTER TABLE users ADD COLUMN local_storage_used INTEGER DEFAULT 0; `); console.log('[数据库迁移] ✓ 用户表已升级'); // 为分享表添加存储类型字段 const shareColumns = db.prepare("PRAGMA table_info(shares)").all(); const hasShareStorageType = shareColumns.some(col => col.name === 'storage_type'); if (!hasShareStorageType) { db.exec(`ALTER TABLE shares ADD COLUMN storage_type TEXT DEFAULT 'sftp';`); console.log('[数据库迁移] ✓ 分享表已升级'); } console.log('[数据库迁移] ✅ 数据库升级到 v2.0 完成!本地存储功能已启用'); } } catch (error) { console.error('[数据库迁移] 迁移失败:', error); throw error; } } // 数据库版本迁移 - v3.0 SFTP → OSS function migrateToOss() { try { const columns = db.prepare("PRAGMA table_info(users)").all(); const hasOssProvider = columns.some(col => col.name === 'oss_provider'); if (!hasOssProvider) { console.log('[数据库迁移] 检测到 SFTP 版本,开始升级到 v3.0 OSS...'); // 添加 OSS 相关字段 db.exec(` ALTER TABLE users ADD COLUMN oss_provider TEXT DEFAULT NULL; ALTER TABLE users ADD COLUMN oss_region TEXT DEFAULT NULL; ALTER TABLE users ADD COLUMN oss_access_key_id TEXT DEFAULT NULL; ALTER TABLE users ADD COLUMN oss_access_key_secret TEXT DEFAULT NULL; ALTER TABLE users ADD COLUMN oss_bucket TEXT DEFAULT NULL; ALTER TABLE users ADD COLUMN oss_endpoint TEXT DEFAULT NULL; ALTER TABLE users ADD COLUMN has_oss_config INTEGER DEFAULT 0; `); console.log('[数据库迁移] ✓ OSS 字段已添加'); } // 修复:无论 OSS 字段是否刚添加,都要确保更新现有的 sftp 数据 // 检查是否有用户仍使用 sftp 类型 const sftpUsers = db.prepare("SELECT COUNT(*) as count FROM users WHERE storage_permission = 'sftp_only' OR current_storage_type = 'sftp'").get(); if (sftpUsers.count > 0) { console.log(`[数据库迁移] 检测到 ${sftpUsers.count} 个用户仍使用 sftp 类型,正在更新...`); // 更新存储权限枚举值:sftp_only → oss_only db.exec(`UPDATE users SET storage_permission = 'oss_only' WHERE storage_permission = 'sftp_only'`); console.log('[数据库迁移] ✓ 存储权限枚举值已更新'); // 更新存储类型:sftp → oss db.exec(`UPDATE users SET current_storage_type = 'oss' WHERE current_storage_type = 'sftp'`); console.log('[数据库迁移] ✓ 存储类型已更新'); // 更新分享表的存储类型 const shareColumns = db.prepare("PRAGMA table_info(shares)").all(); const hasStorageType = shareColumns.some(col => col.name === 'storage_type'); if (hasStorageType) { db.exec(`UPDATE shares SET storage_type = 'oss' WHERE storage_type = 'sftp'`); console.log('[数据库迁移] ✓ 分享表存储类型已更新'); } console.log('[数据库迁移] ✅ SFTP → OSS 数据更新完成!'); } } catch (error) { console.error('[数据库迁移] OSS 迁移失败:', error); // 不抛出错误,允许服务继续启动 } } // 数据库版本迁移 - 添加 OSS 配额字段 function migrateAddOssQuota() { try { const columns = db.prepare("PRAGMA table_info(users)").all(); const hasOssQuota = columns.some(col => col.name === 'oss_storage_quota'); if (!hasOssQuota) { console.log('[数据库迁移] 添加 OSS 配额字段...'); db.exec(); console.log('[数据库迁移] ✅ OSS 配额字段已添加 (默认 0 表示无限制)'); } } catch (error) { console.error('[数据库迁移] OSS 配额迁移失败:', error); } } // 系统日志操作 const SystemLogDB = { // 日志级别常量 LEVELS: { DEBUG: 'debug', INFO: 'info', WARN: 'warn', ERROR: 'error' }, // 日志分类常量 CATEGORIES: { AUTH: 'auth', // 认证相关(登录、登出、注册) USER: 'user', // 用户管理(创建、修改、删除、封禁) FILE: 'file', // 文件操作(上传、下载、删除、重命名) SHARE: 'share', // 分享操作(创建、删除、访问) SYSTEM: 'system', // 系统操作(设置修改、服务启动) SECURITY: 'security' // 安全事件(登录失败、暴力破解、异常访问) }, // 写入日志 log({ level = 'info', category, action, message, userId = null, username = null, ipAddress = null, userAgent = null, details = null }) { try { const stmt = db.prepare(` INSERT INTO system_logs (level, category, action, message, user_id, username, ip_address, user_agent, details, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime')) `); const detailsStr = details ? (typeof details === 'string' ? details : JSON.stringify(details)) : null; stmt.run(level, category, action, message, userId, username, ipAddress, userAgent, detailsStr); } catch (error) { console.error('写入日志失败:', error); } }, // 查询日志(支持分页和筛选) query({ page = 1, pageSize = 50, level = null, category = null, userId = null, startDate = null, endDate = null, keyword = null }) { let sql = 'SELECT * FROM system_logs WHERE 1=1'; let countSql = 'SELECT COUNT(*) as total FROM system_logs WHERE 1=1'; const params = []; if (level) { sql += ' AND level = ?'; countSql += ' AND level = ?'; params.push(level); } if (category) { sql += ' AND category = ?'; countSql += ' AND category = ?'; params.push(category); } if (userId) { sql += ' AND user_id = ?'; countSql += ' AND user_id = ?'; params.push(userId); } if (startDate) { sql += ' AND created_at >= ?'; countSql += ' AND created_at >= ?'; params.push(startDate); } if (endDate) { sql += ' AND created_at <= ?'; countSql += ' AND created_at <= ?'; params.push(endDate); } if (keyword) { sql += ' AND (message LIKE ? OR username LIKE ? OR action LIKE ?)'; countSql += ' AND (message LIKE ? OR username LIKE ? OR action LIKE ?)'; const kw = `%${keyword}%`; params.push(kw, kw, kw); } // 获取总数 const totalResult = db.prepare(countSql).get(...params); const total = totalResult ? totalResult.total : 0; // 分页查询 sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; const offset = (page - 1) * pageSize; const logs = db.prepare(sql).all(...params, pageSize, offset); return { logs, total, page, pageSize, totalPages: Math.ceil(total / pageSize) }; }, // 获取最近的日志 getRecent(limit = 100) { return db.prepare('SELECT * FROM system_logs ORDER BY created_at DESC LIMIT ?').all(limit); }, // 按分类统计 getStatsByCategory() { return db.prepare(` SELECT category, COUNT(*) as count FROM system_logs GROUP BY category ORDER BY count DESC `).all(); }, // 按日期统计(最近7天) getStatsByDate(days = 7) { return db.prepare(` SELECT DATE(created_at) as date, COUNT(*) as count FROM system_logs WHERE created_at >= datetime('now', 'localtime', '-' || ? || ' days') GROUP BY DATE(created_at) ORDER BY date DESC `).all(days); }, // 清理旧日志(保留指定天数) cleanup(keepDays = 90) { const result = db.prepare(` DELETE FROM system_logs WHERE created_at < datetime('now', 'localtime', '-' || ? || ' days') `).run(keepDays); return result.changes; } }; // 事务工具函数 const TransactionDB = { /** * 在事务中执行操作 * @param {Function} fn - 要执行的函数,接收 db 作为参数 * @returns {*} 函数返回值 * @throws {Error} 如果事务失败则抛出错误 */ run(fn) { const transaction = db.transaction((callback) => { return callback(db); }); return transaction(fn); }, /** * 删除用户及其所有相关数据(使用事务) * @param {number} userId - 用户ID * @returns {object} 删除结果 */ deleteUserWithData(userId) { return this.run(() => { // 1. 删除用户的所有分享 const sharesDeleted = db.prepare('DELETE FROM shares WHERE user_id = ?').run(userId); // 2. 删除密码重置令牌 const tokensDeleted = db.prepare('DELETE FROM password_reset_tokens WHERE user_id = ?').run(userId); // 3. 更新日志中的用户引用(设为 NULL,保留日志记录) db.prepare('UPDATE system_logs SET user_id = NULL WHERE user_id = ?').run(userId); // 4. 删除用户记录 const userDeleted = db.prepare('DELETE FROM users WHERE id = ?').run(userId); return { sharesDeleted: sharesDeleted.changes, tokensDeleted: tokensDeleted.changes, userDeleted: userDeleted.changes }; }); } }; // 初始化数据库 initDatabase(); createDefaultAdmin(); initDefaultSettings(); migrateToV2(); // 执行数据库迁移 migrateThemePreference(); // 主题偏好迁移 migrateToOss(); // SFTP → OSS 迁移 migrateAddOssQuota(); // 添加 OSS 配额字段 module.exports = { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB, SystemLogDB, TransactionDB, WalManager };