feat: implement billing, usage tracking, and administrative geocoding features in API with infrastructure updates
This commit is contained in:
+17
@@ -0,0 +1,17 @@
|
||||
import { UsageService } from './usage.service';
|
||||
export declare class UsageController {
|
||||
private usageService;
|
||||
constructor(usageService: UsageService);
|
||||
getSummary(req: any): Promise<{
|
||||
limit: any;
|
||||
percentage: string;
|
||||
plan: any;
|
||||
monthlyUsage: number;
|
||||
monthlyLimit: any;
|
||||
totalToday: number;
|
||||
avgLatency: number;
|
||||
successRate: number;
|
||||
lastUpdated: Date;
|
||||
}>;
|
||||
getHistory(req: any, days?: number): Promise<any>;
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||
};
|
||||
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
||||
return function (target, key) { decorator(target, key, paramIndex); }
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.UsageController = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const usage_service_1 = require("./usage.service");
|
||||
const firebase_auth_guard_1 = require("../auth/guards/firebase-auth.guard");
|
||||
const swagger_1 = require("@nestjs/swagger");
|
||||
let UsageController = class UsageController {
|
||||
usageService;
|
||||
constructor(usageService) {
|
||||
this.usageService = usageService;
|
||||
}
|
||||
async getSummary(req) {
|
||||
const tenant = req.tenant;
|
||||
const summary = await this.usageService.getUsageSummary(tenant.id);
|
||||
const limits = {
|
||||
FREE: 5000,
|
||||
STARTER: 25000,
|
||||
PRO: 100000,
|
||||
ENTERPRISE: 500000,
|
||||
};
|
||||
const limit = limits[tenant.plan] || 5000;
|
||||
return {
|
||||
...summary,
|
||||
limit,
|
||||
percentage: Math.min(((summary.monthlyUsage / limit) * 100), 100).toFixed(1),
|
||||
plan: tenant.plan,
|
||||
};
|
||||
}
|
||||
async getHistory(req, days = 30) {
|
||||
const tenantId = req.tenant.id;
|
||||
return this.usageService.getUsageHistory(tenantId, days);
|
||||
}
|
||||
};
|
||||
exports.UsageController = UsageController;
|
||||
__decorate([
|
||||
(0, common_1.Get)('summary'),
|
||||
(0, swagger_1.ApiOperation)({ summary: 'Get current month usage summary' }),
|
||||
__param(0, (0, common_1.Req)()),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [Object]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], UsageController.prototype, "getSummary", null);
|
||||
__decorate([
|
||||
(0, common_1.Get)('history'),
|
||||
(0, swagger_1.ApiOperation)({ summary: 'Get daily request volume history' }),
|
||||
__param(0, (0, common_1.Req)()),
|
||||
__param(1, (0, common_1.Query)('days')),
|
||||
__metadata("design:type", Function),
|
||||
__metadata("design:paramtypes", [Object, Number]),
|
||||
__metadata("design:returntype", Promise)
|
||||
], UsageController.prototype, "getHistory", null);
|
||||
exports.UsageController = UsageController = __decorate([
|
||||
(0, swagger_1.ApiTags)('usage'),
|
||||
(0, swagger_1.ApiBearerAuth)(),
|
||||
(0, common_1.UseGuards)(firebase_auth_guard_1.FirebaseAuthGuard),
|
||||
(0, common_1.Controller)('usage'),
|
||||
__metadata("design:paramtypes", [usage_service_1.UsageService])
|
||||
], UsageController);
|
||||
//# sourceMappingURL=usage.controller.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"usage.controller.js","sourceRoot":"","sources":["../../src/usage/usage.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAwE;AACxE,mDAA+C;AAC/C,4EAAuE;AACvE,6CAAuE;AAMhE,IAAM,eAAe,GAArB,MAAM,eAAe;IACN;IAApB,YAAoB,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAAG,CAAC;IAI5C,AAAN,KAAK,CAAC,UAAU,CAAQ,GAAQ;QAC9B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QAC1B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAGnE,MAAM,MAAM,GAAG;YACb,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,KAAK;YACd,GAAG,EAAE,MAAM;YACX,UAAU,EAAE,MAAM;SACnB,CAAC;QAEF,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;QAE1C,OAAO;YACL,GAAG,OAAO;YACV,KAAK;YACL,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;YAC5E,IAAI,EAAE,MAAM,CAAC,IAAI;SAClB,CAAC;IACJ,CAAC;IAIK,AAAN,KAAK,CAAC,UAAU,CAAQ,GAAQ,EAAiB,OAAe,EAAE;QAChE,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC3D,CAAC;CACF,CAAA;AAjCY,0CAAe;AAKpB;IAFL,IAAA,YAAG,EAAC,SAAS,CAAC;IACd,IAAA,sBAAY,EAAC,EAAE,OAAO,EAAE,iCAAiC,EAAE,CAAC;IAC3C,WAAA,IAAA,YAAG,GAAE,CAAA;;;;iDAoBtB;AAIK;IAFL,IAAA,YAAG,EAAC,SAAS,CAAC;IACd,IAAA,sBAAY,EAAC,EAAE,OAAO,EAAE,kCAAkC,EAAE,CAAC;IAC5C,WAAA,IAAA,YAAG,GAAE,CAAA;IAAY,WAAA,IAAA,cAAK,EAAC,MAAM,CAAC,CAAA;;;;iDAG/C;0BAhCU,eAAe;IAJ3B,IAAA,iBAAO,EAAC,OAAO,CAAC;IAChB,IAAA,uBAAa,GAAE;IACf,IAAA,kBAAS,EAAC,uCAAiB,CAAC;IAC5B,IAAA,mBAAU,EAAC,OAAO,CAAC;qCAEgB,4BAAY;GADnC,eAAe,CAiC3B"}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
export declare class UsageLog {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
apiKeyId: string;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
userAgent: string;
|
||||
ipAddress: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.UsageLog = void 0;
|
||||
const typeorm_1 = require("typeorm");
|
||||
let UsageLog = class UsageLog {
|
||||
id;
|
||||
tenantId;
|
||||
apiKeyId;
|
||||
endpoint;
|
||||
method;
|
||||
statusCode;
|
||||
responseTimeMs;
|
||||
userAgent;
|
||||
ipAddress;
|
||||
createdAt;
|
||||
};
|
||||
exports.UsageLog = UsageLog;
|
||||
__decorate([
|
||||
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
||||
__metadata("design:type", String)
|
||||
], UsageLog.prototype, "id", void 0);
|
||||
__decorate([
|
||||
(0, typeorm_1.Index)(),
|
||||
(0, typeorm_1.Column)(),
|
||||
__metadata("design:type", String)
|
||||
], UsageLog.prototype, "tenantId", void 0);
|
||||
__decorate([
|
||||
(0, typeorm_1.Index)(),
|
||||
(0, typeorm_1.Column)(),
|
||||
__metadata("design:type", String)
|
||||
], UsageLog.prototype, "apiKeyId", void 0);
|
||||
__decorate([
|
||||
(0, typeorm_1.Column)(),
|
||||
__metadata("design:type", String)
|
||||
], UsageLog.prototype, "endpoint", void 0);
|
||||
__decorate([
|
||||
(0, typeorm_1.Column)(),
|
||||
__metadata("design:type", String)
|
||||
], UsageLog.prototype, "method", void 0);
|
||||
__decorate([
|
||||
(0, typeorm_1.Column)({ type: 'int', default: 200 }),
|
||||
__metadata("design:type", Number)
|
||||
], UsageLog.prototype, "statusCode", void 0);
|
||||
__decorate([
|
||||
(0, typeorm_1.Column)({ type: 'int', default: 0 }),
|
||||
__metadata("design:type", Number)
|
||||
], UsageLog.prototype, "responseTimeMs", void 0);
|
||||
__decorate([
|
||||
(0, typeorm_1.Column)({ nullable: true }),
|
||||
__metadata("design:type", String)
|
||||
], UsageLog.prototype, "userAgent", void 0);
|
||||
__decorate([
|
||||
(0, typeorm_1.Column)({ nullable: true }),
|
||||
__metadata("design:type", String)
|
||||
], UsageLog.prototype, "ipAddress", void 0);
|
||||
__decorate([
|
||||
(0, typeorm_1.Index)(),
|
||||
(0, typeorm_1.CreateDateColumn)(),
|
||||
__metadata("design:type", Date)
|
||||
], UsageLog.prototype, "createdAt", void 0);
|
||||
exports.UsageLog = UsageLog = __decorate([
|
||||
(0, typeorm_1.Entity)('usage_logs')
|
||||
], UsageLog);
|
||||
//# sourceMappingURL=usage.entity.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"usage.entity.js","sourceRoot":"","sources":["../../src/usage/usage.entity.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qCAMiB;AAGV,IAAM,QAAQ,GAAd,MAAM,QAAQ;IAEnB,EAAE,CAAS;IAIX,QAAQ,CAAS;IAIjB,QAAQ,CAAS;IAGjB,QAAQ,CAAS;IAGjB,MAAM,CAAS;IAGf,UAAU,CAAS;IAGnB,cAAc,CAAS;IAGvB,SAAS,CAAS;IAGlB,SAAS,CAAS;IAIlB,SAAS,CAAO;CACjB,CAAA;AAjCY,4BAAQ;AAEnB;IADC,IAAA,gCAAsB,EAAC,MAAM,CAAC;;oCACpB;AAIX;IAFC,IAAA,eAAK,GAAE;IACP,IAAA,gBAAM,GAAE;;0CACQ;AAIjB;IAFC,IAAA,eAAK,GAAE;IACP,IAAA,gBAAM,GAAE;;0CACQ;AAGjB;IADC,IAAA,gBAAM,GAAE;;0CACQ;AAGjB;IADC,IAAA,gBAAM,GAAE;;wCACM;AAGf;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;;4CACnB;AAGnB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;;gDACb;AAGvB;IADC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;2CACT;AAGlB;IADC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;2CACT;AAIlB;IAFC,IAAA,eAAK,GAAE;IACP,IAAA,0BAAgB,GAAE;8BACR,IAAI;2CAAC;mBAhCL,QAAQ;IADpB,IAAA,gBAAM,EAAC,YAAY,CAAC;GACR,QAAQ,CAiCpB"}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { UsageService } from './usage.service';
|
||||
export declare class UsageInterceptor implements NestInterceptor {
|
||||
private usageService;
|
||||
private readonly logger;
|
||||
constructor(usageService: UsageService);
|
||||
intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>>;
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||
};
|
||||
var UsageInterceptor_1;
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.UsageInterceptor = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const rxjs_1 = require("rxjs");
|
||||
const usage_service_1 = require("./usage.service");
|
||||
const tenant_entity_1 = require("../auth/entities/tenant.entity");
|
||||
let UsageInterceptor = UsageInterceptor_1 = class UsageInterceptor {
|
||||
usageService;
|
||||
logger = new common_1.Logger(UsageInterceptor_1.name);
|
||||
constructor(usageService) {
|
||||
this.usageService = usageService;
|
||||
}
|
||||
async intercept(context, next) {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const tenant = request.tenant;
|
||||
const apiKey = request.apiKey;
|
||||
if (tenant && apiKey && !request.url.includes('/auth/management')) {
|
||||
this.logger.debug(`📊 Monitoring request for Tenant: ${tenant.id}`);
|
||||
const plan = tenant.plan || tenant_entity_1.TenantPlan.FREE;
|
||||
const limit = tenant_entity_1.QUOTA_LIMITS[plan];
|
||||
const { allowed: quotaAllowed, used: monthlyUsed } = await this.usageService.checkQuota(tenant.id, limit);
|
||||
if (!quotaAllowed) {
|
||||
throw new common_1.HttpException({
|
||||
statusCode: common_1.HttpStatus.TOO_MANY_REQUESTS,
|
||||
error: 'Quota Exceeded',
|
||||
message: 'Monthly API usage quota exceeded',
|
||||
used: monthlyUsed,
|
||||
limit,
|
||||
upgrade_url: 'https://map-dashbord.intaleqapp.com/#billing'
|
||||
}, common_1.HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
const rpmLimit = tenant_entity_1.RATE_LIMITS[plan];
|
||||
const { allowed: rateAllowed, used: currentRpm } = await this.usageService.checkRateLimit(tenant.id, rpmLimit);
|
||||
if (!rateAllowed) {
|
||||
throw new common_1.HttpException({
|
||||
statusCode: common_1.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'
|
||||
}, common_1.HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
}
|
||||
const startTime = Date.now();
|
||||
return next.handle().pipe((0, rxjs_1.tap)(() => {
|
||||
if (tenant && apiKey) {
|
||||
const response = context.switchToHttp().getResponse();
|
||||
const responseTimeMs = Date.now() - startTime;
|
||||
this.logger.log(`📈 Usage Recorded: ${request.method} ${request.url.split('?')[0]} for Tenant ${tenant.id}`);
|
||||
this.usageService.recordRequest({
|
||||
tenantId: tenant.id,
|
||||
apiKeyId: apiKey.id,
|
||||
endpoint: request.url.split('?')[0],
|
||||
method: request.method,
|
||||
statusCode: response.statusCode,
|
||||
responseTimeMs,
|
||||
userAgent: request.headers['user-agent'],
|
||||
ipAddress: request.ip || request.headers['x-forwarded-for'],
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
exports.UsageInterceptor = UsageInterceptor;
|
||||
exports.UsageInterceptor = UsageInterceptor = UsageInterceptor_1 = __decorate([
|
||||
(0, common_1.Injectable)(),
|
||||
__metadata("design:paramtypes", [usage_service_1.UsageService])
|
||||
], UsageInterceptor);
|
||||
//# sourceMappingURL=usage.interceptor.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"usage.interceptor.js","sourceRoot":"","sources":["../../src/usage/usage.interceptor.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAQwB;AACxB,+BAAuC;AACvC,mDAA+C;AAC/C,kEAAuF;AAGhF,IAAM,gBAAgB,wBAAtB,MAAM,gBAAgB;IAEP;IADH,MAAM,GAAG,IAAI,eAAM,CAAC,kBAAgB,CAAC,IAAI,CAAC,CAAC;IAC5D,YAAoB,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAAI,CAAC;IAEnD,KAAK,CAAC,SAAS,CAAC,OAAyB,EAAE,IAAiB;QAC1D,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAE,CAAC;QACpD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC9B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAG9B,IAAI,MAAM,IAAI,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAClE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;YACpE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,0BAAU,CAAC,IAAI,CAAC;YAC5C,MAAM,KAAK,GAAG,4BAAY,CAAC,IAAI,CAAC,CAAC;YAGjC,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YAC1G,IAAI,CAAC,YAAY,EAAE,CAAC;gBAClB,MAAM,IAAI,sBAAa,CAAC;oBACtB,UAAU,EAAE,mBAAU,CAAC,iBAAiB;oBACxC,KAAK,EAAE,gBAAgB;oBACvB,OAAO,EAAE,kCAAkC;oBAC3C,IAAI,EAAE,WAAW;oBACjB,KAAK;oBACL,WAAW,EAAE,8CAA8C;iBAC5D,EAAE,mBAAU,CAAC,iBAAiB,CAAC,CAAC;YACnC,CAAC;YAGD,MAAM,QAAQ,GAAG,2BAAW,CAAC,IAAI,CAAC,CAAC;YACnC,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;YAC/G,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,MAAM,IAAI,sBAAa,CAAC;oBACtB,UAAU,EAAE,mBAAU,CAAC,iBAAiB;oBACxC,KAAK,EAAE,qBAAqB;oBAC5B,OAAO,EAAE,gCAAgC,QAAQ,gBAAgB,IAAI,QAAQ;oBAC7E,YAAY,EAAE,UAAU;oBACxB,KAAK,EAAE,QAAQ;oBACf,WAAW,EAAE,KAAK;iBACnB,EAAE,mBAAU,CAAC,iBAAiB,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAE7B,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CACvB,IAAA,UAAG,EAAC,GAAG,EAAE;YAEP,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;gBACrB,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,WAAW,EAAE,CAAC;gBACtD,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;gBAE9C,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,sBAAsB,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC7G,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC;oBAC9B,QAAQ,EAAE,MAAM,CAAC,EAAE;oBACnB,QAAQ,EAAE,MAAM,CAAC,EAAE;oBACnB,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;oBACnC,MAAM,EAAE,OAAO,CAAC,MAAM;oBACtB,UAAU,EAAE,QAAQ,CAAC,UAAU;oBAC/B,cAAc;oBACd,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC;oBACxC,SAAS,EAAE,OAAO,CAAC,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC;iBAC5D,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF,CAAA;AAnEY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,mBAAU,GAAE;qCAGuB,4BAAY;GAFnC,gBAAgB,CAmE5B"}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export declare class UsageModule {
|
||||
}
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.UsageModule = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const typeorm_1 = require("@nestjs/typeorm");
|
||||
const usage_entity_1 = require("./usage.entity");
|
||||
const usage_service_1 = require("./usage.service");
|
||||
const usage_controller_1 = require("./usage.controller");
|
||||
let UsageModule = class UsageModule {
|
||||
};
|
||||
exports.UsageModule = UsageModule;
|
||||
exports.UsageModule = UsageModule = __decorate([
|
||||
(0, common_1.Module)({
|
||||
imports: [
|
||||
typeorm_1.TypeOrmModule.forFeature([usage_entity_1.UsageLog]),
|
||||
],
|
||||
providers: [usage_service_1.UsageService],
|
||||
controllers: [usage_controller_1.UsageController],
|
||||
exports: [usage_service_1.UsageService],
|
||||
})
|
||||
], UsageModule);
|
||||
//# sourceMappingURL=usage.module.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"usage.module.js","sourceRoot":"","sources":["../../src/usage/usage.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,6CAAgD;AAChD,iDAA0C;AAC1C,mDAA+C;AAC/C,yDAAqD;AAU9C,IAAM,WAAW,GAAjB,MAAM,WAAW;CAAG,CAAA;AAAd,kCAAW;sBAAX,WAAW;IARvB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,uBAAa,CAAC,UAAU,CAAC,CAAC,uBAAQ,CAAC,CAAC;SACrC;QACD,SAAS,EAAE,CAAC,4BAAY,CAAC;QACzB,WAAW,EAAE,CAAC,kCAAe,CAAC;QAC9B,OAAO,EAAE,CAAC,4BAAY,CAAC;KACxB,CAAC;GACW,WAAW,CAAG"}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { Repository } from 'typeorm';
|
||||
import { UsageLog } from './usage.entity';
|
||||
import { RedisService } from '../common/redis.service';
|
||||
export declare class UsageService {
|
||||
private usageRepository;
|
||||
private redisService;
|
||||
private readonly logger;
|
||||
constructor(usageRepository: Repository<UsageLog>, redisService: RedisService);
|
||||
recordRequest(data: {
|
||||
tenantId: string;
|
||||
apiKeyId: string;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
statusCode: number;
|
||||
responseTimeMs: number;
|
||||
userAgent?: string;
|
||||
ipAddress?: string;
|
||||
}): Promise<void>;
|
||||
getMonthlyUsage(tenantId: string): Promise<number>;
|
||||
checkQuota(tenantId: string, limit: number): Promise<{
|
||||
allowed: boolean;
|
||||
used: number;
|
||||
}>;
|
||||
checkRateLimit(tenantId: string, limit: number): Promise<{
|
||||
allowed: boolean;
|
||||
used: number;
|
||||
}>;
|
||||
getUsageHistory(tenantId: string, days?: number): Promise<any>;
|
||||
getUsageSummary(tenantId: string, plan?: string): Promise<{
|
||||
monthlyUsage: number;
|
||||
monthlyLimit: any;
|
||||
totalToday: number;
|
||||
avgLatency: number;
|
||||
successRate: number;
|
||||
lastUpdated: Date;
|
||||
}>;
|
||||
private getMonthlyKey;
|
||||
private getRateLimitKey;
|
||||
}
|
||||
Vendored
+135
@@ -0,0 +1,135 @@
|
||||
"use strict";
|
||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||
};
|
||||
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
||||
return function (target, key) { decorator(target, key, paramIndex); }
|
||||
};
|
||||
var UsageService_1;
|
||||
var _a;
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.UsageService = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const typeorm_1 = require("@nestjs/typeorm");
|
||||
const typeorm_2 = require("typeorm");
|
||||
const usage_entity_1 = require("./usage.entity");
|
||||
const redis_service_1 = require("../common/redis.service");
|
||||
let UsageService = UsageService_1 = class UsageService {
|
||||
usageRepository;
|
||||
redisService;
|
||||
logger = new common_1.Logger(UsageService_1.name);
|
||||
constructor(usageRepository, redisService) {
|
||||
this.usageRepository = usageRepository;
|
||||
this.redisService = redisService;
|
||||
}
|
||||
async recordRequest(data) {
|
||||
this.usageRepository.save(data).catch((err) => {
|
||||
this.logger.error(`Failed to save usage log to DB: ${err.message}`);
|
||||
});
|
||||
const monthKey = this.getMonthlyKey(data.tenantId);
|
||||
try {
|
||||
await this.redisService.getClient().incr(monthKey);
|
||||
}
|
||||
catch (err) {
|
||||
this.logger.error(`Failed to increment Redis usage counter: ${err.message}`);
|
||||
}
|
||||
}
|
||||
async getMonthlyUsage(tenantId) {
|
||||
const key = this.getMonthlyKey(tenantId);
|
||||
const val = await this.redisService.getClient().get(key);
|
||||
return val ? parseInt(val, 10) : 0;
|
||||
}
|
||||
async checkQuota(tenantId, limit) {
|
||||
const used = await this.getMonthlyUsage(tenantId);
|
||||
return {
|
||||
allowed: used < limit,
|
||||
used,
|
||||
};
|
||||
}
|
||||
async checkRateLimit(tenantId, limit) {
|
||||
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 };
|
||||
}
|
||||
const multi = client.multi();
|
||||
multi.incr(key);
|
||||
if (!usedRaw) {
|
||||
multi.expire(key, 60);
|
||||
}
|
||||
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 };
|
||||
}
|
||||
}
|
||||
async getUsageHistory(tenantId, days = 30) {
|
||||
return this.usageRepository
|
||||
.createQueryBuilder('usage')
|
||||
.select("DATE_TRUNC('day', usage.createdAt)", 'date')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('usage.tenantId = :tenantId', { tenantId })
|
||||
.andWhere("usage.createdAt >= NOW() - (:days || ' days')::INTERVAL", { days: days.toString() })
|
||||
.groupBy("DATE_TRUNC('day', usage.createdAt)")
|
||||
.orderBy('date', 'ASC')
|
||||
.getRawMany();
|
||||
}
|
||||
async getUsageSummary(tenantId, plan = 'FREE') {
|
||||
const monthlyUsage = await this.getMonthlyUsage(tenantId);
|
||||
const QUOTA_LIMITS = {
|
||||
'FREE': 8000,
|
||||
'PRO': 50000,
|
||||
'ENTERPRISE': 1000000
|
||||
};
|
||||
const monthlyLimit = QUOTA_LIMITS[plan] || 8000;
|
||||
const stats = await this.usageRepository
|
||||
.createQueryBuilder('usage')
|
||||
.select('COUNT(*)', 'totalToday')
|
||||
.addSelect('AVG(usage.responseTimeMs)', 'avgLatency')
|
||||
.addSelect('COUNT(CASE WHEN usage.statusCode >= 200 AND usage.statusCode < 300 THEN 1 END)', 'successCount')
|
||||
.where('usage.tenantId = :tenantId', { tenantId })
|
||||
.andWhere('usage.createdAt >= CURRENT_DATE')
|
||||
.getRawOne();
|
||||
const totalToday = parseInt(stats.totalToday || '0', 10);
|
||||
const avgLatency = Math.round(parseFloat(stats.avgLatency || '0'));
|
||||
const successRate = totalToday > 0
|
||||
? Math.round((parseInt(stats.successCount || '0', 10) / totalToday) * 100)
|
||||
: 100;
|
||||
return {
|
||||
monthlyUsage,
|
||||
monthlyLimit,
|
||||
totalToday,
|
||||
avgLatency,
|
||||
successRate,
|
||||
lastUpdated: new Date()
|
||||
};
|
||||
}
|
||||
getMonthlyKey(tenantId) {
|
||||
const now = new Date();
|
||||
const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
||||
return `usage:${tenantId}:${yearMonth}`;
|
||||
}
|
||||
getRateLimitKey(tenantId) {
|
||||
const now = new Date();
|
||||
const window = `${now.getFullYear()}${now.getMonth()}${now.getDate()}${now.getHours()}${now.getMinutes()}`;
|
||||
return `ratelimit:${tenantId}:${window}`;
|
||||
}
|
||||
};
|
||||
exports.UsageService = UsageService;
|
||||
exports.UsageService = UsageService = UsageService_1 = __decorate([
|
||||
(0, common_1.Injectable)(),
|
||||
__param(0, (0, typeorm_1.InjectRepository)(usage_entity_1.UsageLog)),
|
||||
__metadata("design:paramtypes", [typeof (_a = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _a : Object, redis_service_1.RedisService])
|
||||
], UsageService);
|
||||
//# sourceMappingURL=usage.service.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"usage.service.js","sourceRoot":"","sources":["../../src/usage/usage.service.ts"],"names":[],"mappings":"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"}
|
||||
Reference in New Issue
Block a user