You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
89 lines
2.1 KiB
89 lines
2.1 KiB
// server/middleware/rate-limit.ts
|
|
|
|
/**
|
|
* Rate limiting middleware for auth endpoints
|
|
*
|
|
* Prevents brute force attacks on login/registration
|
|
*
|
|
* Limits:
|
|
* - /api/auth/login: 5 attempts per 15 minutes per IP
|
|
* - /api/auth/register: 3 attempts per hour per IP
|
|
*/
|
|
|
|
interface RateLimitEntry {
|
|
count: number
|
|
resetAt: number
|
|
}
|
|
|
|
// In-memory rate limit store (use Redis in production!)
|
|
const rateLimitStore = new Map<string, RateLimitEntry>()
|
|
|
|
// Clean up expired entries every 5 minutes
|
|
setInterval(
|
|
() => {
|
|
const now = Date.now()
|
|
for (const [key, entry] of rateLimitStore.entries()) {
|
|
if (entry.resetAt < now) {
|
|
rateLimitStore.delete(key)
|
|
}
|
|
}
|
|
},
|
|
5 * 60 * 1000
|
|
)
|
|
|
|
export default defineEventHandler((event) => {
|
|
const path = event.path
|
|
|
|
// Only apply to auth endpoints
|
|
if (!path.startsWith('/api/auth/')) {
|
|
return
|
|
}
|
|
|
|
// Get client IP
|
|
const ip = getRequestIP(event, { xForwardedFor: true }) || 'unknown'
|
|
|
|
// Define rate limits per endpoint
|
|
const limits: Record<string, { maxAttempts: number; windowMs: number }> = {
|
|
'/api/auth/login': { maxAttempts: 10, windowMs: 10 * 60 * 1000 }, // 10 per 10min
|
|
'/api/auth/register': { maxAttempts: 3, windowMs: 60 * 60 * 1000 }, // 3 per hour
|
|
}
|
|
|
|
const limit = limits[path]
|
|
if (!limit) {
|
|
return // No rate limit for this endpoint
|
|
}
|
|
|
|
// Check rate limit
|
|
const key = `${ip}:${path}`
|
|
const now = Date.now()
|
|
const entry = rateLimitStore.get(key)
|
|
|
|
if (!entry || entry.resetAt < now) {
|
|
// First attempt or window expired - reset counter
|
|
rateLimitStore.set(key, {
|
|
count: 1,
|
|
resetAt: now + limit.windowMs,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Increment counter
|
|
entry.count++
|
|
|
|
if (entry.count > limit.maxAttempts) {
|
|
// Rate limit exceeded
|
|
const retryAfter = Math.ceil((entry.resetAt - now) / 1000)
|
|
|
|
setResponseStatus(event, 429)
|
|
setResponseHeader(event, 'Retry-After', retryAfter.toString())
|
|
|
|
throw createError({
|
|
statusCode: 429,
|
|
statusMessage: 'Too many requests',
|
|
data: {
|
|
retryAfter,
|
|
message: `Too many attempts. Please try again in ${retryAfter} seconds.`,
|
|
},
|
|
})
|
|
}
|
|
})
|
|
|