Skip to content

Commit f21dc1f

Browse files
committed
feat: 添加 ticket 创建的速率限制
1 parent 4f39694 commit f21dc1f

File tree

1 file changed

+54
-0
lines changed

1 file changed

+54
-0
lines changed

api/ticket/api.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const _ = require('lodash')
22
const AV = require('leanengine')
33
const { Router } = require('express')
44
const { check, query } = require('express-validator')
5+
const Redis = require('ioredis')
56

67
const { captureException } = require('../errorHandler')
78
const { checkPermission } = require('../../oauth/lc')
@@ -17,6 +18,27 @@ const config = require('../../config')
1718
const Ticket = require('./model')
1819
const { getRoles } = require('../common')
1920

21+
// Initialize Redis client if URL is configured
22+
let redisClient = null
23+
if (process.env.REDIS_URL_CACHE) {
24+
try {
25+
redisClient = new Redis(process.env.REDIS_URL_CACHE)
26+
redisClient.on('error', (err) => {
27+
console.error('Redis connection error:', err)
28+
// Optionally disable Redis features if connection fails persistently
29+
redisClient = null
30+
captureException(new Error('Redis connection failed'), { extra: { component: 'TicketAPI' } })
31+
})
32+
console.log('Redis client initialized for rate limiting.')
33+
} catch (error) {
34+
console.error('Failed to initialize Redis client:', error)
35+
captureException(error, { extra: { component: 'TicketAPI', msg: 'Redis init failed' } })
36+
redisClient = null
37+
}
38+
} else {
39+
console.warn('REDIS_URL_CACHE not set. Rate limiting is disabled.')
40+
}
41+
2042
const TICKET_SORT_KEY_MAP = {
2143
created_at: 'createdAt',
2244
updated_at: 'updatedAt',
@@ -73,6 +95,38 @@ router.post(
7395
res.throw(403, 'Your account is not qualified to create ticket.')
7496
}
7597

98+
// === Rate Limiting Start ===
99+
const currentUser = req.user
100+
const isCS = await isCSInTicket(currentUser)
101+
102+
if (!isCS && redisClient) {
103+
try {
104+
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '') // YYYYMMDD
105+
const redisKey = `rate_limit:ticket:create:${currentUser.id}:${today}`
106+
const currentCount = await redisClient.incr(redisKey)
107+
108+
if (currentCount === 1) {
109+
// Set expiry to 24 hours when the key is first created today
110+
await redisClient.expire(redisKey, 86400) // 86400 seconds = 24 hours
111+
}
112+
113+
if (currentCount > 20) {
114+
console.warn(`Rate limit exceeded for user ${currentUser.id}. Count: ${currentCount}`)
115+
// Optionally log the violation details
116+
// LogRateLimitViolation({ userId: currentUser.id, limit: 20, resource: 'ticket_create' })
117+
return res.throw(429, 'Rate limit exceeded. You can create up to 20 tickets per day.')
118+
}
119+
} catch (error) {
120+
console.error(`Redis rate limiting check failed for user ${currentUser.id}:`, error)
121+
captureException(error, {
122+
extra: { component: 'TicketAPI', msg: 'Rate limit check failed', userId: currentUser.id },
123+
})
124+
// Fail open: If Redis fails, allow the request to proceed.
125+
// Alternatively, you could fail closed: return res.throw(500, 'Internal server error during rate check.')
126+
}
127+
}
128+
// === Rate Limiting End ===
129+
76130
const {
77131
title,
78132
category_id,

0 commit comments

Comments
 (0)