127 lines
4.2 KiB
TypeScript
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 },
|
|
})
|