lib/rate-limit.ts: 11 nieuwe scope-configs + enforceUserRateLimit(scope, userId)
helper. Returnt { error, code: 429 } shape voor consistent foutbeleid.
Toegepast op de high-value mutation-paths:
- actions/pbis.ts createPbiAction
- actions/stories.ts createStoryAction
- actions/tasks.ts saveTask (alleen create-path) + createTaskAction
- actions/todos.ts createTodoAction
- actions/sprints.ts createSprintAction
- actions/products.ts createProductAction + createProductFormAction
- actions/api-tokens.ts createApiTokenAction
- actions/questions.ts answerQuestion
- actions/claude-jobs.ts enqueueClaudeJobAction + enqueueClaudeJobsBatchAction
- app/api/profile/avatar/route.ts POST
- app/api/stories/[id]/log/route.ts POST
Limits zijn ruim genoeg voor normaal gebruik, eng genoeg voor abuse-loops:
create-task 100/min, create-todo 60/min, create-pbi 30/min, create-product
5/min, create-token 10/uur, etc. Per-user scope (geen globale block).
Niet aangeraakt: reorder/status-toggle (intra-session frequent, lage abuse),
update/delete (laag-volume), cron-routes (CRON_SECRET-gated).
Consumer-tweaks: 'success' in result narrowing waar TS de bredere union niet
meer accepteerde. Tests: 9 nieuwe op rate-limit-helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
64 lines
2.5 KiB
TypeScript
64 lines
2.5 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { checkRateLimit, enforceUserRateLimit, _resetRateLimit } from '@/lib/rate-limit'
|
|
|
|
beforeEach(() => {
|
|
_resetRateLimit()
|
|
})
|
|
|
|
describe('checkRateLimit (legacy auth-keys)', () => {
|
|
it('staat de eerste request toe', () => {
|
|
expect(checkRateLimit('login:1.2.3.4')).toBe(true)
|
|
})
|
|
|
|
it('blokkeert na exceeding max (login: 10/min)', () => {
|
|
for (let i = 0; i < 10; i++) checkRateLimit('login:1.2.3.4')
|
|
expect(checkRateLimit('login:1.2.3.4')).toBe(false)
|
|
})
|
|
|
|
it('register heeft eigen lagere limiet (5/uur)', () => {
|
|
for (let i = 0; i < 5; i++) checkRateLimit('register:9.9.9.9')
|
|
expect(checkRateLimit('register:9.9.9.9')).toBe(false)
|
|
})
|
|
|
|
it('verschillende keys hebben hun eigen counter', () => {
|
|
for (let i = 0; i < 10; i++) checkRateLimit('login:1.1.1.1')
|
|
expect(checkRateLimit('login:1.1.1.1')).toBe(false)
|
|
expect(checkRateLimit('login:2.2.2.2')).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('enforceUserRateLimit (v1-readiness #3 mutation-scopes)', () => {
|
|
it('returnt null bij eerste call', () => {
|
|
expect(enforceUserRateLimit('create-pbi', 'user-1')).toBeNull()
|
|
})
|
|
|
|
it('returnt 429-shape na exceeding limiet', () => {
|
|
// create-product limiet = 5/min
|
|
for (let i = 0; i < 5; i++) enforceUserRateLimit('create-product', 'user-1')
|
|
const result = enforceUserRateLimit('create-product', 'user-1')
|
|
expect(result).not.toBeNull()
|
|
expect(result?.code).toBe(429)
|
|
expect(result?.error).toContain('Te veel acties')
|
|
})
|
|
|
|
it('scope is per (action, user) — andere user heeft eigen quota', () => {
|
|
for (let i = 0; i < 5; i++) enforceUserRateLimit('create-product', 'user-A')
|
|
expect(enforceUserRateLimit('create-product', 'user-A')).not.toBeNull()
|
|
expect(enforceUserRateLimit('create-product', 'user-B')).toBeNull()
|
|
})
|
|
|
|
it('verschillende scopes voor dezelfde user vullen apart', () => {
|
|
for (let i = 0; i < 5; i++) enforceUserRateLimit('create-product', 'user-1')
|
|
expect(enforceUserRateLimit('create-product', 'user-1')).not.toBeNull()
|
|
// create-task heeft eigen counter
|
|
expect(enforceUserRateLimit('create-task', 'user-1')).toBeNull()
|
|
})
|
|
|
|
it('create-task limiet (100) is hoger dan create-pbi (30)', () => {
|
|
for (let i = 0; i < 30; i++) enforceUserRateLimit('create-pbi', 'u')
|
|
expect(enforceUserRateLimit('create-pbi', 'u')).not.toBeNull()
|
|
// create-task is nog niet hit
|
|
for (let i = 0; i < 30; i++) enforceUserRateLimit('create-task', 'u')
|
|
expect(enforceUserRateLimit('create-task', 'u')).toBeNull()
|
|
})
|
|
})
|