Files
url_tracker_tool/apps/api/dist/services/rate-limit.service.js
Andrei 58f8093689 Rebrand from 'Redirect Intelligence v2' to 'URL Tracker Tool V2' throughout UI
- Updated all component headers and documentation
- Changed navbar and footer branding
- Updated homepage hero badge
- Modified page title in index.html
- Simplified footer text to 'Built with ❤️'
- Consistent V2 capitalization across all references
2025-08-19 19:12:23 +00:00

293 lines
10 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.rateLimitService = exports.BurstLimitError = exports.RateLimitError = exports.RateLimitService = exports.ANONYMOUS_TIER = exports.RATE_LIMIT_TIERS = void 0;
const rate_limiter_flexible_1 = require("rate-limiter-flexible");
const logger_1 = require("../lib/logger");
exports.RATE_LIMIT_TIERS = {
free: {
name: 'Free',
requestsPerHour: 100,
requestsPerMinute: 10,
bulkJobsPerDay: 2,
maxUrls: 50,
exportLimit: 5,
},
pro: {
name: 'Pro',
requestsPerHour: 1000,
requestsPerMinute: 50,
bulkJobsPerDay: 20,
maxUrls: 1000,
exportLimit: 100,
},
enterprise: {
name: 'Enterprise',
requestsPerHour: 10000,
requestsPerMinute: 200,
bulkJobsPerDay: 100,
maxUrls: 10000,
exportLimit: 1000,
},
};
exports.ANONYMOUS_TIER = {
name: 'Anonymous',
requestsPerHour: 50,
requestsPerMinute: 5,
bulkJobsPerDay: 0,
maxUrls: 10,
exportLimit: 0,
};
class RateLimitService {
rateLimiters;
constructor() {
this.rateLimiters = new Map();
this.initializeRateLimiters();
}
initializeRateLimiters() {
this.rateLimiters.set('legacy', new rate_limiter_flexible_1.RateLimiterMemory({
keyPrefix: 'rl_legacy',
points: 100,
duration: 3600,
blockDuration: 3600,
execEvenly: true,
}));
this.rateLimiters.set('anonymous', new rate_limiter_flexible_1.RateLimiterMemory({
keyPrefix: 'rl_anon',
points: exports.ANONYMOUS_TIER.requestsPerHour,
duration: 3600,
blockDuration: 3600,
execEvenly: true,
}));
Object.keys(exports.RATE_LIMIT_TIERS).forEach(tier => {
const config = exports.RATE_LIMIT_TIERS[tier];
this.rateLimiters.set(`user_${tier}_hour`, new rate_limiter_flexible_1.RateLimiterMemory({
keyPrefix: `rl_user_${tier}_h`,
points: config.requestsPerHour,
duration: 3600,
blockDuration: 900,
execEvenly: true,
}));
this.rateLimiters.set(`user_${tier}_minute`, new rate_limiter_flexible_1.RateLimiterMemory({
keyPrefix: `rl_user_${tier}_m`,
points: config.requestsPerMinute,
duration: 60,
blockDuration: 60,
execEvenly: true,
}));
this.rateLimiters.set(`bulk_${tier}_day`, new rate_limiter_flexible_1.RateLimiterMemory({
keyPrefix: `rl_bulk_${tier}_d`,
points: config.bulkJobsPerDay,
duration: 86400,
blockDuration: 86400,
execEvenly: false,
}));
this.rateLimiters.set(`export_${tier}_day`, new rate_limiter_flexible_1.RateLimiterMemory({
keyPrefix: `rl_export_${tier}_d`,
points: config.exportLimit,
duration: 86400,
blockDuration: 86400,
execEvenly: false,
}));
});
}
async getUserTier(userId) {
if (!userId || userId === 'anonymous-user') {
return exports.ANONYMOUS_TIER;
}
return exports.RATE_LIMIT_TIERS.free;
}
async checkRateLimit(type, key, userId) {
try {
const tier = await this.getUserTier(userId);
let limiterKey;
let limit;
if (type === 'legacy') {
limiterKey = 'legacy';
limit = 100;
}
else if (!userId || userId === 'anonymous-user') {
limiterKey = 'anonymous';
limit = exports.ANONYMOUS_TIER.requestsPerHour;
}
else {
const tierName = tier.name.toLowerCase();
switch (type) {
case 'tracking':
limiterKey = `user_${tierName}_hour`;
limit = tier.requestsPerHour;
break;
case 'bulk':
limiterKey = `bulk_${tierName}_day`;
limit = tier.bulkJobsPerDay;
break;
case 'export':
limiterKey = `export_${tierName}_day`;
limit = tier.exportLimit;
break;
default:
throw new Error(`Unknown rate limit type: ${type}`);
}
}
const rateLimiter = this.rateLimiters.get(limiterKey);
if (!rateLimiter) {
throw new Error(`Rate limiter not found: ${limiterKey}`);
}
const result = await rateLimiter.consume(key, 1);
return {
limit,
remaining: result.remainingPoints || 0,
reset: new Date(Date.now() + (result.msBeforeNext || 0)),
tier: tier.name,
};
}
catch (error) {
if (error instanceof Error && error.message.includes('Rate limit')) {
const tier = await this.getUserTier(userId);
throw new RateLimitError(tier.name, 0, new Date(Date.now() + 3600000));
}
logger_1.logger.error('Rate limit check failed:', error);
throw error;
}
}
async checkBurstLimit(userId) {
const tier = await this.getUserTier(userId);
if (tier === exports.ANONYMOUS_TIER || userId === 'anonymous-user')
return;
const tierName = tier.name.toLowerCase();
const limiterKey = `user_${tierName}_minute`;
const rateLimiter = this.rateLimiters.get(limiterKey);
if (!rateLimiter) {
logger_1.logger.warn(`Burst rate limiter not found: ${limiterKey}`);
return;
}
try {
await rateLimiter.consume(userId, 1);
}
catch (error) {
throw new BurstLimitError(tier.name, tier.requestsPerMinute);
}
}
async getRateLimitStatus(type, key, userId) {
const tier = await this.getUserTier(userId);
let limiterKey;
let limit;
if (type === 'legacy') {
limiterKey = 'legacy';
limit = 100;
}
else if (!userId || userId === 'anonymous-user') {
limiterKey = 'anonymous';
limit = exports.ANONYMOUS_TIER.requestsPerHour;
}
else {
const tierName = tier.name.toLowerCase();
switch (type) {
case 'tracking':
limiterKey = `user_${tierName}_hour`;
limit = tier.requestsPerHour;
break;
case 'bulk':
limiterKey = `bulk_${tierName}_day`;
limit = tier.bulkJobsPerDay;
break;
case 'export':
limiterKey = `export_${tierName}_day`;
limit = tier.exportLimit;
break;
default:
throw new Error(`Unknown rate limit type: ${type}`);
}
}
const rateLimiter = this.rateLimiters.get(limiterKey);
if (!rateLimiter) {
throw new Error(`Rate limiter not found: ${limiterKey}`);
}
try {
const result = await rateLimiter.get(key);
return {
limit,
remaining: result ? result.remainingPoints || 0 : limit,
reset: result ? new Date(Date.now() + (result.msBeforeNext || 0)) : new Date(),
tier: tier.name,
};
}
catch (error) {
logger_1.logger.error('Failed to get rate limit status:', error);
return {
limit,
remaining: limit,
reset: new Date(),
tier: tier.name,
};
}
}
async resetRateLimit(key, type) {
try {
if (type) {
const rateLimiter = this.rateLimiters.get(type);
if (rateLimiter) {
await rateLimiter.delete(key);
}
}
else {
for (const rateLimiter of this.rateLimiters.values()) {
await rateLimiter.delete(key).catch(() => { });
}
}
logger_1.logger.info(`Rate limit reset for key: ${key}`, { type });
}
catch (error) {
logger_1.logger.error('Failed to reset rate limit:', error);
throw error;
}
}
async getStatistics() {
try {
return {
totalRequests: 0,
activeKeys: this.rateLimiters.size,
tierDistribution: {
anonymous: 1,
free: Object.keys(exports.RATE_LIMIT_TIERS).length,
pro: Object.keys(exports.RATE_LIMIT_TIERS).length,
enterprise: Object.keys(exports.RATE_LIMIT_TIERS).length,
},
};
}
catch (error) {
logger_1.logger.error('Failed to get rate limit statistics:', error);
return {
totalRequests: 0,
activeKeys: 0,
tierDistribution: { anonymous: 0, free: 0, pro: 0, enterprise: 0 },
};
}
}
}
exports.RateLimitService = RateLimitService;
class RateLimitError extends Error {
tier;
remaining;
reset;
constructor(tier, remaining, reset) {
super(`Rate limit exceeded for ${tier} tier`);
this.tier = tier;
this.remaining = remaining;
this.reset = reset;
this.name = 'RateLimitError';
}
}
exports.RateLimitError = RateLimitError;
class BurstLimitError extends Error {
tier;
limit;
constructor(tier, limit) {
super(`Burst limit exceeded for ${tier} tier (${limit} requests per minute)`);
this.tier = tier;
this.limit = limit;
this.name = 'BurstLimitError';
}
}
exports.BurstLimitError = BurstLimitError;
exports.rateLimitService = new RateLimitService();
//# sourceMappingURL=rate-limit.service.js.map