Initial commit - 玩玩云文件管理系统 v1.0.0
- 完整的前后端代码 - 支持本地存储和SFTP存储 - 文件分享功能 - 上传工具源代码 - 完整的部署文档 - Nginx配置模板 技术栈: - 后端: Node.js + Express + SQLite - 前端: Vue.js 3 + Axios - 存储: 本地存储 / SFTP远程存储
This commit is contained in:
554
backend/database.js
Normal file
554
backend/database.js
Normal file
@@ -0,0 +1,554 @@
|
||||
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 hashedPassword = bcrypt.hashSync('admin123', 10);
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO users (
|
||||
username, email, password,
|
||||
is_admin, is_active, has_ftp_config
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
'admin',
|
||||
'admin@example.com',
|
||||
hashedPassword,
|
||||
1,
|
||||
1,
|
||||
0 // 管理员不需要FTP配置
|
||||
);
|
||||
|
||||
console.log('默认管理员账号已创建');
|
||||
console.log('用户名: admin');
|
||||
console.log('密码: admin123');
|
||||
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));
|
||||
expiresAt = expireDate.toISOString();
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
},
|
||||
|
||||
// 根据分享码查找
|
||||
findByCode(shareCode) {
|
||||
return 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 = ?
|
||||
`).get(shareCode);
|
||||
},
|
||||
|
||||
// 根据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() {
|
||||
// 默认上传限制为100MB
|
||||
if (!SettingsDB.get('max_upload_size')) {
|
||||
SettingsDB.set('max_upload_size', '104857600'); // 100MB 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
|
||||
};
|
||||
Reference in New Issue
Block a user