功能说明: - 管理员可在系统设置中配置全局默认主题 - 普通用户可在设置页面选择:跟随全局/暗色/亮色 - 分享页面自动继承分享者的主题偏好 - 主题设置实时保存,刷新后保持 技术实现: - 后端:数据库添加theme_preference字段,新增主题API - 前端:CSS变量实现主题切换,localStorage缓存避免闪烁 - 分享页:加载时获取分享者主题设置 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
832 lines
25 KiB
JavaScript
832 lines
25 KiB
JavaScript
// 加载环境变量(确保在 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,
|
||
|
||
-- FTP配置(可选)
|
||
ftp_host TEXT,
|
||
ftp_port INTEGER DEFAULT 22,
|
||
ftp_user TEXT,
|
||
ftp_password TEXT,
|
||
http_download_base_url TEXT,
|
||
|
||
-- 上传工具API密钥
|
||
upload_api_key TEXT,
|
||
|
||
-- 用户状态
|
||
is_admin INTEGER DEFAULT 0,
|
||
is_active INTEGER DEFAULT 1,
|
||
is_banned INTEGER DEFAULT 0,
|
||
has_ftp_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_shares_code ON shares(share_code);
|
||
CREATE INDEX IF NOT EXISTS idx_shares_user ON shares(user_id);
|
||
`);
|
||
|
||
// 数据库迁移:添加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_ftp_config, is_verified
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||
`).run(
|
||
adminUsername,
|
||
`${adminUsername}@example.com`,
|
||
hashedPassword,
|
||
1,
|
||
1,
|
||
0, // 管理员不需要FTP配置
|
||
1 // 管理员默认已验证
|
||
);
|
||
|
||
console.log('默认管理员账号已创建');
|
||
console.log('用户名:', adminUsername);
|
||
console.log('密码: ********');
|
||
console.log('⚠️ 请登录后立即修改密码!');
|
||
}
|
||
}
|
||
|
||
// 用户相关操作
|
||
const UserDB = {
|
||
// 创建用户
|
||
create(userData) {
|
||
const hashedPassword = bcrypt.hashSync(userData.password, 10);
|
||
|
||
const hasFtpConfig = userData.ftp_host && userData.ftp_user && userData.ftp_password ? 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,
|
||
ftp_host, ftp_port, ftp_user, ftp_password, http_download_base_url,
|
||
has_ftp_config,
|
||
is_verified, verification_token, verification_expires_at
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`);
|
||
|
||
const result = stmt.run(
|
||
userData.username,
|
||
userData.email,
|
||
hashedPassword,
|
||
userData.ftp_host || null,
|
||
userData.ftp_port || 22,
|
||
userData.ftp_user || null,
|
||
userData.ftp_password || null,
|
||
userData.http_download_base_url || null,
|
||
hasFtpConfig,
|
||
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);
|
||
},
|
||
|
||
// 更新用户
|
||
update(id, updates) {
|
||
const fields = [];
|
||
const values = [];
|
||
|
||
for (const [key, value] of Object.entries(updates)) {
|
||
if (key === 'password') {
|
||
fields.push(`${key} = ?`);
|
||
values.push(bcrypt.hashSync(value, 10));
|
||
} else {
|
||
fields.push(`${key} = ?`);
|
||
values.push(value);
|
||
}
|
||
}
|
||
|
||
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) {
|
||
// 调试日志: findByCode 调用
|
||
const currentTime = db.prepare("SELECT datetime('now', 'localtime') as now").get();
|
||
console.log('[ShareDB.findByCode]', {
|
||
shareCode,
|
||
currentTime: currentTime.now,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
const result = db.prepare(`
|
||
SELECT s.*, u.username, u.ftp_host, u.ftp_port, u.ftp_user, u.ftp_password, u.http_download_base_url, u.theme_preference
|
||
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'))
|
||
`).get(shareCode);
|
||
|
||
// 调试日志: SQL查询结果
|
||
console.log('[ShareDB.findByCode] SQL结果:', {
|
||
found: !!result,
|
||
shareCode: result?.share_code || null,
|
||
expires_at: result?.expires_at || null,
|
||
share_type: result?.share_type || null
|
||
});
|
||
|
||
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;
|
||
`);
|
||
|
||
// 更新现有用户为SFTP模式(保持兼容)
|
||
const updateStmt = db.prepare("UPDATE users SET current_storage_type = 'sftp' WHERE has_ftp_config = 1");
|
||
updateStmt.run();
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
// 系统日志操作
|
||
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;
|
||
}
|
||
};
|
||
|
||
// 初始化数据库
|
||
initDatabase();
|
||
createDefaultAdmin();
|
||
initDefaultSettings();
|
||
migrateToV2(); // 执行数据库迁移
|
||
migrateThemePreference(); // 主题偏好迁移
|
||
|
||
module.exports = {
|
||
db,
|
||
UserDB,
|
||
ShareDB,
|
||
SettingsDB,
|
||
VerificationDB,
|
||
PasswordResetTokenDB,
|
||
SystemLogDB
|
||
};
|