## 问题分析
通过调试日志发现,过期分享仍能访问的根本原因是:
- expires_at 存储格式: `2025-11-14T07:31:31.922Z` (ISO 8601)
- datetime('now') 返回格式: `2025-11-14 08:19:52` (SQLite格式)
- SQLite进行字符串比较时: 'T' (ASCII 84) > ' ' (ASCII 32)
- 导致条件 `expires_at > datetime('now')` 对已过期分享仍返回true
## 修复内容
1. database.js: 修改create方法,将expires_at转换为SQLite datetime格式
- 旧格式: 2025-11-14T07:31:31.922Z
- 新格式: 2025-11-14 07:31:31
2. fix_expires_at_format.js: 数据库修复脚本
- 将已存在的ISO格式时间转换为SQLite格式
- 确保历史数据也能正确过滤
## 部署步骤
```bash
cd /var/www/wanwanyun
git pull
cd backend
node fix_expires_at_format.js # 修复历史数据
pm2 restart wanwanyun-backend # 重启服务
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
581 lines
16 KiB
JavaScript
581 lines
16 KiB
JavaScript
const Database = require('better-sqlite3');
|
||
const bcrypt = require('bcryptjs');
|
||
const path = require('path');
|
||
|
||
// 创建或连接数据库
|
||
const db = new Database(path.join(__dirname, 'ftp-manager.db'));
|
||
|
||
// 启用外键约束
|
||
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 TABLE IF NOT EXISTS password_reset_requests (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
new_password TEXT NOT NULL,
|
||
status TEXT DEFAULT 'pending', -- pending, approved, rejected
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
reviewed_at DATETIME,
|
||
reviewed_by INTEGER,
|
||
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||
FOREIGN KEY (reviewed_by) REFERENCES users (id)
|
||
)
|
||
`);
|
||
|
||
// 创建索引
|
||
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);
|
||
CREATE INDEX IF NOT EXISTS idx_reset_requests_user ON password_reset_requests(user_id);
|
||
CREATE INDEX IF NOT EXISTS idx_reset_requests_status ON password_reset_requests(status);
|
||
`);
|
||
|
||
// 数据库迁移:添加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);
|
||
}
|
||
|
||
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
|
||
) VALUES (?, ?, ?, ?, ?, ?)
|
||
`).run(
|
||
adminUsername,
|
||
`${adminUsername}@example.com`,
|
||
hashedPassword,
|
||
1,
|
||
1,
|
||
0 // 管理员不需要FTP配置
|
||
);
|
||
|
||
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;
|
||
|
||
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
|
||
) 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
|
||
);
|
||
|
||
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';
|
||
let code = '';
|
||
for (let i = 0; i < length; i++) {
|
||
code += chars.charAt(Math.floor(Math.random() * 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));
|
||
// 转换为SQLite datetime格式 (YYYY-MM-DD HH:MM:SS),而不是ISO格式
|
||
// 这样才能正确与 datetime('now') 进行比较
|
||
expiresAt = expireDate.toISOString().replace('T', ' ').replace(/\.\d+Z$/, '');
|
||
}
|
||
|
||
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;
|
||
const sharePath = share_type === 'file' ? file_path : '/';
|
||
|
||
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') 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
|
||
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'))
|
||
`).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 PasswordResetDB = {
|
||
// 创建密码重置请求
|
||
create(userId, newPassword) {
|
||
const hashedPassword = bcrypt.hashSync(newPassword, 10);
|
||
|
||
// 删除该用户之前的pending请求
|
||
db.prepare('DELETE FROM password_reset_requests WHERE user_id = ? AND status = ?')
|
||
.run(userId, 'pending');
|
||
|
||
const stmt = db.prepare(`
|
||
INSERT INTO password_reset_requests (user_id, new_password, status)
|
||
VALUES (?, ?, 'pending')
|
||
`);
|
||
|
||
const result = stmt.run(userId, hashedPassword);
|
||
return result.lastInsertRowid;
|
||
},
|
||
|
||
// 获取待审核的请求
|
||
getPending() {
|
||
return db.prepare(`
|
||
SELECT r.*, u.username, u.email
|
||
FROM password_reset_requests r
|
||
JOIN users u ON r.user_id = u.id
|
||
WHERE r.status = 'pending'
|
||
ORDER BY r.created_at DESC
|
||
`).all();
|
||
},
|
||
|
||
// 审核请求(批准或拒绝)
|
||
review(requestId, adminId, approved) {
|
||
const request = db.prepare('SELECT * FROM password_reset_requests WHERE id = ?').get(requestId);
|
||
|
||
if (!request || request.status !== 'pending') {
|
||
throw new Error('请求不存在或已被处理');
|
||
}
|
||
|
||
const newStatus = approved ? 'approved' : 'rejected';
|
||
|
||
db.prepare(`
|
||
UPDATE password_reset_requests
|
||
SET status = ?, reviewed_at = CURRENT_TIMESTAMP, reviewed_by = ?
|
||
WHERE id = ?
|
||
`).run(newStatus, adminId, requestId);
|
||
|
||
// 如果批准,更新用户密码
|
||
if (approved) {
|
||
db.prepare('UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
|
||
.run(request.new_password, request.user_id);
|
||
}
|
||
|
||
return true;
|
||
},
|
||
|
||
// 获取用户的所有请求
|
||
getUserRequests(userId) {
|
||
return db.prepare(`
|
||
SELECT * FROM password_reset_requests
|
||
WHERE user_id = ?
|
||
ORDER BY created_at DESC
|
||
`).all(userId);
|
||
},
|
||
|
||
// 检查用户是否有待处理的请求
|
||
hasPendingRequest(userId) {
|
||
const request = db.prepare(`
|
||
SELECT id FROM password_reset_requests
|
||
WHERE user_id = ? AND status = 'pending'
|
||
`).get(userId);
|
||
return !!request;
|
||
}
|
||
};
|
||
|
||
// 初始化默认设置
|
||
function initDefaultSettings() {
|
||
// 默认上传限制为10GB
|
||
if (!SettingsDB.get('max_upload_size')) {
|
||
SettingsDB.set('max_upload_size', '10737418240'); // 10GB in bytes
|
||
}
|
||
}
|
||
|
||
// 数据库版本迁移 - 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;
|
||
}
|
||
}
|
||
|
||
// 初始化数据库
|
||
initDatabase();
|
||
createDefaultAdmin();
|
||
initDefaultSettings();
|
||
migrateToV2(); // 执行数据库迁移
|
||
|
||
module.exports = {
|
||
db,
|
||
UserDB,
|
||
ShareDB,
|
||
SettingsDB,
|
||
PasswordResetDB
|
||
};
|