mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 17:26:41 +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.
63 lines
1.7 KiB
TypeScript
63 lines
1.7 KiB
TypeScript
import {
|
|
Injectable,
|
|
CanActivate,
|
|
ExecutionContext,
|
|
HttpException,
|
|
HttpStatus,
|
|
} from '@nestjs/common';
|
|
import { ApiKeysService, ApiKeyData } from '../api-keys/api-keys.service';
|
|
import { CREDIT_COSTS } from '../config/pricing';
|
|
|
|
@Injectable()
|
|
export class CreditsGuard implements CanActivate {
|
|
constructor(private readonly apiKeyService: ApiKeysService) {}
|
|
|
|
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 endpoint = this.extractEndpoint(request.path);
|
|
const estimatedCredits = this.estimateCredits(endpoint, request);
|
|
|
|
const hasCredits = await this.apiKeyService.hasEnoughCredits(apiKey.id, estimatedCredits);
|
|
|
|
if (!hasCredits) {
|
|
throw new HttpException(
|
|
{
|
|
statusCode: HttpStatus.PAYMENT_REQUIRED,
|
|
message: 'Insufficient credits. Please upgrade your plan or wait for monthly reset.',
|
|
creditsRequired: estimatedCredits,
|
|
creditsUsed: apiKey.creditsUsed,
|
|
monthlyLimit: apiKey.monthlyCredits,
|
|
},
|
|
HttpStatus.PAYMENT_REQUIRED
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private extractEndpoint(path: string): string {
|
|
const match = path.match(/\/v1\/(\w+)/);
|
|
return match ? match[1] : 'unknown';
|
|
}
|
|
|
|
private estimateCredits(endpoint: string, request: any): number {
|
|
switch (endpoint) {
|
|
case 'search':
|
|
return CREDIT_COSTS.search;
|
|
case 'tts':
|
|
const text = request.body?.text || '';
|
|
return Math.max(1, Math.ceil(text.length / 1000) * CREDIT_COSTS.tts.per1000Chars);
|
|
case 'stt':
|
|
// Estimate based on file size or default to 1 minute
|
|
return CREDIT_COSTS.stt.perMinute;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
}
|