Files
carmanagement/apps/api/src/middleware/rateLimiter.ts
T
2026-06-10 00:40:19 -04:00

127 lines
4.2 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 ?? '')
// 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,
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: 120,
standardHeaders: 'draft-7',
legacyHeaders: false,
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 marketplace and site endpoints (no auth)
export const publicLimiter = rateLimit({
windowMs: 60 * 1000,
max: 60,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: (req) => getClientIpKey(req),
message: { error: 'too_many_requests', message: '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,
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,
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 },
})