nestjs-ddd-cli 2.2.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -408
- package/ddd.schema.json +111 -0
- package/dist/commands/aggregate-validator.d.ts +9 -0
- package/dist/commands/aggregate-validator.js +953 -0
- package/dist/commands/aggregate-validator.js.map +1 -0
- package/dist/commands/ai-assist.d.ts +8 -0
- package/dist/commands/ai-assist.js +337 -0
- package/dist/commands/ai-assist.js.map +1 -0
- package/dist/commands/api-contracts.d.ts +9 -0
- package/dist/commands/api-contracts.js +1368 -0
- package/dist/commands/api-contracts.js.map +1 -0
- package/dist/commands/api-docs.d.ts +8 -0
- package/dist/commands/api-docs.js +408 -0
- package/dist/commands/api-docs.js.map +1 -0
- package/dist/commands/api-versioning.d.ts +11 -0
- package/dist/commands/api-versioning.js +643 -0
- package/dist/commands/api-versioning.js.map +1 -0
- package/dist/commands/audit-logging.d.ts +9 -0
- package/dist/commands/audit-logging.js +1129 -0
- package/dist/commands/audit-logging.js.map +1 -0
- package/dist/commands/batch-generate.d.ts +10 -0
- package/dist/commands/batch-generate.js +405 -0
- package/dist/commands/batch-generate.js.map +1 -0
- package/dist/commands/caching-strategies.d.ts +9 -0
- package/dist/commands/caching-strategies.js +874 -0
- package/dist/commands/caching-strategies.js.map +1 -0
- package/dist/commands/code-analyzer.d.ts +42 -0
- package/dist/commands/code-analyzer.js +474 -0
- package/dist/commands/code-analyzer.js.map +1 -0
- package/dist/commands/database-seeding.d.ts +6 -0
- package/dist/commands/database-seeding.js +621 -0
- package/dist/commands/database-seeding.js.map +1 -0
- package/dist/commands/db-optimization.d.ts +7 -0
- package/dist/commands/db-optimization.js +687 -0
- package/dist/commands/db-optimization.js.map +1 -0
- package/dist/commands/dependency-graph.d.ts +6 -0
- package/dist/commands/dependency-graph.js +329 -0
- package/dist/commands/dependency-graph.js.map +1 -0
- package/dist/commands/doctor-enhanced.d.ts +22 -0
- package/dist/commands/doctor-enhanced.js +543 -0
- package/dist/commands/doctor-enhanced.js.map +1 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +151 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/env-manager.d.ts +6 -0
- package/dist/commands/env-manager.js +419 -0
- package/dist/commands/env-manager.js.map +1 -0
- package/dist/commands/event-sourcing-full.d.ts +10 -0
- package/dist/commands/event-sourcing-full.js +1107 -0
- package/dist/commands/event-sourcing-full.js.map +1 -0
- package/dist/commands/feature-flags.d.ts +9 -0
- package/dist/commands/feature-flags.js +824 -0
- package/dist/commands/feature-flags.js.map +1 -0
- package/dist/commands/filter-dsl.d.ts +10 -0
- package/dist/commands/filter-dsl.js +1407 -0
- package/dist/commands/filter-dsl.js.map +1 -0
- package/dist/commands/generate-all.js +485 -32
- package/dist/commands/generate-all.js.map +1 -1
- package/dist/commands/generate-deployment.d.ts +8 -0
- package/dist/commands/generate-deployment.js +746 -0
- package/dist/commands/generate-deployment.js.map +1 -0
- package/dist/commands/generate-domain-service.d.ts +14 -0
- package/dist/commands/generate-domain-service.js +796 -0
- package/dist/commands/generate-domain-service.js.map +1 -0
- package/dist/commands/generate-entity.js +82 -24
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/commands/generate-from-schema.d.ts +56 -0
- package/dist/commands/generate-from-schema.js +222 -0
- package/dist/commands/generate-from-schema.js.map +1 -0
- package/dist/commands/generate-orchestrator.d.ts +14 -0
- package/dist/commands/generate-orchestrator.js +887 -0
- package/dist/commands/generate-orchestrator.js.map +1 -0
- package/dist/commands/generate-repository.d.ts +14 -0
- package/dist/commands/generate-repository.js +1019 -0
- package/dist/commands/generate-repository.js.map +1 -0
- package/dist/commands/generate-shared.d.ts +4 -0
- package/dist/commands/generate-shared.js +388 -0
- package/dist/commands/generate-shared.js.map +1 -0
- package/dist/commands/generate-value-object.d.ts +32 -0
- package/dist/commands/generate-value-object.js +700 -0
- package/dist/commands/generate-value-object.js.map +1 -0
- package/dist/commands/graphql-subscriptions.d.ts +6 -0
- package/dist/commands/graphql-subscriptions.js +607 -0
- package/dist/commands/graphql-subscriptions.js.map +1 -0
- package/dist/commands/graphql-types.d.ts +5 -0
- package/dist/commands/graphql-types.js +423 -0
- package/dist/commands/graphql-types.js.map +1 -0
- package/dist/commands/health-probes-advanced.d.ts +6 -0
- package/dist/commands/health-probes-advanced.js +655 -0
- package/dist/commands/health-probes-advanced.js.map +1 -0
- package/dist/commands/i18n-setup.d.ts +10 -0
- package/dist/commands/i18n-setup.js +677 -0
- package/dist/commands/i18n-setup.js.map +1 -0
- package/dist/commands/init-config.d.ts +6 -0
- package/dist/commands/init-config.js +370 -0
- package/dist/commands/init-config.js.map +1 -0
- package/dist/commands/init-project.js +56 -6
- package/dist/commands/init-project.js.map +1 -1
- package/dist/commands/interactive-scaffold.d.ts +5 -0
- package/dist/commands/interactive-scaffold.js +271 -0
- package/dist/commands/interactive-scaffold.js.map +1 -0
- package/dist/commands/metrics-prometheus.d.ts +6 -0
- package/dist/commands/metrics-prometheus.js +681 -0
- package/dist/commands/metrics-prometheus.js.map +1 -0
- package/dist/commands/migration-engine.d.ts +6 -0
- package/dist/commands/migration-engine.js +446 -0
- package/dist/commands/migration-engine.js.map +1 -0
- package/dist/commands/migration.d.ts +12 -0
- package/dist/commands/migration.js +484 -0
- package/dist/commands/migration.js.map +1 -0
- package/dist/commands/monorepo.d.ts +8 -0
- package/dist/commands/monorepo.js +483 -0
- package/dist/commands/monorepo.js.map +1 -0
- package/dist/commands/multi-database.d.ts +5 -0
- package/dist/commands/multi-database.js +439 -0
- package/dist/commands/multi-database.js.map +1 -0
- package/dist/commands/observability-tracing.d.ts +10 -0
- package/dist/commands/observability-tracing.js +740 -0
- package/dist/commands/observability-tracing.js.map +1 -0
- package/dist/commands/openapi-export.d.ts +8 -0
- package/dist/commands/openapi-export.js +359 -0
- package/dist/commands/openapi-export.js.map +1 -0
- package/dist/commands/perf-analyzer.d.ts +8 -0
- package/dist/commands/perf-analyzer.js +423 -0
- package/dist/commands/perf-analyzer.js.map +1 -0
- package/dist/commands/rate-limiting.d.ts +10 -0
- package/dist/commands/rate-limiting.js +953 -0
- package/dist/commands/rate-limiting.js.map +1 -0
- package/dist/commands/recipe-plugin.d.ts +56 -0
- package/dist/commands/recipe-plugin.js +315 -0
- package/dist/commands/recipe-plugin.js.map +1 -0
- package/dist/commands/recipe.d.ts +6 -0
- package/dist/commands/recipe.js +3941 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.d.ts +1 -0
- package/dist/commands/recipes/elasticsearch.recipe.js +761 -0
- package/dist/commands/recipes/elasticsearch.recipe.js.map +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.d.ts +1 -0
- package/dist/commands/recipes/event-sourcing.recipe.js +889 -0
- package/dist/commands/recipes/event-sourcing.recipe.js.map +1 -0
- package/dist/commands/recipes/index.d.ts +7 -0
- package/dist/commands/recipes/index.js +24 -0
- package/dist/commands/recipes/index.js.map +1 -0
- package/dist/commands/recipes/message-queue.recipe.d.ts +1 -0
- package/dist/commands/recipes/message-queue.recipe.js +706 -0
- package/dist/commands/recipes/message-queue.recipe.js.map +1 -0
- package/dist/commands/recipes/middleware.recipe.d.ts +1 -0
- package/dist/commands/recipes/middleware.recipe.js +383 -0
- package/dist/commands/recipes/middleware.recipe.js.map +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.d.ts +1 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js +520 -0
- package/dist/commands/recipes/multi-tenancy.recipe.js.map +1 -0
- package/dist/commands/recipes/oauth2.recipe.d.ts +1 -0
- package/dist/commands/recipes/oauth2.recipe.js +472 -0
- package/dist/commands/recipes/oauth2.recipe.js.map +1 -0
- package/dist/commands/recipes/websocket.recipe.d.ts +1 -0
- package/dist/commands/recipes/websocket.recipe.js +453 -0
- package/dist/commands/recipes/websocket.recipe.js.map +1 -0
- package/dist/commands/resilience-patterns.d.ts +13 -0
- package/dist/commands/resilience-patterns.js +1029 -0
- package/dist/commands/resilience-patterns.js.map +1 -0
- package/dist/commands/security-patterns.d.ts +11 -0
- package/dist/commands/security-patterns.js +2233 -0
- package/dist/commands/security-patterns.js.map +1 -0
- package/dist/commands/template-debug.d.ts +27 -0
- package/dist/commands/template-debug.js +388 -0
- package/dist/commands/template-debug.js.map +1 -0
- package/dist/commands/test-factory-full.d.ts +9 -0
- package/dist/commands/test-factory-full.js +1570 -0
- package/dist/commands/test-factory-full.js.map +1 -0
- package/dist/commands/test-scaffold.d.ts +7 -0
- package/dist/commands/test-scaffold.js +621 -0
- package/dist/commands/test-scaffold.js.map +1 -0
- package/dist/index.js +1088 -0
- package/dist/index.js.map +1 -1
- package/dist/templates/ai-context/CLAUDE.md.hbs +158 -0
- package/dist/templates/ai-context/conventions.md.hbs +154 -0
- package/dist/templates/command/create-command.hbs +6 -14
- package/dist/templates/command/delete-command.hbs +19 -0
- package/dist/templates/command/update-command.hbs +24 -0
- package/dist/templates/controller/controller.hbs +64 -17
- package/dist/templates/dto/create-dto.hbs +29 -5
- package/dist/templates/dto/filter-dto.hbs +52 -0
- package/dist/templates/dto/filter-query.dto.hbs +148 -0
- package/dist/templates/dto/paginated-response.dto.hbs +29 -0
- package/dist/templates/dto/pagination-query.dto.hbs +30 -0
- package/dist/templates/dto/response-dto.hbs +38 -0
- package/dist/templates/dto/update-dto.hbs +11 -0
- package/dist/templates/entity/entity.hbs +32 -1
- package/dist/templates/event/domain-event.hbs +33 -7
- package/dist/templates/event/event-handler.hbs +40 -0
- package/dist/templates/exception/base-exceptions.hbs +69 -0
- package/dist/templates/exception/entity-not-found.exception.hbs +7 -0
- package/dist/templates/mapper/mapper.hbs +49 -24
- package/dist/templates/module/module.hbs +34 -10
- package/dist/templates/orm-entity/orm-entity.hbs +63 -12
- package/dist/templates/prisma/prisma-mapper.hbs +71 -0
- package/dist/templates/prisma/prisma-repository.hbs +114 -0
- package/dist/templates/prisma/prisma-schema.hbs +20 -0
- package/dist/templates/prisma/prisma-service.hbs +51 -0
- package/dist/templates/query/get-all.query.hbs +50 -0
- package/dist/templates/query/get-by-id.query.hbs +31 -0
- package/dist/templates/repository/repository.hbs +55 -13
- package/dist/templates/resolver/graphql-input.hbs +54 -0
- package/dist/templates/resolver/graphql-type.hbs +58 -0
- package/dist/templates/resolver/pagination-args.hbs +33 -0
- package/dist/templates/resolver/resolver.hbs +62 -0
- package/dist/templates/shared/prisma-query-builder.util.hbs +189 -0
- package/dist/templates/shared/query-builder.util.hbs +218 -0
- package/dist/templates/test/controller.spec.hbs +124 -0
- package/dist/templates/test/repository.spec.hbs +158 -0
- package/dist/templates/test/usecase.spec.hbs +116 -0
- package/dist/templates/usecase/create-usecase.hbs +19 -7
- package/dist/templates/usecase/delete-usecase.hbs +17 -0
- package/dist/templates/usecase/update-usecase.hbs +31 -0
- package/dist/utils/config.utils.d.ts +45 -0
- package/dist/utils/config.utils.js +211 -0
- package/dist/utils/config.utils.js.map +1 -0
- package/dist/utils/error.utils.d.ts +145 -0
- package/dist/utils/error.utils.js +422 -0
- package/dist/utils/error.utils.js.map +1 -0
- package/dist/utils/field.utils.d.ts +54 -0
- package/dist/utils/field.utils.js +389 -0
- package/dist/utils/field.utils.js.map +1 -0
- package/dist/utils/file.utils.d.ts +19 -8
- package/dist/utils/file.utils.js +135 -4
- package/dist/utils/file.utils.js.map +1 -1
- package/dist/utils/idempotency.utils.d.ts +123 -0
- package/dist/utils/idempotency.utils.js +444 -0
- package/dist/utils/idempotency.utils.js.map +1 -0
- package/dist/utils/naming.utils.js +26 -3
- package/dist/utils/naming.utils.js.map +1 -1
- package/dist/utils/performance.utils.d.ts +37 -0
- package/dist/utils/performance.utils.js +158 -0
- package/dist/utils/performance.utils.js.map +1 -0
- package/dist/utils/relation.utils.d.ts +92 -0
- package/dist/utils/relation.utils.js +388 -0
- package/dist/utils/relation.utils.js.map +1 -0
- package/dist/utils/rollback.utils.d.ts +49 -0
- package/dist/utils/rollback.utils.js +306 -0
- package/dist/utils/rollback.utils.js.map +1 -0
- package/dist/utils/schema.utils.d.ts +123 -0
- package/dist/utils/schema.utils.js +419 -0
- package/dist/utils/schema.utils.js.map +1 -0
- package/dist/utils/security.utils.d.ts +57 -0
- package/dist/utils/security.utils.js +315 -0
- package/dist/utils/security.utils.js.map +1 -0
- package/dist/utils/template-engine.utils.d.ts +80 -0
- package/dist/utils/template-engine.utils.js +463 -0
- package/dist/utils/template-engine.utils.js.map +1 -0
- package/dist/utils/validation-registry.utils.d.ts +160 -0
- package/dist/utils/validation-registry.utils.js +526 -0
- package/dist/utils/validation-registry.utils.js.map +1 -0
- package/package.json +3 -1
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rate Limiting & Throttling Framework Generator
|
|
4
|
+
* Generates comprehensive rate limiting infrastructure
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.setupRateLimiting = setupRateLimiting;
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
47
|
+
async function setupRateLimiting(basePath, options = {}) {
|
|
48
|
+
console.log(chalk_1.default.bold.blue('\n⏱️ Setting up Rate Limiting Framework\n'));
|
|
49
|
+
const sharedPath = path.join(basePath, 'src/shared/rate-limiting');
|
|
50
|
+
if (!fs.existsSync(sharedPath)) {
|
|
51
|
+
fs.mkdirSync(sharedPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
// Generate rate limiting module
|
|
54
|
+
const moduleContent = generateRateLimitingModule(options);
|
|
55
|
+
fs.writeFileSync(path.join(sharedPath, 'rate-limiting.module.ts'), moduleContent);
|
|
56
|
+
console.log(chalk_1.default.green(` ✓ Created rate limiting module`));
|
|
57
|
+
// Generate rate limiter service
|
|
58
|
+
const serviceContent = generateRateLimiterService(options);
|
|
59
|
+
fs.writeFileSync(path.join(sharedPath, 'rate-limiter.service.ts'), serviceContent);
|
|
60
|
+
console.log(chalk_1.default.green(` ✓ Created rate limiter service`));
|
|
61
|
+
// Generate rate limit guard
|
|
62
|
+
const guardContent = generateRateLimitGuard();
|
|
63
|
+
fs.writeFileSync(path.join(sharedPath, 'rate-limit.guard.ts'), guardContent);
|
|
64
|
+
console.log(chalk_1.default.green(` ✓ Created rate limit guard`));
|
|
65
|
+
// Generate throttle decorator
|
|
66
|
+
const decoratorContent = generateThrottleDecorator();
|
|
67
|
+
fs.writeFileSync(path.join(sharedPath, 'throttle.decorator.ts'), decoratorContent);
|
|
68
|
+
console.log(chalk_1.default.green(` ✓ Created throttle decorator`));
|
|
69
|
+
// Generate quota manager
|
|
70
|
+
const quotaContent = generateQuotaManager();
|
|
71
|
+
fs.writeFileSync(path.join(sharedPath, 'quota-manager.ts'), quotaContent);
|
|
72
|
+
console.log(chalk_1.default.green(` ✓ Created quota manager`));
|
|
73
|
+
// Generate metrics collector
|
|
74
|
+
const metricsContent = generateMetricsCollector();
|
|
75
|
+
fs.writeFileSync(path.join(sharedPath, 'metrics.collector.ts'), metricsContent);
|
|
76
|
+
console.log(chalk_1.default.green(` ✓ Created metrics collector`));
|
|
77
|
+
console.log(chalk_1.default.bold.green('\n✅ Rate limiting framework ready!\n'));
|
|
78
|
+
}
|
|
79
|
+
function generateRateLimitingModule(options) {
|
|
80
|
+
return `import { Module, Global, DynamicModule } from '@nestjs/common';
|
|
81
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
82
|
+
import { RateLimiterService } from './rate-limiter.service';
|
|
83
|
+
import { RateLimitGuard } from './rate-limit.guard';
|
|
84
|
+
import { QuotaManager } from './quota-manager';
|
|
85
|
+
import { MetricsCollector } from './metrics.collector';
|
|
86
|
+
|
|
87
|
+
export interface RateLimitingModuleOptions {
|
|
88
|
+
strategy: 'token-bucket' | 'sliding-window' | 'fixed-window';
|
|
89
|
+
storage: 'memory' | 'redis';
|
|
90
|
+
redis?: {
|
|
91
|
+
host: string;
|
|
92
|
+
port: number;
|
|
93
|
+
password?: string;
|
|
94
|
+
};
|
|
95
|
+
defaults: {
|
|
96
|
+
ttl: number;
|
|
97
|
+
limit: number;
|
|
98
|
+
};
|
|
99
|
+
skipIf?: (context: any) => boolean;
|
|
100
|
+
keyGenerator?: (context: any) => string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Global()
|
|
104
|
+
@Module({})
|
|
105
|
+
export class RateLimitingModule {
|
|
106
|
+
static forRoot(options: RateLimitingModuleOptions): DynamicModule {
|
|
107
|
+
return {
|
|
108
|
+
module: RateLimitingModule,
|
|
109
|
+
providers: [
|
|
110
|
+
{
|
|
111
|
+
provide: 'RATE_LIMITING_OPTIONS',
|
|
112
|
+
useValue: options,
|
|
113
|
+
},
|
|
114
|
+
RateLimiterService,
|
|
115
|
+
QuotaManager,
|
|
116
|
+
MetricsCollector,
|
|
117
|
+
{
|
|
118
|
+
provide: APP_GUARD,
|
|
119
|
+
useClass: RateLimitGuard,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
exports: [RateLimiterService, QuotaManager, MetricsCollector],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static forFeature(config: FeatureRateLimitConfig): DynamicModule {
|
|
127
|
+
return {
|
|
128
|
+
module: RateLimitingModule,
|
|
129
|
+
providers: [
|
|
130
|
+
{
|
|
131
|
+
provide: 'FEATURE_RATE_LIMIT_CONFIG',
|
|
132
|
+
useValue: config,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface FeatureRateLimitConfig {
|
|
140
|
+
name: string;
|
|
141
|
+
ttl: number;
|
|
142
|
+
limit: number;
|
|
143
|
+
keyPrefix?: string;
|
|
144
|
+
}
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
function generateRateLimiterService(options) {
|
|
148
|
+
const strategy = options.strategy || 'sliding-window';
|
|
149
|
+
return `import { Injectable, Inject, Logger } from '@nestjs/common';
|
|
150
|
+
import { MetricsCollector } from './metrics.collector';
|
|
151
|
+
|
|
152
|
+
export interface RateLimitResult {
|
|
153
|
+
allowed: boolean;
|
|
154
|
+
remaining: number;
|
|
155
|
+
resetAt: Date;
|
|
156
|
+
retryAfter?: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface RateLimitConfig {
|
|
160
|
+
key: string;
|
|
161
|
+
limit: number;
|
|
162
|
+
ttl: number; // in seconds
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Injectable()
|
|
166
|
+
export class RateLimiterService {
|
|
167
|
+
private readonly logger = new Logger(RateLimiterService.name);
|
|
168
|
+
private readonly storage = new Map<string, RateLimitEntry>();
|
|
169
|
+
|
|
170
|
+
constructor(
|
|
171
|
+
@Inject('RATE_LIMITING_OPTIONS') private readonly options: any,
|
|
172
|
+
private readonly metrics: MetricsCollector,
|
|
173
|
+
) {}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if request should be allowed
|
|
177
|
+
*/
|
|
178
|
+
async check(config: RateLimitConfig): Promise<RateLimitResult> {
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
const windowStart = now - (config.ttl * 1000);
|
|
181
|
+
|
|
182
|
+
let entry = this.storage.get(config.key);
|
|
183
|
+
|
|
184
|
+
// Clean old entries
|
|
185
|
+
if (entry) {
|
|
186
|
+
entry.requests = entry.requests.filter(time => time > windowStart);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!entry) {
|
|
190
|
+
entry = { requests: [], createdAt: now };
|
|
191
|
+
this.storage.set(config.key, entry);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const currentCount = entry.requests.length;
|
|
195
|
+
const remaining = Math.max(0, config.limit - currentCount);
|
|
196
|
+
const resetAt = new Date(windowStart + (config.ttl * 1000));
|
|
197
|
+
|
|
198
|
+
if (currentCount >= config.limit) {
|
|
199
|
+
const retryAfter = Math.ceil((entry.requests[0] + (config.ttl * 1000) - now) / 1000);
|
|
200
|
+
|
|
201
|
+
this.metrics.recordRateLimitHit(config.key, false);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
allowed: false,
|
|
205
|
+
remaining: 0,
|
|
206
|
+
resetAt,
|
|
207
|
+
retryAfter: Math.max(1, retryAfter),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Record request
|
|
212
|
+
entry.requests.push(now);
|
|
213
|
+
this.metrics.recordRateLimitHit(config.key, true);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
allowed: true,
|
|
217
|
+
remaining: remaining - 1,
|
|
218
|
+
resetAt,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Token bucket algorithm
|
|
224
|
+
*/
|
|
225
|
+
async checkTokenBucket(config: RateLimitConfig & { refillRate: number }): Promise<RateLimitResult> {
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
let entry = this.storage.get(config.key) as TokenBucketEntry | undefined;
|
|
228
|
+
|
|
229
|
+
if (!entry) {
|
|
230
|
+
entry = {
|
|
231
|
+
tokens: config.limit,
|
|
232
|
+
lastRefill: now,
|
|
233
|
+
requests: [],
|
|
234
|
+
createdAt: now,
|
|
235
|
+
};
|
|
236
|
+
this.storage.set(config.key, entry);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Refill tokens
|
|
240
|
+
const timePassed = (now - entry.lastRefill) / 1000;
|
|
241
|
+
const tokensToAdd = Math.floor(timePassed * config.refillRate);
|
|
242
|
+
|
|
243
|
+
if (tokensToAdd > 0) {
|
|
244
|
+
entry.tokens = Math.min(config.limit, entry.tokens + tokensToAdd);
|
|
245
|
+
entry.lastRefill = now;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (entry.tokens < 1) {
|
|
249
|
+
const timeToNextToken = Math.ceil((1 - entry.tokens) / config.refillRate);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
allowed: false,
|
|
253
|
+
remaining: 0,
|
|
254
|
+
resetAt: new Date(now + timeToNextToken * 1000),
|
|
255
|
+
retryAfter: timeToNextToken,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
entry.tokens--;
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
allowed: true,
|
|
263
|
+
remaining: Math.floor(entry.tokens),
|
|
264
|
+
resetAt: new Date(now + config.ttl * 1000),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Reset rate limit for a key
|
|
270
|
+
*/
|
|
271
|
+
async reset(key: string): Promise<void> {
|
|
272
|
+
// Validate key to prevent injection
|
|
273
|
+
const sanitizedKey = key.replace(/[^a-zA-Z0-9:_\\-@.]/g, '').substring(0, 256);
|
|
274
|
+
this.storage.delete(sanitizedKey);
|
|
275
|
+
this.logger.debug(\`Reset rate limit for: \${sanitizedKey}\`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Block an IP address temporarily (brute force protection)
|
|
280
|
+
*/
|
|
281
|
+
async blockIp(ip: string, durationSeconds: number = 3600): Promise<void> {
|
|
282
|
+
const sanitizedIp = this.sanitizeIp(ip);
|
|
283
|
+
if (!sanitizedIp) {
|
|
284
|
+
this.logger.warn(\`Invalid IP address format: \${ip}\`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const blockKey = \`blocked_ip:\${sanitizedIp}\`;
|
|
289
|
+
this.storage.set(blockKey, {
|
|
290
|
+
requests: [],
|
|
291
|
+
createdAt: Date.now(),
|
|
292
|
+
blockedUntil: Date.now() + (durationSeconds * 1000),
|
|
293
|
+
} as BlockedEntry);
|
|
294
|
+
|
|
295
|
+
this.logger.warn(\`IP blocked for \${durationSeconds}s: \${sanitizedIp}\`);
|
|
296
|
+
this.metrics.recordIpBlock(sanitizedIp);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Check if an IP is blocked
|
|
301
|
+
*/
|
|
302
|
+
async isIpBlocked(ip: string): Promise<boolean> {
|
|
303
|
+
const sanitizedIp = this.sanitizeIp(ip);
|
|
304
|
+
if (!sanitizedIp) return false;
|
|
305
|
+
|
|
306
|
+
const blockKey = \`blocked_ip:\${sanitizedIp}\`;
|
|
307
|
+
const entry = this.storage.get(blockKey) as BlockedEntry | undefined;
|
|
308
|
+
|
|
309
|
+
if (entry && entry.blockedUntil && entry.blockedUntil > Date.now()) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Clean up expired block
|
|
314
|
+
if (entry) {
|
|
315
|
+
this.storage.delete(blockKey);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Detect brute force and auto-block
|
|
323
|
+
*/
|
|
324
|
+
async detectBruteForce(
|
|
325
|
+
ip: string,
|
|
326
|
+
endpoint: string,
|
|
327
|
+
config: { threshold: number; windowSeconds: number; blockDurationSeconds: number }
|
|
328
|
+
): Promise<boolean> {
|
|
329
|
+
const sanitizedIp = this.sanitizeIp(ip);
|
|
330
|
+
if (!sanitizedIp) return false;
|
|
331
|
+
|
|
332
|
+
const key = \`brute_force:\${sanitizedIp}:\${endpoint}\`;
|
|
333
|
+
const result = await this.check({
|
|
334
|
+
key,
|
|
335
|
+
limit: config.threshold,
|
|
336
|
+
ttl: config.windowSeconds,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (!result.allowed) {
|
|
340
|
+
await this.blockIp(sanitizedIp, config.blockDurationSeconds);
|
|
341
|
+
this.logger.warn(\`Brute force detected from \${sanitizedIp} on \${endpoint}\`);
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Sanitize and validate IP address
|
|
350
|
+
*/
|
|
351
|
+
private sanitizeIp(ip: string): string | null {
|
|
352
|
+
if (!ip || typeof ip !== 'string') return null;
|
|
353
|
+
|
|
354
|
+
// Remove any port suffix
|
|
355
|
+
const cleanIp = ip.split(':')[0].trim();
|
|
356
|
+
|
|
357
|
+
// Basic IPv4 validation
|
|
358
|
+
const ipv4Regex = /^(\\d{1,3}\\.){3}\\d{1,3}$/;
|
|
359
|
+
// Basic IPv6 validation (simplified)
|
|
360
|
+
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
|
|
361
|
+
|
|
362
|
+
if (ipv4Regex.test(cleanIp) || ipv6Regex.test(cleanIp)) {
|
|
363
|
+
return cleanIp;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Get current status for a key
|
|
371
|
+
*/
|
|
372
|
+
async getStatus(key: string, config: RateLimitConfig): Promise<RateLimitStatus> {
|
|
373
|
+
const entry = this.storage.get(key);
|
|
374
|
+
const now = Date.now();
|
|
375
|
+
const windowStart = now - (config.ttl * 1000);
|
|
376
|
+
|
|
377
|
+
if (!entry) {
|
|
378
|
+
return {
|
|
379
|
+
key,
|
|
380
|
+
current: 0,
|
|
381
|
+
limit: config.limit,
|
|
382
|
+
remaining: config.limit,
|
|
383
|
+
resetAt: new Date(now + config.ttl * 1000),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const validRequests = entry.requests.filter(time => time > windowStart);
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
key,
|
|
391
|
+
current: validRequests.length,
|
|
392
|
+
limit: config.limit,
|
|
393
|
+
remaining: Math.max(0, config.limit - validRequests.length),
|
|
394
|
+
resetAt: new Date(windowStart + (config.ttl * 1000)),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Cleanup expired entries
|
|
400
|
+
*/
|
|
401
|
+
cleanup(): void {
|
|
402
|
+
const now = Date.now();
|
|
403
|
+
const maxAge = 3600000; // 1 hour
|
|
404
|
+
|
|
405
|
+
for (const [key, entry] of this.storage.entries()) {
|
|
406
|
+
if (now - entry.createdAt > maxAge && entry.requests.length === 0) {
|
|
407
|
+
this.storage.delete(key);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
interface RateLimitEntry {
|
|
414
|
+
requests: number[];
|
|
415
|
+
createdAt: number;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
interface TokenBucketEntry extends RateLimitEntry {
|
|
419
|
+
tokens: number;
|
|
420
|
+
lastRefill: number;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
interface BlockedEntry extends RateLimitEntry {
|
|
424
|
+
blockedUntil?: number;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export interface RateLimitStatus {
|
|
428
|
+
key: string;
|
|
429
|
+
current: number;
|
|
430
|
+
limit: number;
|
|
431
|
+
remaining: number;
|
|
432
|
+
resetAt: Date;
|
|
433
|
+
}
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
436
|
+
function generateRateLimitGuard() {
|
|
437
|
+
return `import {
|
|
438
|
+
Injectable,
|
|
439
|
+
CanActivate,
|
|
440
|
+
ExecutionContext,
|
|
441
|
+
HttpException,
|
|
442
|
+
HttpStatus,
|
|
443
|
+
Inject,
|
|
444
|
+
} from '@nestjs/common';
|
|
445
|
+
import { Reflector } from '@nestjs/core';
|
|
446
|
+
import { RateLimiterService } from './rate-limiter.service';
|
|
447
|
+
import { MetricsCollector } from './metrics.collector';
|
|
448
|
+
|
|
449
|
+
@Injectable()
|
|
450
|
+
export class RateLimitGuard implements CanActivate {
|
|
451
|
+
constructor(
|
|
452
|
+
private readonly reflector: Reflector,
|
|
453
|
+
private readonly rateLimiter: RateLimiterService,
|
|
454
|
+
private readonly metrics: MetricsCollector,
|
|
455
|
+
@Inject('RATE_LIMITING_OPTIONS') private readonly options: any,
|
|
456
|
+
) {}
|
|
457
|
+
|
|
458
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
459
|
+
// Check if rate limiting is disabled for this route
|
|
460
|
+
const skipRateLimit = this.reflector.get<boolean>('skipRateLimit', context.getHandler());
|
|
461
|
+
if (skipRateLimit) {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const request = context.switchToHttp().getRequest();
|
|
466
|
+
|
|
467
|
+
// Check if IP is blocked (brute force protection)
|
|
468
|
+
const ip = this.extractIp(request);
|
|
469
|
+
if (ip && await this.rateLimiter.isIpBlocked(ip)) {
|
|
470
|
+
throw new HttpException(
|
|
471
|
+
{
|
|
472
|
+
statusCode: HttpStatus.FORBIDDEN,
|
|
473
|
+
message: 'IP address temporarily blocked',
|
|
474
|
+
},
|
|
475
|
+
HttpStatus.FORBIDDEN,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Check custom skip condition
|
|
480
|
+
if (this.options.skipIf && this.options.skipIf(context)) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Get rate limit config from decorator or use defaults
|
|
485
|
+
const config = this.reflector.get<{ limit: number; ttl: number }>(
|
|
486
|
+
'rateLimit',
|
|
487
|
+
context.getHandler(),
|
|
488
|
+
) || this.options.defaults;
|
|
489
|
+
|
|
490
|
+
// Generate key
|
|
491
|
+
const key = this.generateKey(context);
|
|
492
|
+
|
|
493
|
+
// Check rate limit
|
|
494
|
+
const result = await this.rateLimiter.check({
|
|
495
|
+
key,
|
|
496
|
+
limit: config.limit,
|
|
497
|
+
ttl: config.ttl,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Set headers
|
|
501
|
+
const response = context.switchToHttp().getResponse();
|
|
502
|
+
response.setHeader('X-RateLimit-Limit', config.limit);
|
|
503
|
+
response.setHeader('X-RateLimit-Remaining', result.remaining);
|
|
504
|
+
response.setHeader('X-RateLimit-Reset', result.resetAt.toISOString());
|
|
505
|
+
|
|
506
|
+
if (!result.allowed) {
|
|
507
|
+
response.setHeader('Retry-After', result.retryAfter);
|
|
508
|
+
|
|
509
|
+
throw new HttpException(
|
|
510
|
+
{
|
|
511
|
+
statusCode: HttpStatus.TOO_MANY_REQUESTS,
|
|
512
|
+
message: 'Too many requests',
|
|
513
|
+
retryAfter: result.retryAfter,
|
|
514
|
+
},
|
|
515
|
+
HttpStatus.TOO_MANY_REQUESTS,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private generateKey(context: ExecutionContext): string {
|
|
523
|
+
if (this.options.keyGenerator) {
|
|
524
|
+
return this.options.keyGenerator(context);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const request = context.switchToHttp().getRequest();
|
|
528
|
+
const ip = this.extractIp(request);
|
|
529
|
+
const userId = request.user?.id;
|
|
530
|
+
const path = request.path;
|
|
531
|
+
const method = request.method;
|
|
532
|
+
|
|
533
|
+
// Use user ID if authenticated, otherwise IP
|
|
534
|
+
const identifier = userId || ip || 'unknown';
|
|
535
|
+
|
|
536
|
+
// Sanitize key components
|
|
537
|
+
const sanitizedPath = path.replace(/[^a-zA-Z0-9\\/\\-_]/g, '').substring(0, 100);
|
|
538
|
+
const sanitizedMethod = method.replace(/[^A-Z]/g, '').substring(0, 10);
|
|
539
|
+
const sanitizedIdentifier = identifier.replace(/[^a-zA-Z0-9:@.\\-_]/g, '').substring(0, 128);
|
|
540
|
+
|
|
541
|
+
return \`rate_limit:\${sanitizedIdentifier}:\${sanitizedMethod}:\${sanitizedPath}\`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Extract real IP from request (handles proxies)
|
|
546
|
+
*/
|
|
547
|
+
private extractIp(request: any): string | null {
|
|
548
|
+
// Check common proxy headers (in order of preference)
|
|
549
|
+
const forwardedFor = request.headers?.['x-forwarded-for'];
|
|
550
|
+
if (forwardedFor) {
|
|
551
|
+
// Take the first IP (client IP) from comma-separated list
|
|
552
|
+
const ips = forwardedFor.split(',').map((ip: string) => ip.trim());
|
|
553
|
+
const clientIp = ips[0];
|
|
554
|
+
// Basic validation
|
|
555
|
+
if (/^[\\d.:a-fA-F]+$/.test(clientIp)) {
|
|
556
|
+
return clientIp;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const realIp = request.headers?.['x-real-ip'];
|
|
561
|
+
if (realIp && /^[\\d.:a-fA-F]+$/.test(realIp)) {
|
|
562
|
+
return realIp;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Fallback to direct connection IP
|
|
566
|
+
const ip = request.ip || request.connection?.remoteAddress;
|
|
567
|
+
if (ip && /^[\\d.:a-fA-F]+$/.test(ip)) {
|
|
568
|
+
return ip;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
`;
|
|
575
|
+
}
|
|
576
|
+
function generateThrottleDecorator() {
|
|
577
|
+
return `import { SetMetadata, applyDecorators } from '@nestjs/common';
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Rate limit decorator
|
|
581
|
+
*/
|
|
582
|
+
export function RateLimit(options: { limit: number; ttl: number }) {
|
|
583
|
+
return SetMetadata('rateLimit', options);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Skip rate limiting decorator
|
|
588
|
+
*/
|
|
589
|
+
export function SkipRateLimit() {
|
|
590
|
+
return SetMetadata('skipRateLimit', true);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Throttle decorator (alias for RateLimit)
|
|
595
|
+
*/
|
|
596
|
+
export function Throttle(limit: number, ttl: number) {
|
|
597
|
+
return RateLimit({ limit, ttl });
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Throttle by user
|
|
602
|
+
*/
|
|
603
|
+
export function ThrottleByUser(limit: number, ttl: number) {
|
|
604
|
+
return applyDecorators(
|
|
605
|
+
SetMetadata('rateLimit', { limit, ttl }),
|
|
606
|
+
SetMetadata('rateLimitKeyType', 'user'),
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Throttle by IP
|
|
612
|
+
*/
|
|
613
|
+
export function ThrottleByIP(limit: number, ttl: number) {
|
|
614
|
+
return applyDecorators(
|
|
615
|
+
SetMetadata('rateLimit', { limit, ttl }),
|
|
616
|
+
SetMetadata('rateLimitKeyType', 'ip'),
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Throttle by API key
|
|
622
|
+
*/
|
|
623
|
+
export function ThrottleByApiKey(limit: number, ttl: number) {
|
|
624
|
+
return applyDecorators(
|
|
625
|
+
SetMetadata('rateLimit', { limit, ttl }),
|
|
626
|
+
SetMetadata('rateLimitKeyType', 'apiKey'),
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Burst limit decorator
|
|
632
|
+
* Allows bursts up to burstLimit, then throttles
|
|
633
|
+
*/
|
|
634
|
+
export function BurstLimit(options: {
|
|
635
|
+
burstLimit: number;
|
|
636
|
+
sustainedLimit: number;
|
|
637
|
+
ttl: number;
|
|
638
|
+
}) {
|
|
639
|
+
return SetMetadata('burstLimit', options);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Dynamic rate limit
|
|
644
|
+
* Uses a function to determine the limit
|
|
645
|
+
*/
|
|
646
|
+
export function DynamicRateLimit(
|
|
647
|
+
limiter: (context: any) => { limit: number; ttl: number },
|
|
648
|
+
) {
|
|
649
|
+
return SetMetadata('dynamicRateLimit', limiter);
|
|
650
|
+
}
|
|
651
|
+
`;
|
|
652
|
+
}
|
|
653
|
+
function generateQuotaManager() {
|
|
654
|
+
return `import { Injectable, Logger } from '@nestjs/common';
|
|
655
|
+
|
|
656
|
+
export interface QuotaConfig {
|
|
657
|
+
name: string;
|
|
658
|
+
limit: number;
|
|
659
|
+
period: 'hourly' | 'daily' | 'monthly';
|
|
660
|
+
hardLimit?: boolean;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export interface QuotaUsage {
|
|
664
|
+
name: string;
|
|
665
|
+
used: number;
|
|
666
|
+
limit: number;
|
|
667
|
+
remaining: number;
|
|
668
|
+
resetAt: Date;
|
|
669
|
+
percentage: number;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
@Injectable()
|
|
673
|
+
export class QuotaManager {
|
|
674
|
+
private readonly logger = new Logger(QuotaManager.name);
|
|
675
|
+
private readonly quotas = new Map<string, QuotaEntry>();
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Register a quota
|
|
679
|
+
*/
|
|
680
|
+
register(userId: string, config: QuotaConfig): void {
|
|
681
|
+
const key = this.getKey(userId, config.name);
|
|
682
|
+
const resetAt = this.calculateResetTime(config.period);
|
|
683
|
+
|
|
684
|
+
this.quotas.set(key, {
|
|
685
|
+
config,
|
|
686
|
+
used: 0,
|
|
687
|
+
resetAt,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Check and consume quota
|
|
693
|
+
*/
|
|
694
|
+
async consume(
|
|
695
|
+
userId: string,
|
|
696
|
+
quotaName: string,
|
|
697
|
+
amount: number = 1,
|
|
698
|
+
): Promise<{ allowed: boolean; usage: QuotaUsage }> {
|
|
699
|
+
const key = this.getKey(userId, quotaName);
|
|
700
|
+
let entry = this.quotas.get(key);
|
|
701
|
+
|
|
702
|
+
if (!entry) {
|
|
703
|
+
throw new Error(\`Quota '\${quotaName}' not registered for user \${userId}\`);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Reset if period has passed
|
|
707
|
+
if (new Date() > entry.resetAt) {
|
|
708
|
+
entry.used = 0;
|
|
709
|
+
entry.resetAt = this.calculateResetTime(entry.config.period);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const wouldExceed = entry.used + amount > entry.config.limit;
|
|
713
|
+
|
|
714
|
+
if (wouldExceed && entry.config.hardLimit) {
|
|
715
|
+
return {
|
|
716
|
+
allowed: false,
|
|
717
|
+
usage: this.getUsage(entry),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
entry.used += amount;
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
allowed: !wouldExceed,
|
|
725
|
+
usage: this.getUsage(entry),
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Get quota usage
|
|
731
|
+
*/
|
|
732
|
+
getUsage(entry: QuotaEntry): QuotaUsage;
|
|
733
|
+
getUsage(userId: string, quotaName: string): QuotaUsage | null;
|
|
734
|
+
getUsage(userIdOrEntry: string | QuotaEntry, quotaName?: string): QuotaUsage | null {
|
|
735
|
+
if (typeof userIdOrEntry === 'object') {
|
|
736
|
+
const entry = userIdOrEntry;
|
|
737
|
+
return {
|
|
738
|
+
name: entry.config.name,
|
|
739
|
+
used: entry.used,
|
|
740
|
+
limit: entry.config.limit,
|
|
741
|
+
remaining: Math.max(0, entry.config.limit - entry.used),
|
|
742
|
+
resetAt: entry.resetAt,
|
|
743
|
+
percentage: (entry.used / entry.config.limit) * 100,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const key = this.getKey(userIdOrEntry, quotaName!);
|
|
748
|
+
const entry = this.quotas.get(key);
|
|
749
|
+
return entry ? this.getUsage(entry) : null;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Get all quotas for a user
|
|
754
|
+
*/
|
|
755
|
+
getAllQuotas(userId: string): QuotaUsage[] {
|
|
756
|
+
const usages: QuotaUsage[] = [];
|
|
757
|
+
|
|
758
|
+
for (const [key, entry] of this.quotas.entries()) {
|
|
759
|
+
if (key.startsWith(\`\${userId}:\`)) {
|
|
760
|
+
usages.push(this.getUsage(entry));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return usages;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Reset quota
|
|
769
|
+
*/
|
|
770
|
+
reset(userId: string, quotaName: string): void {
|
|
771
|
+
const key = this.getKey(userId, quotaName);
|
|
772
|
+
const entry = this.quotas.get(key);
|
|
773
|
+
|
|
774
|
+
if (entry) {
|
|
775
|
+
entry.used = 0;
|
|
776
|
+
entry.resetAt = this.calculateResetTime(entry.config.period);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
private getKey(userId: string, quotaName: string): string {
|
|
781
|
+
return \`\${userId}:\${quotaName}\`;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private calculateResetTime(period: 'hourly' | 'daily' | 'monthly'): Date {
|
|
785
|
+
const now = new Date();
|
|
786
|
+
|
|
787
|
+
switch (period) {
|
|
788
|
+
case 'hourly':
|
|
789
|
+
return new Date(now.getTime() + 3600000);
|
|
790
|
+
case 'daily':
|
|
791
|
+
const tomorrow = new Date(now);
|
|
792
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
793
|
+
tomorrow.setHours(0, 0, 0, 0);
|
|
794
|
+
return tomorrow;
|
|
795
|
+
case 'monthly':
|
|
796
|
+
const nextMonth = new Date(now);
|
|
797
|
+
nextMonth.setMonth(nextMonth.getMonth() + 1);
|
|
798
|
+
nextMonth.setDate(1);
|
|
799
|
+
nextMonth.setHours(0, 0, 0, 0);
|
|
800
|
+
return nextMonth;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
interface QuotaEntry {
|
|
806
|
+
config: QuotaConfig;
|
|
807
|
+
used: number;
|
|
808
|
+
resetAt: Date;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Quota decorator
|
|
813
|
+
*/
|
|
814
|
+
export function Quota(quotaName: string, amount: number = 1): MethodDecorator {
|
|
815
|
+
return function (
|
|
816
|
+
target: any,
|
|
817
|
+
propertyKey: string | symbol,
|
|
818
|
+
descriptor: PropertyDescriptor,
|
|
819
|
+
) {
|
|
820
|
+
const original = descriptor.value;
|
|
821
|
+
|
|
822
|
+
descriptor.value = async function (...args: any[]) {
|
|
823
|
+
const quotaManager: QuotaManager = (this as any).quotaManager;
|
|
824
|
+
const userId = args[0]?.user?.id; // Adjust based on your request structure
|
|
825
|
+
|
|
826
|
+
if (quotaManager && userId) {
|
|
827
|
+
const result = await quotaManager.consume(userId, quotaName, amount);
|
|
828
|
+
if (!result.allowed) {
|
|
829
|
+
throw new Error(\`Quota exceeded for \${quotaName}\`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return original.apply(this, args);
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
return descriptor;
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
`;
|
|
840
|
+
}
|
|
841
|
+
function generateMetricsCollector() {
|
|
842
|
+
return `import { Injectable, Logger } from '@nestjs/common';
|
|
843
|
+
|
|
844
|
+
export interface RateLimitMetrics {
|
|
845
|
+
totalRequests: number;
|
|
846
|
+
allowedRequests: number;
|
|
847
|
+
blockedRequests: number;
|
|
848
|
+
uniqueKeys: number;
|
|
849
|
+
topBlockedKeys: { key: string; count: number }[];
|
|
850
|
+
requestsPerMinute: number;
|
|
851
|
+
blockRate: number;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
@Injectable()
|
|
855
|
+
export class MetricsCollector {
|
|
856
|
+
private readonly logger = new Logger(MetricsCollector.name);
|
|
857
|
+
private readonly metrics: MetricEntry[] = [];
|
|
858
|
+
private readonly keyStats = new Map<string, KeyStats>();
|
|
859
|
+
private readonly maxEntries = 10000;
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Record a rate limit check
|
|
863
|
+
*/
|
|
864
|
+
recordRateLimitHit(key: string, allowed: boolean): void {
|
|
865
|
+
const now = Date.now();
|
|
866
|
+
|
|
867
|
+
this.metrics.push({
|
|
868
|
+
timestamp: now,
|
|
869
|
+
key,
|
|
870
|
+
allowed,
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
// Update key stats
|
|
874
|
+
const stats = this.keyStats.get(key) || { allowed: 0, blocked: 0 };
|
|
875
|
+
if (allowed) {
|
|
876
|
+
stats.allowed++;
|
|
877
|
+
} else {
|
|
878
|
+
stats.blocked++;
|
|
879
|
+
}
|
|
880
|
+
this.keyStats.set(key, stats);
|
|
881
|
+
|
|
882
|
+
// Cleanup old entries
|
|
883
|
+
this.cleanup();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Get current metrics
|
|
888
|
+
*/
|
|
889
|
+
getMetrics(windowMinutes: number = 5): RateLimitMetrics {
|
|
890
|
+
const windowStart = Date.now() - (windowMinutes * 60 * 1000);
|
|
891
|
+
const recentMetrics = this.metrics.filter(m => m.timestamp > windowStart);
|
|
892
|
+
|
|
893
|
+
const allowed = recentMetrics.filter(m => m.allowed).length;
|
|
894
|
+
const blocked = recentMetrics.filter(m => !m.allowed).length;
|
|
895
|
+
const total = recentMetrics.length;
|
|
896
|
+
|
|
897
|
+
// Get top blocked keys
|
|
898
|
+
const blockedByKey = new Map<string, number>();
|
|
899
|
+
for (const metric of recentMetrics.filter(m => !m.allowed)) {
|
|
900
|
+
blockedByKey.set(metric.key, (blockedByKey.get(metric.key) || 0) + 1);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const topBlockedKeys = Array.from(blockedByKey.entries())
|
|
904
|
+
.sort((a, b) => b[1] - a[1])
|
|
905
|
+
.slice(0, 10)
|
|
906
|
+
.map(([key, count]) => ({ key, count }));
|
|
907
|
+
|
|
908
|
+
return {
|
|
909
|
+
totalRequests: total,
|
|
910
|
+
allowedRequests: allowed,
|
|
911
|
+
blockedRequests: blocked,
|
|
912
|
+
uniqueKeys: new Set(recentMetrics.map(m => m.key)).size,
|
|
913
|
+
topBlockedKeys,
|
|
914
|
+
requestsPerMinute: total / windowMinutes,
|
|
915
|
+
blockRate: total > 0 ? (blocked / total) * 100 : 0,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Get stats for a specific key
|
|
921
|
+
*/
|
|
922
|
+
getKeyStats(key: string): KeyStats | null {
|
|
923
|
+
return this.keyStats.get(key) || null;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Reset metrics
|
|
928
|
+
*/
|
|
929
|
+
reset(): void {
|
|
930
|
+
this.metrics.length = 0;
|
|
931
|
+
this.keyStats.clear();
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private cleanup(): void {
|
|
935
|
+
if (this.metrics.length > this.maxEntries) {
|
|
936
|
+
this.metrics.splice(0, this.metrics.length - this.maxEntries);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
interface MetricEntry {
|
|
942
|
+
timestamp: number;
|
|
943
|
+
key: string;
|
|
944
|
+
allowed: boolean;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
interface KeyStats {
|
|
948
|
+
allowed: number;
|
|
949
|
+
blocked: number;
|
|
950
|
+
}
|
|
951
|
+
`;
|
|
952
|
+
}
|
|
953
|
+
//# sourceMappingURL=rate-limiting.js.map
|