Files
tech-achievement-management…/server/middleware/auditLogger.js
T
2026-04-19 14:05:40 +08:00

169 lines
7.1 KiB
JavaScript

const logger = require('../utils/logger');
const db = require('../db');
/**
* 根据请求方法和 URL 获取操作说明
*/
const getActionDescription = (method, url) => {
// 移除查询参数
const path = url.split('?')[0];
// --- Auth ---
if (path === '/api/auth/login' && method === 'POST') return '用户登录';
if (path === '/api/auth/register' && method === 'POST') return '用户注册';
if (path === '/api/auth/send-code' && method === 'POST') return '发送验证码';
if (path === '/api/auth/reset-password' && method === 'POST') return '重置密码';
// --- Achievements ---
if (path === '/api/achievements' && method === 'POST') return '上传成果';
if (path === '/api/achievements' && method === 'GET') {
if (url.includes('export_action=true')) return '导出用户成果数据';
return '查看成果列表';
}
if (path === '/api/achievements/export/csv' && method === 'GET') return '导出成果(CSV)';
if (path === '/api/achievements/export/zip' && method === 'GET') return '导出成果(ZIP)';
if (path === '/api/achievements/validate' && method === 'GET') return '数据校验';
// 动态路径匹配
if (path.match(/^\/api\/achievements\/audit\/\d+$/) && method === 'POST') return '审核成果';
if (path.match(/^\/api\/achievements\/\d+\/attachments\/zip$/) && method === 'GET') return '下载成果附件包';
if (path.match(/^\/api\/achievements\/attachments\/\d+\/download$/) && method === 'GET') return '下载单个附件';
if (path.match(/^\/api\/achievements\/\d+$/)) {
if (method === 'GET') return '查看成果详情';
if (method === 'PUT') return '修改成果';
if (method === 'DELETE') return '删除成果';
}
// --- Users ---
if (path === '/api/users' && method === 'POST') return '创建用户';
if (path === '/api/users' && method === 'GET') return '查看用户列表';
if (path === '/api/users/search' && method === 'GET') return '搜索用户';
if (path === '/api/users/pending-list' && method === 'GET') return '查看待定用户';
if (path.match(/^\/api\/users\/\d+$/)) {
if (method === 'PUT') return '修改用户';
if (method === 'DELETE') return '删除用户';
}
// --- Dictionaries ---
if (path === '/api/dictionaries/departments' && method === 'GET') return '查看部门列表(公开)';
if (path.match(/^\/api\/dictionaries\/[^/]+$/)) {
if (method === 'GET') return '查看字典列表';
if (method === 'POST') return '添加字典项';
}
if (path.match(/^\/api\/dictionaries\/[^/]+\/\d+$/)) {
if (method === 'PUT') return '修改字典项';
if (method === 'DELETE') return '删除字典项';
}
// --- Notifications ---
if (path === '/api/notifications' && method === 'GET') return '查看通知列表';
if (path === '/api/notifications' && method === 'POST') return '发布通知';
if (path.match(/^\/api\/notifications\/attachments\/\d+\/download$/) && method === 'GET') return '下载通知附件';
if (path.match(/^\/api\/notifications\/\d+$/)) {
if (method === 'GET') return '查看通知详情';
if (method === 'DELETE') return '删除通知';
}
// --- Statistics ---
if (path === '/api/statistics' && method === 'GET') return '查看统计数据';
// --- Logs ---
if (path === '/api/logs' && method === 'GET') return '查看系统日志';
// 默认
return null;
};
/**
* 全局审计日志中间件
* 记录所有 API 请求的操作人、IP、方法、URL 和 结果状态
*/
const auditLogger = (req, res, next) => {
// 记录请求开始时间
const start = Date.now();
// 监听请求完成事件
res.on('finish', async () => {
const duration = Date.now() - start;
const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
const method = req.method;
const url = req.originalUrl || req.url;
const status = res.statusCode;
// 忽略非 API 请求 (虽然通常挂载在 /api 下,但双重保险)
if (!url.startsWith('/api/')) return;
// 忽略 OPTIONS 预检请求,避免日志刷屏
if (method === 'OPTIONS') return;
// URL 脱敏处理:隐藏 token, password, code 等敏感参数
let safeUrl = url;
try {
safeUrl = safeUrl.replace(/([?&])(token|password|code)=([^&]+)/gi, '$1$2=***');
} catch (e) {
// 忽略正则错误,保持原样
}
let userInfo = 'Guest';
let userId = null;
let username = null;
let realName = null;
// 1. 尝试获取已登录用户信息
// 注意:req.user 由 verifyToken 中间件挂载。
// 由于我们在 res 'finish' 事件中读取,此时路由处理已完成,req.user 应该已存在(如果通过了认证)。
if (req.user) {
userInfo = `${req.user.real_name || req.user.username} (ID:${req.user.id})`;
userId = req.user.id;
username = req.user.username;
realName = req.user.real_name;
}
// 2. 对于未登录的关键接口,尝试从 body 中提取身份信息
else if (req.body) {
if (url.includes('/login')) {
userInfo = `[尝试登录: ${req.body.username || '未知'}]`;
username = req.body.username;
} else if (url.includes('/register')) {
userInfo = `[尝试注册: ${req.body.username || '未知'}]`;
username = req.body.username;
} else if (url.includes('/send-code')) {
userInfo = `[请求验证码: ${req.body.phoneNumber || '未知'}]`;
username = req.body.phoneNumber;
} else if (url.includes('/reset-password')) {
userInfo = `[重置密码: ${req.body.phoneNumber || '未知'}]`;
username = req.body.phoneNumber;
}
}
// 获取操作说明
const description = getActionDescription(method, url);
// 构建日志消息
const logMessage = `[AUDIT] IP:${clientIp} | User:${userInfo} | ${method} ${safeUrl} | Action:${description || 'Unknown'} | Status:${status} | Time:${duration}ms`;
// 根据状态码记录不同级别的日志
if (status >= 500) {
logger.error(logMessage);
} else if (status >= 400) {
logger.warn(logMessage);
} else {
logger.info(logMessage);
}
// 写入数据库
try {
await db.query(
'INSERT INTO audit_logs (user_id, username, real_name, ip_address, method, url, description, status, duration) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)',
[userId, username, realName, clientIp, method, safeUrl, description, status, duration]
);
} catch (err) {
// 数据库写入失败不应影响主流程,仅记录错误
console.error('审计日志写入数据库失败:', err);
}
});
next();
};
module.exports = auditLogger;