Files
carmanagement/apps/api/src/middleware/rateLimiter.ts
T
root 2b422ca197
Build & Push / Pipeline Tests (push) Failing after 1m14s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 53s
Test / API Unit Tests (push) Failing after 50s
Test / Homepage Unit Tests (push) Successful in 45s
Test / Carplace Unit Tests (push) Successful in 44s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 43s
Test / API Integration Tests (push) Successful in 1m4s
fix search feature
2026-07-26 01:18:05 -04:00

145 lines
4.8 KiB
TypeScript

import rateLimit, { ipKeyGenerator } from 'express-rate-limit'
import type { Request } from 'express'
import { verifyAnyActorToken } from '../security/tokens'
import { getSessionCookieName } from '../security/sessionCookies'
const SESSION_COOKIE_NAMES = [
getSessionCookieName('admin'),
getSessionCookieName('employee'),
getSessionCookieName('renter'),
]
function readCookie(cookieHeader: string | undefined, name: string): string | null {
if (!cookieHeader) return null
for (const chunk of cookieHeader.split(';')) {
const [rawName, ...rawValue] = chunk.trim().split('=')
if (rawName === name) return decodeURIComponent(rawValue.join('='))
}
return null
}
function getBearerToken(req: Request): string | null {
const authHeader = req.headers.authorization
if (!authHeader?.startsWith('Bearer ')) return null
const token = authHeader.slice(7).trim()
return token || null
}
function getRequestToken(req: Request): string | null {
const cookieHeader = req.headers.cookie
for (const name of SESSION_COOKIE_NAMES) {
const token = readCookie(cookieHeader, name)
if (token) return token
}
return getBearerToken(req)
}
function getAuthenticatedActorKey(req: Request): string | null {
const token = getRequestToken(req)
if (!token) return null
try {
const payload = verifyAnyActorToken(token)
return `${payload.type}:${payload.sub}`
} catch {
return null
}
}
// req.ip is already the real client IP when app.set('trust proxy', 1) is configured
const getClientIpKey = (req: Request) => ipKeyGenerator(req.ip ?? '')
const skipPreflightRequest = (req: Request) => req.method === 'OPTIONS'
// Strict limiter for auth endpoints — prevents brute-force and credential stuffing.
// Successful requests (e.g. GET /me profile reads) are skipped so only failed
// attempts count toward the cap.
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20,
standardHeaders: 'draft-7',
legacyHeaders: false,
skip: skipPreflightRequest,
skipSuccessfulRequests: true,
keyGenerator: (req) => getClientIpKey(req),
message: { error: 'too_many_requests', message: 'Too many attempts, please try again later', statusCode: 429 },
})
// Standard limiter for general authenticated API endpoints
export const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 300,
standardHeaders: 'draft-7',
legacyHeaders: false,
skip: skipPreflightRequest,
keyGenerator: (req) => {
const ip = getClientIpKey(req)
const actorKey = getAuthenticatedActorKey(req)
const companyId = (req as any).companyId ?? ''
const renterId = (req as any).renterId ?? ''
return `${ip}:${actorKey || companyId || renterId || 'anonymous'}`
},
message: { error: 'too_many_requests', message: 'Rate limit exceeded', statusCode: 429 },
})
// Limiter for public carplace and site endpoints (no auth)
export const publicLimiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
standardHeaders: 'draft-7',
legacyHeaders: false,
skip: skipPreflightRequest,
keyGenerator: (req) => getClientIpKey(req),
message: { error: 'too_many_requests', message: 'Rate limit exceeded', statusCode: 429 },
})
// Dedicated limiter for public payment/subscription webhooks. Provider retries
// still fit under this limit, but spray-and-pray signature attempts do not.
export const webhookLimiter = rateLimit({
windowMs: 60 * 1000,
max: 30,
standardHeaders: 'draft-7',
legacyHeaders: false,
skip: skipPreflightRequest,
keyGenerator: (req) => getClientIpKey(req),
message: { error: 'too_many_requests', message: 'Webhook rate limit exceeded', statusCode: 429 },
})
// Tight limiter for admin endpoints
export const adminLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: 'draft-7',
legacyHeaders: false,
skip: skipPreflightRequest,
keyGenerator: (req) => {
const ip = getClientIpKey(req)
return `${ip}:${getAuthenticatedActorKey(req) || 'anonymous'}`
},
message: { error: 'too_many_requests', message: 'Too many admin requests', statusCode: 429 },
})
// Applied after authentication so limits can include actor identity rather than
// pretending every employee behind the same NAT is the same organism.
export const actorLimiter = rateLimit({
windowMs: 60 * 1000,
max: 240,
standardHeaders: 'draft-7',
legacyHeaders: false,
skip: skipPreflightRequest,
keyGenerator: (req) => {
const ip = getClientIpKey(req)
const actorKey = getAuthenticatedActorKey(req)
const companyId = (req as any).companyId ?? ''
const employeeId = (req as any).employee?.id ?? ''
const renterId = (req as any).renterId ?? ''
const adminId = (req as any).admin?.id ?? ''
return `${ip}:${companyId}:${actorKey || employeeId || renterId || adminId || 'anonymous'}`
},
message: { error: 'too_many_requests', message: 'Authenticated rate limit exceeded', statusCode: 429 },
})