mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 16:14:12 +00:00
fix: standardize rate limit headers and remove dead code
- Use rateLimitHeaders() spread on /api/posts success response to include X-RateLimit-Limit, X-RateLimit-Reset, Retry-After matching /api/search and /api/search/suggestions behavior - Remove legacy checkRateLimit, RateLimitResult, rateLimitStore, and cleanup interval from src/lib/search.ts (dead code after migration to central searchLimiter) - Update tests to use searchLimiter from @/lib/security instead of the removed checkRateLimit All integration tests pass (20 passed, 12 skipped). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2fae62eaf3
commit
82b4a4e558
3 changed files with 8 additions and 56 deletions
|
|
@ -134,7 +134,7 @@ export async function GET(request: NextRequest) {
|
|||
|
||||
return NextResponse.json(response, {
|
||||
headers: {
|
||||
'X-RateLimit-Remaining': String(rateLimit.remaining),
|
||||
...rateLimitHeaders(rateLimit, POSTS_RATE_LIMIT),
|
||||
'Cache-Control': 'public, max-age=60, s-maxage=60',
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -79,12 +79,6 @@ export interface PaginatedPosts {
|
|||
hasPrevPage: boolean
|
||||
}
|
||||
|
||||
export interface RateLimitResult {
|
||||
allowed: boolean
|
||||
remaining: number
|
||||
retryAfter?: number
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TTL Cache Implementation
|
||||
// ============================================================================
|
||||
|
|
@ -154,48 +148,6 @@ class TTLCache<K, V> {
|
|||
export const searchCache = new TTLCache<string, SearchResult>(60)
|
||||
export const suggestionCache = new TTLCache<string, Suggestion[]>(60)
|
||||
|
||||
// ============================================================================
|
||||
// Rate Limiting
|
||||
// ============================================================================
|
||||
|
||||
interface RateLimitEntry {
|
||||
count: number
|
||||
windowStart: number
|
||||
}
|
||||
|
||||
const rateLimitStore = new Map<string, RateLimitEntry>()
|
||||
const RATE_LIMIT_WINDOW_MS = 60000 // 1 minute
|
||||
const RATE_LIMIT_MAX_REQUESTS = 30 // 30 requests per minute
|
||||
|
||||
// Cleanup rate limit entries every 5 minutes
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
for (const [key, entry] of rateLimitStore.entries()) {
|
||||
if (now - entry.windowStart > RATE_LIMIT_WINDOW_MS * 2) {
|
||||
rateLimitStore.delete(key)
|
||||
}
|
||||
}
|
||||
}, 300000)
|
||||
|
||||
export function checkRateLimit(ip: string): RateLimitResult {
|
||||
const now = Date.now()
|
||||
const entry = rateLimitStore.get(ip)
|
||||
|
||||
if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
|
||||
// New window
|
||||
rateLimitStore.set(ip, { count: 1, windowStart: now })
|
||||
return { allowed: true, remaining: RATE_LIMIT_MAX_REQUESTS - 1 }
|
||||
}
|
||||
|
||||
if (entry.count >= RATE_LIMIT_MAX_REQUESTS) {
|
||||
const retryAfter = Math.ceil((entry.windowStart + RATE_LIMIT_WINDOW_MS - now) / 1000)
|
||||
return { allowed: false, remaining: 0, retryAfter }
|
||||
}
|
||||
|
||||
entry.count++
|
||||
return { allowed: true, remaining: RATE_LIMIT_MAX_REQUESTS - entry.count }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import {
|
|||
searchPosts,
|
||||
getSearchSuggestions,
|
||||
getPostsByCategory,
|
||||
checkRateLimit,
|
||||
searchCache,
|
||||
suggestionCache,
|
||||
extractTextFromLexical,
|
||||
} from '@/lib/search'
|
||||
import { searchLimiter } from '@/lib/security'
|
||||
|
||||
let payload: Payload
|
||||
|
||||
|
|
@ -78,24 +78,24 @@ describe('Search Library', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('checkRateLimit', () => {
|
||||
it('allows requests within limit', () => {
|
||||
describe('searchLimiter (central rate limiter)', () => {
|
||||
it('allows requests within limit', async () => {
|
||||
const testIp = `test-${Date.now()}-1`
|
||||
const result = checkRateLimit(testIp)
|
||||
const result = await searchLimiter.check(testIp)
|
||||
|
||||
expect(result.allowed).toBe(true)
|
||||
expect(result.remaining).toBe(29) // 30 - 1
|
||||
})
|
||||
|
||||
it('blocks requests exceeding limit', () => {
|
||||
it('blocks requests exceeding limit', async () => {
|
||||
const testIp = `test-${Date.now()}-2`
|
||||
|
||||
// Use up all requests
|
||||
for (let i = 0; i < 30; i++) {
|
||||
checkRateLimit(testIp)
|
||||
await searchLimiter.check(testIp)
|
||||
}
|
||||
|
||||
const result = checkRateLimit(testIp)
|
||||
const result = await searchLimiter.check(testIp)
|
||||
expect(result.allowed).toBe(false)
|
||||
expect(result.remaining).toBe(0)
|
||||
expect(result.retryAfter).toBeGreaterThan(0)
|
||||
|
|
|
|||
Loading…
Reference in a new issue