UNPKG

express-rate-limiter-ts

Version:

A powerful, flexible and modern rate limiter middleware for Express.js written in TypeScript.

121 lines (120 loc) 4.75 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PostgresStore = void 0; const pg_1 = require("pg"); // PostgresStore: Fully working, async, counter and ban supported store for production. class PostgresStore { constructor(options) { this.windowMs = options.windowMs; this.client = new pg_1.Client({ connectionString: options.connectionString }); this.connect(); } // Start connection async connect() { await this.client.connect(); // Create required tables (idempotent) await this.client.query(` CREATE TABLE IF NOT EXISTS rate_limiter_counters ( key TEXT PRIMARY KEY, count INTEGER NOT NULL, expires_at BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS rate_limiter_bans ( key TEXT PRIMARY KEY, reason TEXT, expires_at BIGINT NOT NULL ); CREATE TABLE IF NOT EXISTS rate_limiter_stats ( route TEXT PRIMARY KEY, count INTEGER NOT NULL, last_triggered BIGINT NOT NULL ); `); } // Increment counter and check limit async incr(key) { await this.connect(); const now = Date.now(); const expiresAt = now + this.windowMs; // UPSERT to increment counter const res = await this.client.query(`INSERT INTO rate_limiter_counters (key, count, expires_at) VALUES ($1, 1, $2) ON CONFLICT (key) DO UPDATE SET count = rate_limiter_counters.count + 1, expires_at = $2 RETURNING count, expires_at`, [key, expiresAt]); return { current: res.rows[0].count, remaining: 0, // Remaining limit will be calculated in middleware resetTime: new Date(Number(res.rows[0].expires_at)), exceeded: false, }; } // Read counter async get(key) { await this.connect(); const res = await this.client.query('SELECT count, expires_at FROM rate_limiter_counters WHERE key = $1', [key]); if (res.rows.length === 0) return { count: 0, resetTime: Date.now() + this.windowMs }; return { count: res.rows[0].count, resetTime: Number(res.rows[0].expires_at) }; } // Reset counter async reset(key) { await this.connect(); await this.client.query('DELETE FROM rate_limiter_counters WHERE key = $1', [key]); } // Record statistic async recordStat(route) { await this.connect(); const now = Date.now(); await this.client.query(`INSERT INTO rate_limiter_stats (route, count, last_triggered) VALUES ($1, 1, $2) ON CONFLICT (route) DO UPDATE SET count = rate_limiter_stats.count + 1, last_triggered = $2`, [route, now]); } // Get statistics async getStats() { await this.connect(); const res = await this.client.query('SELECT route, count, last_triggered FROM rate_limiter_stats'); const stats = {}; for (const row of res.rows) { stats[row.route] = { count: row.count, lastTriggered: new Date(Number(row.last_triggered)), }; } return stats; } // Apply ban async ban(key, seconds, reason) { await this.connect(); const expiresAt = Date.now() + seconds * 1000; await this.client.query(`INSERT INTO rate_limiter_bans (key, reason, expires_at) VALUES ($1, $2, $3) ON CONFLICT (key) DO UPDATE SET reason = $2, expires_at = $3`, [key, reason, expiresAt]); } // Check ban async isBanned(key) { await this.connect(); const res = await this.client.query('SELECT reason, expires_at FROM rate_limiter_bans WHERE key = $1', [key]); if (res.rows.length && res.rows[0].expires_at > Date.now()) { return { banned: true, banExpiresAt: new Date(Number(res.rows[0].expires_at)), reason: res.rows[0].reason }; } return { banned: false }; } // Get banned keys async getBanned() { await this.connect(); const now = Date.now(); const res = await this.client.query('SELECT key, reason, expires_at FROM rate_limiter_bans WHERE expires_at > $1', [now]); const result = {}; for (const row of res.rows) { result[row.key] = { banExpiresAt: new Date(Number(row.expires_at)), reason: row.reason }; } return result; } // Remove ban async unban(key) { await this.connect(); await this.client.query('DELETE FROM rate_limiter_bans WHERE key = $1', [key]); await this.client.query('DELETE FROM rate_limiter_counters WHERE key = $1', [key]); } } exports.PostgresStore = PostgresStore;