managarten/services/mana-api-gateway/src/guards/credits.guard.ts
Till-JS 6f1b2654f1 feat: add mana-api-gateway for monetizing core services
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.
2026-01-29 17:30:21 +01:00

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;
}
}
}