feat(admin): 添加系统日志功能

## 新功能

1. **系统日志数据库**
   - 新增 system_logs 表
   - 支持日志级别:debug/info/warn/error
   - 支持日志分类:auth/user/file/share/system/security
   - 记录用户ID、用户名、IP地址、User-Agent

2. **日志记录**
   - 用户注册成功/失败
   - 用户登录成功/失败(密码错误)
   - 系统操作(日志清理等)

3. **管理员API**
   - GET /api/admin/logs - 查询日志(支持分页和筛选)
   - GET /api/admin/logs/stats - 获取日志统计
   - POST /api/admin/logs/cleanup - 清理旧日志

4. **前端界面**
   - 日志列表展示(时间、级别、分类、内容、用户、IP)
   - 筛选功能(级别、分类、关键词搜索)
   - 分页导航
   - 清理旧日志功能

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-27 19:54:46 +08:00
parent 15ea15518c
commit c3bc58a88b
4 changed files with 598 additions and 3 deletions

View File

@@ -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
};

View File

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