2026-04-16-1

This commit is contained in:
Hamza-Ayed
2026-04-16 03:39:49 +03:00
parent 3d61362602
commit 58f06eeba3
15 changed files with 579 additions and 106 deletions
+25 -4
View File
@@ -18,6 +18,12 @@ const QUOTA_LIMITS: Record<TenantPlan, number> = {
[TenantPlan.ENTERPRISE]: 1000000,
};
const RATE_LIMITS: Record<TenantPlan, number> = {
[TenantPlan.FREE]: 10,
[TenantPlan.PRO]: 500,
[TenantPlan.ENTERPRISE]: 5000,
};
@Injectable()
export class UsageInterceptor implements NestInterceptor {
private readonly logger = new Logger(UsageInterceptor.name);
@@ -34,17 +40,32 @@ export class UsageInterceptor implements NestInterceptor {
const plan = tenant.plan || TenantPlan.FREE;
const limit = QUOTA_LIMITS[plan];
const { allowed, used } = await this.usageService.checkQuota(tenant.id, limit);
if (!allowed) {
// 1. Quota Enforcement
const { allowed: quotaAllowed, used: monthlyUsed } = await this.usageService.checkQuota(tenant.id, limit);
if (!quotaAllowed) {
throw new HttpException({
statusCode: HttpStatus.TOO_MANY_REQUESTS,
error: 'Quota Exceeded',
message: 'Monthly API usage quota exceeded',
used,
used: monthlyUsed,
limit,
upgrade_url: 'https://map-dashbord.intaleqapp.com/#billing'
}, HttpStatus.TOO_MANY_REQUESTS);
}
// 2. Rate Limit Enforcement
const rpmLimit = RATE_LIMITS[plan];
const { allowed: rateAllowed, used: currentRpm } = await this.usageService.checkRateLimit(tenant.id, rpmLimit);
if (!rateAllowed) {
throw new HttpException({
statusCode: HttpStatus.TOO_MANY_REQUESTS,
error: 'Rate Limit Exceeded',
message: `Request rate limit exceeded (${rpmLimit} req/min for ${plan} plan)`,
current_rate: currentRpm,
limit: rpmLimit,
retry_after: '60s'
}, HttpStatus.TOO_MANY_REQUESTS);
}
}
const startTime = Date.now();
+46 -2
View File
@@ -61,6 +61,35 @@ export class UsageService {
};
}
/**
* Check if a tenant has exceeded their per-minute rate limit
*/
async checkRateLimit(tenantId: string, limit: number): Promise<{ allowed: boolean; used: number }> {
const key = this.getRateLimitKey(tenantId);
try {
const client = this.redisService.getClient();
const usedRaw = await client.get(key);
const used = usedRaw ? parseInt(usedRaw, 10) : 0;
if (used >= limit) {
return { allowed: false, used };
}
// Increment and set expiry if new
const multi = client.multi();
multi.incr(key);
if (!usedRaw) {
multi.expire(key, 60); // 1 minute window
}
await multi.exec();
return { allowed: true, used: used + 1 };
} catch (err) {
this.logger.error(`Rate limit check failed for ${tenantId}: ${err.message}`);
return { allowed: true, used: 0 }; // Fail open for reliability
}
}
/**
* Get usage history for charts
*/
@@ -77,11 +106,19 @@ export class UsageService {
}
/**
* Get real-time summary for the dashboard
* Get real-time summary for the dashboard including limits
*/
async getUsageSummary(tenantId: string) {
async getUsageSummary(tenantId: string, plan: string = 'FREE') {
const monthlyUsage = await this.getMonthlyUsage(tenantId);
// Map of plans to limits (synced with interceptor)
const QUOTA_LIMITS = {
'FREE': 8000,
'PRO': 50000,
'ENTERPRISE': 1000000
};
const monthlyLimit = QUOTA_LIMITS[plan] || 8000;
// Get daily stats and performance metrics
const stats = await this.usageRepository
.createQueryBuilder('usage')
@@ -100,6 +137,7 @@ export class UsageService {
return {
monthlyUsage,
monthlyLimit,
totalToday,
avgLatency,
successRate,
@@ -112,4 +150,10 @@ export class UsageService {
const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
return `usage:${tenantId}:${yearMonth}`;
}
private getRateLimitKey(tenantId: string): string {
const now = new Date();
const window = `${now.getFullYear()}${now.getMonth()}${now.getDate()}${now.getHours()}${now.getMinutes()}`;
return `ratelimit:${tenantId}:${window}`;
}
}