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": {
"testEnvironment": "node",
"transform": {
"^.+\\.tsx?$": "ts-jest"
"^.+\\.tsx?$": "@swc/jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
@@ -34,13 +34,18 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@prisma/adapter-mariadb": "^7.9.1",
"@prisma/client": "^7.9.1",
"dotenv": "^17.4.2",
"mariadb": "^3.5.3",
"next": "^16.3.0",
"prisma": "^7.9.1",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@swc/core": "^1.15.47",
"@swc/jest": "^0.2.39",
"@types/jest": "^30.0.0",
"@types/node": "^26.2.0",
"@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
}