Files
vue-driven-cloud-storage/backend/database.js
yuyx efaa2308eb feat: 全面优化代码质量至 8.55/10 分
## 安全增强
- 添加 CSRF 防护机制(Double Submit Cookie 模式)
- 增强密码强度验证(8字符+两种字符类型)
- 添加 Session 密钥安全检查
- 修复 .htaccess 文件上传漏洞
- 统一使用 getSafeErrorMessage() 保护敏感错误信息
- 增强数据库原型污染防护
- 添加被封禁用户分享访问检查

## 功能修复
- 修复模态框点击外部关闭功能
- 修复 share.html 未定义方法调用
- 修复 verify.html 和 reset-password.html API 路径
- 修复数据库 SFTP->OSS 迁移逻辑
- 修复 OSS 未配置时的错误提示
- 添加文件夹名称长度限制
- 添加文件列表 API 路径验证

## UI/UX 改进
- 添加 6 个按钮加载状态(登录/注册/修改密码等)
- 将 15+ 处 alert() 替换为 Toast 通知
- 添加防重复提交机制(创建文件夹/分享)
- 优化 loadUserProfile 防抖调用

## 代码质量
- 消除 formatFileSize 重复定义
- 集中模块导入到文件顶部
- 添加 JSDoc 注释
- 创建路由拆分示例 (routes/)

## 测试套件
- 添加 boundary-tests.js (60 用例)
- 添加 network-concurrent-tests.js (33 用例)
- 添加 state-consistency-tests.js (38 用例)
- 添加 test_share.js 和 test_admin.js

## 文档和配置
- 新增 INSTALL_GUIDE.md 手动部署指南
- 新增 VERSION.txt 版本历史
- 完善 .env.example 配置说明
- 新增 docker-compose.yml
- 完善 nginx.conf.example

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 10:45:51 +08:00

942 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 加载环境变量(确保在 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');
// 数据库路径配置
// 优先使用环境变量 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);
// 启用外键约束
db.pragma('foreign_keys = ON');
// 初始化数据库表
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);
`);
// 数据库迁移添加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);
`);
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);
},
// 更新用户
// 安全修复:使用白名单验证字段名,防止 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'
];
const fields = [];
const values = [];
for (const [key, value] of Object.entries(updates)) {
// 安全检查 1确保是对象自身的属性防止原型污染
if (!Object.prototype.hasOwnProperty.call(updates, key)) {
continue;
}
// 安全检查 2只允许白名单中的字段
if (!ALLOWED_FIELDS.includes(key)) {
console.warn(`[安全警告] 尝试更新非法字段: ${key}`);
continue;
}
if (key === 'password') {
fields.push(`${key} = ?`);
values.push(bcrypt.hashSync(value, 10));
} else {
fields.push(`${key} = ?`);
values.push(value);
}
}
// 如果没有有效字段,返回空结果
if (fields.length === 0) {
return { changes: 0 };
}
fields.push('updated_at = CURRENT_TIMESTAMP');
values.push(id);
const stmt = db.prepare(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`);
return stmt.run(...values);
},
// 获取所有用户
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,
};
},
// 根据分享码查找
// 增强: 检查分享者是否被封禁(被封禁用户的分享不可访问)
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
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();
}
};
// 邮箱验证管理(增强安全:哈希存储)
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);
// 不抛出错误,允许服务继续启动
}
}
// 系统日志操作
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 迁移
module.exports = {
db,
UserDB,
ShareDB,
SettingsDB,
VerificationDB,
PasswordResetTokenDB,
SystemLogDB,
TransactionDB
};