2026-04-16-1
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user