fix: 全面检查修复 — 路由顺序 + 安全加固
- logs.ts: /track/stats 和 /track/list 移到 /:date 之前,修复路由匹配问题 - config.ts: 添加配置键白名单校验,限制值长度 500 字符 - track.ts: 添加输入校验,限制事件数量和字段长度 - index.ts: /api/track 添加 rate limiter
This commit is contained in:
@@ -159,7 +159,7 @@ app.use('/api/filters', apiLimiter, filterRoutes)
|
|||||||
app.use('/api/admin', apiLimiter, adminRoutes)
|
app.use('/api/admin', apiLimiter, adminRoutes)
|
||||||
app.use('/api/feedback', apiLimiter, feedbackRoutes)
|
app.use('/api/feedback', apiLimiter, feedbackRoutes)
|
||||||
app.use('/api/config', apiLimiter, configRoutes)
|
app.use('/api/config', apiLimiter, configRoutes)
|
||||||
app.use('/api/track', trackRoutes)
|
app.use('/api/track', apiLimiter, trackRoutes)
|
||||||
app.use('/api/logs', apiLimiter, logsRoutes)
|
app.use('/api/logs', apiLimiter, logsRoutes)
|
||||||
|
|
||||||
app.get('/api/health', async (_req, res) => {
|
app.get('/api/health', async (_req, res) => {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ router.get('/', async (_req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 允许修改的配置键白名单 */
|
||||||
|
const ALLOWED_KEYS = ['app_name', 'app_version', 'app_slogan', 'app_description']
|
||||||
|
|
||||||
/** 更新配置(管理员权限) */
|
/** 更新配置(管理员权限) */
|
||||||
router.put('/', requireAdmin, async (req, res) => {
|
router.put('/', requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -27,8 +30,14 @@ router.put('/', requireAdmin, async (req, res) => {
|
|||||||
return res.status(400).json({ code: 40001, message: '无效的配置数据' })
|
return res.status(400).json({ code: 40001, message: '无效的配置数据' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量更新配置
|
// 批量更新配置(只允许白名单内的键)
|
||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
if (!ALLOWED_KEYS.includes(key)) {
|
||||||
|
return res.status(400).json({ code: 40001, message: `不允许修改配置项: ${key}` })
|
||||||
|
}
|
||||||
|
if (typeof value !== 'string' || value.length > 500) {
|
||||||
|
return res.status(400).json({ code: 40001, message: `配置值无效或过长: ${key}` })
|
||||||
|
}
|
||||||
await pool.query(
|
await pool.query(
|
||||||
'INSERT INTO sys_config (config_key, config_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE config_value = ?',
|
'INSERT INTO sys_config (config_key, config_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE config_value = ?',
|
||||||
[key, value, value]
|
[key, value, value]
|
||||||
|
|||||||
@@ -92,7 +92,87 @@ router.get('/stats', requireAdmin, (_req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 查询指定日期的日志 */
|
/** 获取埋点统计(必须在 /:date 之前注册) */
|
||||||
|
router.get('/track/stats', requireAdmin, async (_req, res) => {
|
||||||
|
try {
|
||||||
|
// 今日统计
|
||||||
|
const [todayRows] = await pool.execute(
|
||||||
|
`SELECT event, COUNT(*) as count FROM track_events
|
||||||
|
WHERE DATE(created_at) = CURDATE()
|
||||||
|
GROUP BY event ORDER BY count DESC`
|
||||||
|
)
|
||||||
|
|
||||||
|
// 最近7天每日统计
|
||||||
|
const [dailyRows] = await pool.execute(
|
||||||
|
`SELECT DATE(created_at) as date, COUNT(*) as count FROM track_events
|
||||||
|
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
|
||||||
|
GROUP BY DATE(created_at) ORDER BY date DESC`
|
||||||
|
)
|
||||||
|
|
||||||
|
// 最近7天事件分布
|
||||||
|
const [eventRows] = await pool.execute(
|
||||||
|
`SELECT event, COUNT(*) as count FROM track_events
|
||||||
|
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
|
||||||
|
GROUP BY event ORDER BY count DESC LIMIT 10`
|
||||||
|
)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
today: todayRows,
|
||||||
|
daily: dailyRows,
|
||||||
|
events: eventRows
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Track] Stats error:', err)
|
||||||
|
res.status(500).json({ code: 50000, message: '获取埋点统计失败' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 查询埋点事件(必须在 /:date 之前注册) */
|
||||||
|
router.get('/track/list', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { event, page = '1', pageSize = '50' } = req.query
|
||||||
|
const pNum = Math.max(1, parseInt(page as string) || 1)
|
||||||
|
const pSize = Math.min(200, Math.max(1, parseInt(pageSize as string) || 50))
|
||||||
|
const offset = (pNum - 1) * pSize
|
||||||
|
|
||||||
|
let where = '1=1'
|
||||||
|
const params: any[] = []
|
||||||
|
|
||||||
|
if (event && typeof event === 'string') {
|
||||||
|
where += ' AND t.event = ?'
|
||||||
|
params.push(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [countResult] = await pool.execute(
|
||||||
|
`SELECT COUNT(*) as total FROM track_events t WHERE ${where}`,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
const total = (countResult as any[])[0].total
|
||||||
|
|
||||||
|
const [rows] = await pool.execute(
|
||||||
|
`SELECT t.*, u.nickname
|
||||||
|
FROM track_events t
|
||||||
|
LEFT JOIN users u ON t.user_id = u.id
|
||||||
|
WHERE ${where}
|
||||||
|
ORDER BY t.created_at DESC
|
||||||
|
LIMIT ? OFFSET ?`,
|
||||||
|
[...params, pSize, offset]
|
||||||
|
)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
code: 0,
|
||||||
|
data: { list: rows, total, page: pNum, pageSize: pSize }
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Track] List error:', err)
|
||||||
|
res.status(500).json({ code: 50000, message: '查询埋点失败' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 查询指定日期的日志(必须在 /track/* 之后注册) */
|
||||||
router.get('/:date', requireAdmin, (req, res) => {
|
router.get('/:date', requireAdmin, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { date } = req.params
|
const { date } = req.params
|
||||||
@@ -144,84 +224,4 @@ router.get('/:date', requireAdmin, (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 获取埋点统计 */
|
|
||||||
router.get('/track/stats', requireAdmin, async (_req, res) => {
|
|
||||||
try {
|
|
||||||
// 今日统计
|
|
||||||
const [todayRows] = await pool.execute(
|
|
||||||
`SELECT event, COUNT(*) as count FROM track_events
|
|
||||||
WHERE DATE(created_at) = CURDATE()
|
|
||||||
GROUP BY event ORDER BY count DESC`
|
|
||||||
)
|
|
||||||
|
|
||||||
// 最近7天每日统计
|
|
||||||
const [dailyRows] = await pool.execute(
|
|
||||||
`SELECT DATE(created_at) as date, COUNT(*) as count FROM track_events
|
|
||||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
|
|
||||||
GROUP BY DATE(created_at) ORDER BY date DESC`
|
|
||||||
)
|
|
||||||
|
|
||||||
// 最近7天事件分布
|
|
||||||
const [eventRows] = await pool.execute(
|
|
||||||
`SELECT event, COUNT(*) as count FROM track_events
|
|
||||||
WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
|
|
||||||
GROUP BY event ORDER BY count DESC LIMIT 10`
|
|
||||||
)
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
code: 0,
|
|
||||||
data: {
|
|
||||||
today: todayRows,
|
|
||||||
daily: dailyRows,
|
|
||||||
events: eventRows
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Track] Stats error:', err)
|
|
||||||
res.status(500).json({ code: 50000, message: '获取埋点统计失败' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 查询埋点事件 */
|
|
||||||
router.get('/track/list', requireAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { event, page = '1', pageSize = '50' } = req.query
|
|
||||||
const pNum = Math.max(1, parseInt(page as string) || 1)
|
|
||||||
const pSize = Math.min(200, Math.max(1, parseInt(pageSize as string) || 50))
|
|
||||||
const offset = (pNum - 1) * pSize
|
|
||||||
|
|
||||||
let where = '1=1'
|
|
||||||
const params: any[] = []
|
|
||||||
|
|
||||||
if (event && typeof event === 'string') {
|
|
||||||
where += ' AND t.event = ?'
|
|
||||||
params.push(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [countResult] = await pool.execute(
|
|
||||||
`SELECT COUNT(*) as total FROM track_events t WHERE ${where}`,
|
|
||||||
params
|
|
||||||
)
|
|
||||||
const total = (countResult as any[])[0].total
|
|
||||||
|
|
||||||
const [rows] = await pool.execute(
|
|
||||||
`SELECT t.*, u.nickname
|
|
||||||
FROM track_events t
|
|
||||||
LEFT JOIN users u ON t.user_id = u.id
|
|
||||||
WHERE ${where}
|
|
||||||
ORDER BY t.created_at DESC
|
|
||||||
LIMIT ? OFFSET ?`,
|
|
||||||
[...params, pSize, offset]
|
|
||||||
)
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
code: 0,
|
|
||||||
data: { list: rows, total, page: pNum, pageSize: pSize }
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Track] List error:', err)
|
|
||||||
res.status(500).json({ code: 50000, message: '查询埋点失败' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -13,11 +13,15 @@ router.post('/', async (req, res) => {
|
|||||||
return res.status(400).json({ code: 40001, message: '无效的埋点数据' })
|
return res.status(400).json({ code: 40001, message: '无效的埋点数据' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量插入埋点事件
|
// 批量插入埋点事件(限制每个事件的字段长度)
|
||||||
for (const event of events) {
|
for (const event of events.slice(0, 50)) { // 最多 50 个事件
|
||||||
|
const eventType = typeof event.event === 'string' ? event.event.slice(0, 50) : 'unknown'
|
||||||
|
const page = typeof event.page === 'string' ? event.page.slice(0, 200) : ''
|
||||||
|
const data = event.data && typeof event.data === 'object' ? event.data : {}
|
||||||
|
|
||||||
await pool.execute(
|
await pool.execute(
|
||||||
'INSERT INTO track_events (event, page, data, user_id) VALUES (?, ?, ?, ?)',
|
'INSERT INTO track_events (event, page, data, user_id) VALUES (?, ?, ?, ?)',
|
||||||
[event.event, event.page || '', JSON.stringify(event.data || {}), userId]
|
[eventType, page, JSON.stringify(data), userId]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user