mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 20:54:11 +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, {
|
return NextResponse.json(response, {
|
||||||
headers: {
|
headers: {
|
||||||
'X-RateLimit-Remaining': String(rateLimit.remaining),
|
...rateLimitHeaders(rateLimit, POSTS_RATE_LIMIT),
|
||||||
'Cache-Control': 'public, max-age=60, s-maxage=60',
|
'Cache-Control': 'public, max-age=60, s-maxage=60',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -79,12 +79,6 @@ export interface PaginatedPosts {
|
||||||
hasPrevPage: boolean
|
hasPrevPage: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RateLimitResult {
|
|
||||||
allowed: boolean
|
|
||||||
remaining: number
|
|
||||||
retryAfter?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// TTL Cache Implementation
|
// TTL Cache Implementation
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -154,48 +148,6 @@ class TTLCache<K, V> {
|
||||||
export const searchCache = new TTLCache<string, SearchResult>(60)
|
export const searchCache = new TTLCache<string, SearchResult>(60)
|
||||||
export const suggestionCache = new TTLCache<string, Suggestion[]>(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
|
// Helper Functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ import {
|
||||||
searchPosts,
|
searchPosts,
|
||||||
getSearchSuggestions,
|
getSearchSuggestions,
|
||||||
getPostsByCategory,
|
getPostsByCategory,
|
||||||
checkRateLimit,
|
|
||||||
searchCache,
|
searchCache,
|
||||||
suggestionCache,
|
suggestionCache,
|
||||||
extractTextFromLexical,
|
extractTextFromLexical,
|
||||||
} from '@/lib/search'
|
} from '@/lib/search'
|
||||||
|
import { searchLimiter } from '@/lib/security'
|
||||||
|
|
||||||
let payload: Payload
|
let payload: Payload
|
||||||
|
|
||||||
|
|
@ -78,24 +78,24 @@ describe('Search Library', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('checkRateLimit', () => {
|
describe('searchLimiter (central rate limiter)', () => {
|
||||||
it('allows requests within limit', () => {
|
it('allows requests within limit', async () => {
|
||||||
const testIp = `test-${Date.now()}-1`
|
const testIp = `test-${Date.now()}-1`
|
||||||
const result = checkRateLimit(testIp)
|
const result = await searchLimiter.check(testIp)
|
||||||
|
|
||||||
expect(result.allowed).toBe(true)
|
expect(result.allowed).toBe(true)
|
||||||
expect(result.remaining).toBe(29) // 30 - 1
|
expect(result.remaining).toBe(29) // 30 - 1
|
||||||
})
|
})
|
||||||
|
|
||||||
it('blocks requests exceeding limit', () => {
|
it('blocks requests exceeding limit', async () => {
|
||||||
const testIp = `test-${Date.now()}-2`
|
const testIp = `test-${Date.now()}-2`
|
||||||
|
|
||||||
// Use up all requests
|
// Use up all requests
|
||||||
for (let i = 0; i < 30; i++) {
|
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.allowed).toBe(false)
|
||||||
expect(result.remaining).toBe(0)
|
expect(result.remaining).toBe(0)
|
||||||
expect(result.retryAfter).toBeGreaterThan(0)
|
expect(result.retryAfter).toBeGreaterThan(0)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue