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 @@ + +
+
+

+ 系统日志 +

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ 共 {{ systemLogs.total }} 条日志,第 {{ systemLogs.page }}/{{ systemLogs.totalPages }} 页 +
+ + +
+
+ +
+ {{ formatLogTime(log.created_at) }} +
+ +
+ + {{ getLogLevelText(log.level) }} + +
+ +
+ + {{ getLogCategoryText(log.category) }} +
+ +
+
{{ log.action }}
+
{{ log.message }}
+
+ {{ log.username }} + {{ log.ip_address }} +
+
+
+
+ + +
+ +

暂无日志记录

+
+ + +
+ +

加载中...

+
+ + +
+ + + {{ systemLogs.page }} / {{ systemLogs.totalPages }} + + +
+
+

用户管理

diff --git a/frontend/app.js b/frontend/app.js index 83110a1..c770110 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -157,6 +157,21 @@ createApp({ checks: [] }, + // 系统日志 + systemLogs: { + loading: false, + logs: [], + total: 0, + page: 1, + pageSize: 30, + totalPages: 0, + filters: { + level: '', + category: '', + keyword: '' + } + }, + // Toast通知 toasts: [], toastIdCounter: 0, @@ -2050,11 +2065,12 @@ handleDragLeave(e) { this.loadShares(); break; case 'admin': - // 切换到管理后台时,重新加载用户列表和健康检测 + // 切换到管理后台时,重新加载用户列表、健康检测和系统日志 if (this.user && this.user.is_admin) { this.loadUsers(); this.loadServerStorageStats(); this.loadHealthCheck(); + this.loadSystemLogs(1); } break; case 'settings': @@ -2331,6 +2347,125 @@ handleDragLeave(e) { return texts[status] || '未知'; }, + // ===== 系统日志 ===== + + async loadSystemLogs(page = 1) { + this.systemLogs.loading = true; + try { + const params = new URLSearchParams({ + page: page, + pageSize: this.systemLogs.pageSize + }); + + if (this.systemLogs.filters.level) { + params.append('level', this.systemLogs.filters.level); + } + if (this.systemLogs.filters.category) { + params.append('category', this.systemLogs.filters.category); + } + if (this.systemLogs.filters.keyword) { + params.append('keyword', this.systemLogs.filters.keyword); + } + + const response = await axios.get(`${this.apiBase}/api/admin/logs?${params}`, { + headers: { Authorization: `Bearer ${this.token}` } + }); + + if (response.data.success) { + this.systemLogs.logs = response.data.logs; + this.systemLogs.total = response.data.total; + this.systemLogs.page = response.data.page; + this.systemLogs.totalPages = response.data.totalPages; + } + } catch (error) { + console.error('加载系统日志失败:', error); + this.showToast('error', '错误', '加载系统日志失败'); + } finally { + this.systemLogs.loading = false; + } + }, + + filterLogs() { + this.loadSystemLogs(1); + }, + + clearLogFilters() { + this.systemLogs.filters = { level: '', category: '', keyword: '' }; + this.loadSystemLogs(1); + }, + + getLogLevelColor(level) { + const colors = { + debug: 'background: #6c757d; color: white;', + info: 'background: #17a2b8; color: white;', + warn: 'background: #ffc107; color: black;', + error: 'background: #dc3545; color: white;' + }; + return colors[level] || 'background: #6c757d; color: white;'; + }, + + getLogLevelText(level) { + const texts = { debug: '调试', info: '信息', warn: '警告', error: '错误' }; + return texts[level] || level; + }, + + getLogCategoryText(category) { + const texts = { + auth: '认证', + user: '用户', + file: '文件', + share: '分享', + system: '系统', + security: '安全' + }; + return texts[category] || category; + }, + + getLogCategoryIcon(category) { + const icons = { + auth: 'fa-key', + user: 'fa-user', + file: 'fa-file', + share: 'fa-share-alt', + system: 'fa-cog', + security: 'fa-shield-alt' + }; + return icons[category] || 'fa-info'; + }, + + formatLogTime(timestamp) { + if (!timestamp) return ''; + const date = new Date(timestamp); + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + }, + + async cleanupLogs() { + if (!confirm('确定要清理90天前的日志吗?此操作不可恢复。')) return; + + try { + const response = await axios.post( + `${this.apiBase}/api/admin/logs/cleanup`, + { keepDays: 90 }, + { headers: { Authorization: `Bearer ${this.token}` } } + ); + + if (response.data.success) { + this.showToast('success', '成功', response.data.message); + this.loadSystemLogs(1); + } + } catch (error) { + console.error('清理日志失败:', error); + this.showToast('error', '错误', '清理日志失败'); + } + }, + // ===== 上传工具管理 ===== // 检测上传工具是否存在