mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 23:46:42 +02:00
Implement custom NestJS API Gateway for mana-search, mana-stt, and mana-tts: - API Key management with CRUD operations and key regeneration - Redis-based sliding window rate limiting - Credit-based billing with tier support (free, pro, enterprise) - Usage tracking with daily aggregates - Proxy services to backend microservices - Prometheus metrics endpoint - JWT auth for management API, API key auth for public API Database schema uses separate `api_gateway` schema in shared manacore DB.
70 lines
2 KiB
TypeScript
70 lines
2 KiB
TypeScript
import {
|
|
Injectable,
|
|
CanActivate,
|
|
ExecutionContext,
|
|
HttpException,
|
|
HttpStatus,
|
|
Inject,
|
|
} from '@nestjs/common';
|
|
import Redis from 'ioredis';
|
|
import { ApiKeyData } from '../api-keys/api-keys.service';
|
|
|
|
export const REDIS_CLIENT = 'REDIS_CLIENT';
|
|
|
|
@Injectable()
|
|
export class RateLimitGuard implements CanActivate {
|
|
constructor(@Inject(REDIS_CLIENT) private readonly redis: Redis) {}
|
|
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
const request = context.switchToHttp().getRequest();
|
|
const apiKey = request.apiKey as ApiKeyData;
|
|
|
|
if (!apiKey) {
|
|
return true; // Let ApiKeyGuard handle missing key
|
|
}
|
|
|
|
const key = `ratelimit:${apiKey.id}`;
|
|
const limit = apiKey.rateLimit;
|
|
const window = 60; // 60 seconds
|
|
|
|
// Sliding window rate limiting using sorted set
|
|
const now = Date.now();
|
|
const windowStart = now - window * 1000;
|
|
|
|
// Remove old entries
|
|
await this.redis.zremrangebyscore(key, 0, windowStart);
|
|
|
|
// Count current requests
|
|
const count = await this.redis.zcard(key);
|
|
|
|
if (count >= limit) {
|
|
// Get the oldest entry to calculate retry-after
|
|
const oldestEntries = await this.redis.zrange(key, 0, 0, 'WITHSCORES');
|
|
const oldestTimestamp = oldestEntries.length > 1 ? parseInt(oldestEntries[1], 10) : now;
|
|
const retryAfter = Math.ceil((oldestTimestamp + window * 1000 - now) / 1000);
|
|
|
|
throw new HttpException(
|
|
{
|
|
statusCode: HttpStatus.TOO_MANY_REQUESTS,
|
|
message: 'Rate limit exceeded',
|
|
retryAfter,
|
|
limit,
|
|
remaining: 0,
|
|
},
|
|
HttpStatus.TOO_MANY_REQUESTS
|
|
);
|
|
}
|
|
|
|
// Add current request
|
|
await this.redis.zadd(key, now, `${now}`);
|
|
await this.redis.expire(key, window);
|
|
|
|
// Add rate limit headers to response
|
|
const response = context.switchToHttp().getResponse();
|
|
response.setHeader('X-RateLimit-Limit', limit);
|
|
response.setHeader('X-RateLimit-Remaining', Math.max(0, limit - count - 1));
|
|
response.setHeader('X-RateLimit-Reset', Math.ceil(now / 1000) + window);
|
|
|
|
return true;
|
|
}
|
|
}
|