feat: add IP-based rate limiter (swc/jest replaces ts-jest for TS7 compat)
This commit is contained in:
+6
-1
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user