diff --git a/backend/database.js b/backend/database.js index 7ffe770..49b97d8 100644 --- a/backend/database.js +++ b/backend/database.js @@ -148,6 +148,32 @@ function initDatabase() { 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('数据库初始化完成'); } @@ -610,6 +636,141 @@ function migrateToV2() { } } +// 系统日志操作 +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) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + 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', '-' || ? || ' 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', '-' || ? || ' days') + `).run(keepDays); + + return result.changes; + } +}; + // 初始化数据库 initDatabase(); createDefaultAdmin(); @@ -622,5 +783,6 @@ module.exports = { ShareDB, SettingsDB, VerificationDB, - PasswordResetTokenDB + PasswordResetTokenDB, + SystemLogDB }; diff --git a/backend/server.js b/backend/server.js index 4529b43..5935d5a 100644 --- a/backend/server.js +++ b/backend/server.js @@ -20,7 +20,7 @@ const util = require('util'); const execAsync = util.promisify(exec); const execFileAsync = util.promisify(execFile); -const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB } = require('./database'); +const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB, SystemLogDB } = require('./database'); const { generateToken, authMiddleware, adminMiddleware, isJwtSecretSecure } = require('./auth'); const app = express(); @@ -359,6 +359,96 @@ function getProtocol(req) { return req.protocol || (req.secure ? 'https' : 'http'); } +// ===== 系统日志工具函数 ===== + +// 从请求中提取日志信息 +function getLogInfoFromReq(req) { + return { + ipAddress: req.ip || req.socket?.remoteAddress || 'unknown', + userAgent: req.get('User-Agent') || 'unknown', + userId: req.user?.id || null, + username: req.user?.username || null + }; +} + +// 记录认证日志 +function logAuth(req, action, message, details = null, level = 'info') { + const info = getLogInfoFromReq(req); + SystemLogDB.log({ + level, + category: 'auth', + action, + message, + ...info, + details + }); +} + +// 记录用户管理日志 +function logUser(req, action, message, details = null, level = 'info') { + const info = getLogInfoFromReq(req); + SystemLogDB.log({ + level, + category: 'user', + action, + message, + ...info, + details + }); +} + +// 记录文件操作日志 +function logFile(req, action, message, details = null, level = 'info') { + const info = getLogInfoFromReq(req); + SystemLogDB.log({ + level, + category: 'file', + action, + message, + ...info, + details + }); +} + +// 记录分享操作日志 +function logShare(req, action, message, details = null, level = 'info') { + const info = getLogInfoFromReq(req); + SystemLogDB.log({ + level, + category: 'share', + action, + message, + ...info, + details + }); +} + +// 记录系统操作日志 +function logSystem(req, action, message, details = null, level = 'info') { + const info = req ? getLogInfoFromReq(req) : {}; + SystemLogDB.log({ + level, + category: 'system', + action, + message, + ...info, + details + }); +} + +// 记录安全事件日志 +function logSecurity(req, action, message, details = null, level = 'warn') { + const info = getLogInfoFromReq(req); + SystemLogDB.log({ + level, + category: 'security', + action, + message, + ...info, + details + }); +} + // 文件上传配置(临时存储) const upload = multer({ dest: path.join(__dirname, 'uploads'), @@ -1148,6 +1238,9 @@ app.post('/api/register', }); } + // 记录注册日志 + logAuth(req, 'register', `新用户注册: ${username}`, { userId, email }); + res.json({ success: true, message: '注册成功,请查收邮箱完成验证', @@ -1155,6 +1248,7 @@ app.post('/api/register', }); } catch (error) { console.error('注册失败:', error); + logAuth(req, 'register_failed', `用户注册失败: ${req.body.username || 'unknown'}`, { error: error.message }, 'error'); res.status(500).json({ success: false, message: '注册失败: ' + error.message @@ -1448,6 +1542,9 @@ app.post('/api/login', } if (!UserDB.verifyPassword(password, user.password)) { + // 记录登录失败安全日志 + logSecurity(req, 'login_failed', `登录失败(密码错误): ${username}`, { userId: user.id }); + // 记录失败尝试 if (req.rateLimitKeys) { const result = loginLimiter.recordFailure(req.rateLimitKeys.ipKey); @@ -1487,6 +1584,9 @@ app.post('/api/login', path: '/' // 限制Cookie作用域 }); + // 记录登录成功日志 + logAuth(req, 'login', `用户登录成功: ${user.username}`, { userId: user.id, isAdmin: user.is_admin }); + res.json({ success: true, message: '登录成功', @@ -1506,6 +1606,7 @@ app.post('/api/login', }); } catch (error) { console.error('登录失败:', error); + logAuth(req, 'login_error', `登录异常: ${req.body.username || 'unknown'}`, { error: error.message }, 'error'); res.status(500).json({ success: false, message: '登录失败: ' + error.message @@ -3720,6 +3821,91 @@ app.get('/api/admin/users', authMiddleware, adminMiddleware, (req, res) => { } }); +// 获取系统日志 +app.get('/api/admin/logs', authMiddleware, adminMiddleware, (req, res) => { + try { + const { + page = 1, + pageSize = 50, + level, + category, + userId, + startDate, + endDate, + keyword + } = req.query; + + const result = SystemLogDB.query({ + page: parseInt(page), + pageSize: Math.min(parseInt(pageSize) || 50, 200), // 限制最大每页200条 + level: level || null, + category: category || null, + userId: userId ? parseInt(userId) : null, + startDate: startDate || null, + endDate: endDate || null, + keyword: keyword || null + }); + + res.json({ + success: true, + ...result + }); + } catch (error) { + console.error('获取系统日志失败:', error); + res.status(500).json({ + success: false, + message: '获取系统日志失败: ' + error.message + }); + } +}); + +// 获取日志统计 +app.get('/api/admin/logs/stats', authMiddleware, adminMiddleware, (req, res) => { + try { + const categoryStats = SystemLogDB.getStatsByCategory(); + const dateStats = SystemLogDB.getStatsByDate(7); + + res.json({ + success: true, + stats: { + byCategory: categoryStats, + byDate: dateStats + } + }); + } catch (error) { + console.error('获取日志统计失败:', error); + res.status(500).json({ + success: false, + message: '获取日志统计失败: ' + error.message + }); + } +}); + +// 清理旧日志 +app.post('/api/admin/logs/cleanup', authMiddleware, adminMiddleware, (req, res) => { + try { + const { keepDays = 90 } = req.body; + const days = Math.max(7, Math.min(parseInt(keepDays) || 90, 365)); // 最少保留7天,最多365天 + + const deletedCount = SystemLogDB.cleanup(days); + + // 记录清理操作 + logSystem(req, 'logs_cleanup', `管理员清理了 ${deletedCount} 条日志(保留 ${days} 天)`, { deletedCount, keepDays: days }); + + res.json({ + success: true, + message: `已清理 ${deletedCount} 条日志`, + deletedCount + }); + } catch (error) { + console.error('清理日志失败:', error); + res.status(500).json({ + success: false, + message: '清理日志失败: ' + error.message + }); + } +}); + // 封禁/解封用户 app.post('/api/admin/users/:id/ban', authMiddleware, adminMiddleware, (req, res) => { try { diff --git a/frontend/app.html b/frontend/app.html index 4fdde2a..85ed3c0 100644 --- a/frontend/app.html +++ b/frontend/app.html @@ -1874,6 +1874,118 @@ + +
暂无日志记录
+加载中...
+