feat(rate-limit): per-user mutation-rate-limiting (v1-readiness #3)
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>
This commit is contained in:
parent
43778e3bcb
commit
a0a10001d5
16 changed files with 175 additions and 13 deletions
64
__tests__/lib/rate-limit.test.ts
Normal file
64
__tests__/lib/rate-limit.test.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
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()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue