feat: add IP-based rate limiter (swc/jest replaces ts-jest for TS7 compat)

This commit is contained in:
2026-08-10 14:28:13 +02:00
parent 28159d0a4a
commit 3feb336bb0
3 changed files with 66 additions and 1 deletions
+6 -1
View File
@@ -20,7 +20,7 @@
"jest": { "jest": {
"testEnvironment": "node", "testEnvironment": "node",
"transform": { "transform": {
"^.+\\.tsx?$": "ts-jest" "^.+\\.tsx?$": "@swc/jest"
}, },
"moduleNameMapper": { "moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1" "^@/(.*)$": "<rootDir>/src/$1"
@@ -34,13 +34,18 @@
"license": "ISC", "license": "ISC",
"type": "commonjs", "type": "commonjs",
"dependencies": { "dependencies": {
"@prisma/adapter-mariadb": "^7.9.1",
"@prisma/client": "^7.9.1", "@prisma/client": "^7.9.1",
"dotenv": "^17.4.2",
"mariadb": "^3.5.3",
"next": "^16.3.0", "next": "^16.3.0",
"prisma": "^7.9.1", "prisma": "^7.9.1",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8" "react-dom": "^19.2.8"
}, },
"devDependencies": { "devDependencies": {
"@swc/core": "^1.15.47",
"@swc/jest": "^0.2.39",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^26.2.0", "@types/node": "^26.2.0",
"@types/react": "^19.2.18", "@types/react": "^19.2.18",
+41
View File
@@ -0,0 +1,41 @@
import { isRateLimited } from '../rate-limit'
beforeEach(() => {
jest.useFakeTimers()
jest.resetModules()
})
afterEach(() => {
jest.useRealTimers()
})
describe('isRateLimited', () => {
it('allows requests under the limit', () => {
const ip = '1.2.3.4'
for (let i = 0; i < 5; i++) {
expect(isRateLimited(ip)).toBe(false)
}
})
it('blocks the 6th request within the window', () => {
const ip = '10.0.0.1'
for (let i = 0; i < 5; i++) isRateLimited(ip)
expect(isRateLimited(ip)).toBe(true)
})
it('resets after the window expires', () => {
const ip = '10.0.0.2'
for (let i = 0; i < 5; i++) isRateLimited(ip)
expect(isRateLimited(ip)).toBe(true)
jest.advanceTimersByTime(3_600_001)
expect(isRateLimited(ip)).toBe(false)
})
it('tracks different IPs independently', () => {
const ipA = '192.168.1.1'
const ipB = '192.168.1.2'
for (let i = 0; i < 5; i++) isRateLimited(ipA)
expect(isRateLimited(ipA)).toBe(true)
expect(isRateLimited(ipB)).toBe(false)
})
})
+19
View File
@@ -0,0 +1,19 @@
import { config } from '../../config/app.config'
type Entry = { count: number; resetAt: number }
const store = new Map<string, Entry>()
export function isRateLimited(ip: string): boolean {
const now = Date.now()
const entry = store.get(ip)
if (!entry || now > entry.resetAt) {
store.set(ip, { count: 1, resetAt: now + config.RATE_LIMIT_WINDOW_MS })
return false
}
if (entry.count >= config.RATE_LIMIT_MAX) return true
entry.count++
return false
}