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": {
|
"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",
|
||||||
|
|||||||
@@ -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