svelte-guardian
Version:
Batteries included authentication for SvelteKit applications.
111 lines (110 loc) • 4.23 kB
JavaScript
import Redis from 'redis';
export class RedisRateLimiter {
redis;
constructor(config) {
// Prepare Redis connection options
const connectionOptions = {
url: config.url,
socket: config.socket ? {
host: config.socket.host,
port: config.socket.port,
tls: config.tls
} : undefined,
username: config.username,
password: config.password,
database: config.database
};
// Add any additional connection options
if (config.connectionOptions) {
Object.assign(connectionOptions, {
connectTimeout: config.connectionOptions.connectTimeout,
disconnectTimeout: config.connectionOptions.disconnectTimeout,
pingInterval: config.connectionOptions.pingInterval,
maxRetriesPerRequest: config.connectionOptions.maxRetriesPerRequest,
retryStrategy: config.connectionOptions.retryStrategy
});
}
// Create Redis client
this.redis = Redis.createClient(connectionOptions);
// Connect to Redis
this.redis.connect().catch(console.error);
}
// Optional: Factory method for backward compatibility
static fromUrl(url, token) {
return new RedisRateLimiter({
url,
password: token
});
}
async limit(event, config) {
const now = Date.now();
// Use custom key generator if provided, otherwise use default
const key = config.keyGenerator
? config.keyGenerator(event)
: this.defaultKeyGenerator(event);
try {
// Check for existing block
const blockKey = `block:${key}`;
const isBlocked = await this.redis.exists(blockKey);
if (isBlocked) {
const blockExpiration = await this.redis.ttl(blockKey);
return {
allowed: false,
remaining: 0,
resetTime: now + blockExpiration * 1000,
blockedUntil: now + blockExpiration * 1000
};
}
// Increment request count
const current = await this.redis.incr(key);
// Set expiration if this is the first request
if (current === 1) {
await this.redis.expire(key, Math.ceil((config.windowMs || 60000) / 1000));
}
// Default block duration to 5 minutes
const blockDuration = config.blockDuration || 5 * 60 * 1000;
// Check if limit is exceeded
if (current > (config.maxRequests || 10)) {
// Create block key
await this.redis.set(blockKey, 'blocked', {
PX: blockDuration // Redis: PX means milliseconds expiration
});
return {
allowed: false,
remaining: 0,
resetTime: now + config.windowMs,
blockedUntil: now + blockDuration
};
}
return {
allowed: true,
remaining: (config.maxRequests || 10) - current,
resetTime: now + (config.windowMs || 60000)
};
}
catch (error) {
console.error('Redis rate limiting error:', error);
// Fail open if Redis is unavailable
//TODO Consider falling back to in memory rate limiter
return {
allowed: true,
remaining: config.maxRequests || 10,
resetTime: now + (config.windowMs || 60000)
};
}
}
defaultKeyGenerator(event) {
// Prioritize user ID if available
const session = event.locals.auth?.() || event.locals.session;
if (session?.user?.id) {
return `rate_limit:user:${session.user.id}`;
}
// Fallback to IP address
const ip = event.getClientAddress();
return `rate_limit:ip:${ip}`;
}
// Cleanup method to close Redis connection
async close() {
await this.redis.quit();
}
}