express-project-builder 1.0.27 → 1.0.28

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 CHANGED
@@ -394,40 +394,27 @@ router.post(
394
394
  import { createProgressiveRateLimiter } from "../../middlewares/rateLimitingHandler";
395
395
 
396
396
  // Create rate limiter instances with different configurations
397
- const apiRateLimiter = createProgressiveRateLimiter({
398
- initialWindowMs: 60 * 1000, // 1 minute
399
- initialMax: 100, // limit each IP to 100 requests per windowMs
400
- initialBlockMs: 15 * 60 * 1000, // block for 15 minutes on first offense
397
+ const globalRateLimiter = createProgressiveRateLimiter({
398
+ windowMs: 60 * 1000, // 1 minute window
399
+ maxRequests: 200, // 200 requests per window
400
+ initialBlockMs: 15 * 60 * 1000, // 15 minutes initial block
401
+ // enableLogger: true, // Enable console logs
401
402
  message: {
402
403
  success: false,
403
404
  message: "Too many requests from this IP, please try again later.",
404
405
  },
405
- skipSuccessfulRequests: false,
406
- skipFailedRequests: false,
407
- // Custom keyGenerator to handle proxied requests
408
406
  keyGenerator: (req: Request) => {
409
- // Check for X-Forwarded-For header (common when behind proxy)
407
+ // Get real IP from proxy headers
410
408
  const forwarded = req.headers["x-forwarded-for"] as string;
411
- if (forwarded) {
412
- // If multiple IPs, take the first one (client IP)
413
- return forwarded.split(",")[0].trim();
414
- }
415
-
416
- // Fallback to other methods
417
- return (
418
- req.ip ||
419
- req.socket?.remoteAddress ||
420
- req.connection?.remoteAddress ||
421
- "unknown"
422
- );
409
+ if (forwarded) return forwarded.split(",")[0].trim();
410
+ return req.ip || req.socket?.remoteAddress || "unknown";
423
411
  },
424
412
  });
425
-
426
413
  // Apply rate limiter to a specific route
427
- router.get("/products", apiRateLimiter, ProductControllers.getAllProducts);
414
+ router.get("/products", globalRateLimiter, ProductControllers.getAllProducts);
428
415
 
429
416
  // Apply rate limiter globally to all API routes
430
- app.use("/v1/api/", apiRateLimiter, routers);
417
+ app.use("/v1/api/", globalRateLimiter, routers);
431
418
  ```
432
419
 
433
420
  - /src/middlewares/**validateRequest.ts** <br/>
@@ -1 +1 @@
1
- {"version":3,"file":"createAppFile.d.ts","sourceRoot":"","sources":["../../../src/lib/src/createAppFile.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,aAAa,GAAU,aAAa,MAAM,KAAG,OAAO,CAAC,IAAI,CA2IrE,CAAC"}
1
+ {"version":3,"file":"createAppFile.d.ts","sourceRoot":"","sources":["../../../src/lib/src/createAppFile.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,aAAa,GAAU,aAAa,MAAM,KAAG,OAAO,CAAC,IAAI,CAiIrE,CAAC"}
@@ -19,33 +19,23 @@ const app: Application = express();
19
19
  // Enable trust proxy (if behind proxy)
20
20
  app.enable('trust proxy');
21
21
 
22
- // Rate limiting configuration
23
- const apiRateLimiter = createProgressiveRateLimiter({
24
- initialWindowMs: 60 * 1000, // 1 minute
25
- initialMax: 100, // limit each IP to 100 requests per windowMs
26
- initialBlockMs: 15 * 60 * 1000, // block for 15 minutes on first offense
22
+ // 🧱 Create limiter instance
23
+ const globalRateLimiter = createProgressiveRateLimiter({
24
+ windowMs: 60 * 1000, // 1 minute window
25
+ maxRequests: 200, // 200 requests per window
26
+ initialBlockMs: 15 * 60 * 1000, // 15 minutes initial block
27
+ // enableLogger: true, // Enable console logs
27
28
  message: {
28
29
  success: false,
29
30
  message: 'Too many requests from this IP, please try again later.'
30
31
  },
31
- skipSuccessfulRequests: false,
32
- skipFailedRequests: false,
33
- // Add this custom keyGenerator
34
32
  keyGenerator: (req: Request) => {
35
- // Check for X-Forwarded-For header (common when behind proxy)
36
- const forwarded = req.headers['x-forwarded-for'] as string;
37
- if (forwarded) {
38
- // If multiple IPs, take the first one (client IP)
39
- return forwarded.split(',')[0].trim();
40
- }
41
-
42
- // Fallback to other methods
43
- return req.ip ||
44
- req.socket?.remoteAddress ||
45
- req.connection?.remoteAddress ||
46
- 'unknown';
33
+ // Get real IP from proxy headers
34
+ const forwarded = req.headers['x-forwarded-for'] as string
35
+ if (forwarded) return forwarded.split(',')[0].trim()
36
+ return req.ip || req.socket?.remoteAddress || 'unknown'
47
37
  }
48
- });
38
+ })
49
39
 
50
40
  // CORS configuration
51
41
  const getCorsOrigin = async (): Promise<string[]> => {
@@ -81,7 +71,7 @@ app.use(bigIntSerializer);
81
71
  // CORS configuration with dynamic origins
82
72
  app.use(
83
73
  '/v1/api',
84
- apiRateLimiter,
74
+ globalRateLimiter,
85
75
  cors({
86
76
  origin: async (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
87
77
  try {
@@ -107,7 +97,7 @@ app.use(
107
97
  }),
108
98
  );
109
99
  // API routes
110
- app.use('/v1/api', apiRateLimiter, routers);
100
+ app.use('/v1/api', globalRateLimiter, routers);
111
101
  // Home route
112
102
  const homeRoute = (req: Request, res: Response): void => {
113
103
  res.status(200).json({
@@ -118,9 +108,9 @@ const homeRoute = (req: Request, res: Response): void => {
118
108
  timestamp: new Date().toISOString()
119
109
  });
120
110
  };
121
- app.get('/', apiRateLimiter, homeRoute);
111
+ app.get('/', globalRateLimiter, homeRoute);
122
112
  // Health check endpoint
123
- app.get('/health', apiRateLimiter, (req: Request, res: Response) => {
113
+ app.get('/health', globalRateLimiter, (req: Request, res: Response) => {
124
114
  res.status(200).json({
125
115
  status: 'OK',
126
116
  timestamp: new Date().toISOString(),
@@ -1 +1 @@
1
- {"version":3,"file":"createAppFile.js","sourceRoot":"","sources":["../../../src/lib/src/createAppFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,KAAK,MAAM,OAAO,CAAC,CAAC,qBAAqB;AAEhD,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,WAAmB,EAAiB,EAAE;IACxE,IAAI,CAAC;QACH,IAAI,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgIrB,CAAC;QACE,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,QAAQ,CAAC,EAAE,WAAW,CAAC,CAAC;QACvE,mCAAmC;QACnC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC,CAAC;IAC9D,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,mCAAmC;QACnC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,4BAA4B,CAAC,EAAE,GAAG,CAAC,CAAC;QAC5D,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC,CAAC"}
1
+ {"version":3,"file":"createAppFile.js","sourceRoot":"","sources":["../../../src/lib/src/createAppFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,KAAK,MAAM,OAAO,CAAC,CAAC,qBAAqB;AAEhD,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,WAAmB,EAAiB,EAAE;IACxE,IAAI,CAAC;QACH,IAAI,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsHrB,CAAC;QACE,MAAM,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,QAAQ,CAAC,EAAE,WAAW,CAAC,CAAC;QACvE,mCAAmC;QACnC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC,CAAC;IAC9D,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,mCAAmC;QACnC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,4BAA4B,CAAC,EAAE,GAAG,CAAC,CAAC;QAC5D,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"create_RateLimiting_Handler_Guard.d.ts","sourceRoot":"","sources":["../../../../src/lib/src/middlewares/create_RateLimiting_Handler_Guard.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,eAAO,MAAM,iCAAiC,GAC5C,aAAa,MAAM,KAClB,OAAO,CAAC,IAAI,CA6Qd,CAAC"}
1
+ {"version":3,"file":"create_RateLimiting_Handler_Guard.d.ts","sourceRoot":"","sources":["../../../../src/lib/src/middlewares/create_RateLimiting_Handler_Guard.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,eAAO,MAAM,iCAAiC,GAC5C,aAAa,MAAM,KAClB,OAAO,CAAC,IAAI,CAyYd,CAAC"}
@@ -8,254 +8,378 @@ import chalk from "chalk";
8
8
  */
9
9
  export const create_RateLimiting_Handler_Guard = async (projectPath) => {
10
10
  try {
11
- const rateLimitingHandlerTemplate = `import { NextFunction, Request, Response } from 'express';
11
+ const rateLimitingHandlerTemplate = `/* eslint-disable @typescript-eslint/ban-ts-comment */
12
+ /* eslint-disable @typescript-eslint/no-explicit-any */
13
+ /* eslint-disable @typescript-eslint/no-unused-vars */
14
+ import { NextFunction, Request, Response } from 'express'
15
+
16
+ interface RateLimitOptions {
17
+ windowMs?: number
18
+ maxRequests?: number
19
+ initialBlockMs?: number
20
+ message?: string | object
21
+ skipSuccessfulRequests?: boolean
22
+ skipFailedRequests?: boolean
23
+ keyGenerator?: (req: Request) => string
24
+ enableLogger?: boolean
25
+ skip?: (req: Request) => boolean
26
+ }
27
+
28
+ interface RequestTracker {
29
+ count: number
30
+ resetTime: number
31
+ windowStart: number
32
+ }
33
+
34
+ interface BlockInfo {
35
+ unblockTime: number
36
+ blockCount: number
37
+ lastBlockDuration: number
38
+ blockHistory: Array<{
39
+ duration: number
40
+ timestamp: number
41
+ }>
42
+ }
43
+
44
+ // Symbol to mark that a request has already been processed
45
+ const RATE_LIMIT_CHECKED = Symbol('rateLimitChecked')
12
46
 
13
47
  /**
14
- * Creates a progressive rate limiter with increasing block durations
15
- * @param options Configuration options
16
- * @returns Express middleware function
48
+ * Creates a progressive rate limiter with escalating block durations
49
+ * Features:
50
+ * - IP-based or user-based rate limiting
51
+ * - Progressive blocking (15min → 1day → 7days → 30days → 1year)
52
+ * - Automatic cleanup of old entries
53
+ * - Prevents double-counting on same request
54
+ * - Standard rate limit headers
17
55
  */
18
56
  export const createProgressiveRateLimiter = (
19
- options: {
20
- initialWindowMs?: number; // Time window for counting requests
21
- initialMax?: number; // Max requests per window
22
- initialBlockMs?: number; // Initial block duration
23
- message?: string | object; // Custom message when blocked
24
- skipSuccessfulRequests?: boolean;
25
- skipFailedRequests?: boolean;
26
- keyGenerator?: (req: Request) => string;
27
- } = {}
57
+ options: RateLimitOptions = {}
28
58
  ) => {
29
- // Set default values
30
59
  const {
31
- initialWindowMs = 60 * 1000, // 1 minute by default
32
- initialMax = 15, // 15 requests by default
33
- initialBlockMs = 20 * 60 * 1000, // 20 minutes initial block
60
+ windowMs = 60 * 1000, // 1 minute
61
+ maxRequests = 15,
62
+ initialBlockMs = 20 * 60 * 1000, // 20 minutes
34
63
  message = {
35
64
  success: false,
36
65
  message: 'Too many requests. You are temporarily blocked.'
37
66
  },
38
67
  skipSuccessfulRequests = false,
39
68
  skipFailedRequests = false,
40
- keyGenerator = (req: Request) =>
41
- req.ip || req.connection.remoteAddress || 'unknown'
42
- } = options;
43
-
44
- // Store for tracking request counts and blocked IPs
45
- const requestCounts = new Map<string, { count: number; resetTime: number }>();
46
- const blockedIPs = new Map<
47
- string,
48
- {
49
- unblockTime: number;
50
- blockHistory: { duration: number; timestamp: number }[];
69
+ keyGenerator = (req: Request) => {
70
+ // Get real IP from common proxy headers
71
+ const forwarded = req.headers['x-forwarded-for'] as string
72
+ if (forwarded) return forwarded.split(',')[0].trim()
73
+ return req.ip || req.socket?.remoteAddress || 'unknown'
74
+ },
75
+ enableLogger = false,
76
+ skip = () => false
77
+ } = options
78
+
79
+ // In-memory stores
80
+ const requestTrackers = new Map<string, RequestTracker>()
81
+ const blockedClients = new Map<string, BlockInfo>()
82
+
83
+ // Logging helper
84
+ const log = (...args: any[]) => {
85
+ if (enableLogger) {
86
+ console.log('[RateLimiter]', ...args)
51
87
  }
52
- >();
88
+ }
53
89
 
54
- // Cleanup function to remove old entries
90
+ /**
91
+ * Cleanup old entries to prevent memory leaks
92
+ */
55
93
  const cleanup = () => {
56
- const now = Date.now();
57
-
58
- // Clean up request counts
59
- for (const [key, value] of requestCounts.entries()) {
60
- if (value.resetTime < now) {
61
- requestCounts.delete(key);
94
+ const now = Date.now()
95
+ let cleanedTrackers = 0
96
+ let cleanedBlocks = 0
97
+
98
+ // Clean expired request trackers
99
+ for (const [key, tracker] of requestTrackers.entries()) {
100
+ if (tracker.resetTime < now) {
101
+ requestTrackers.delete(key)
102
+ cleanedTrackers++
62
103
  }
63
104
  }
64
-
65
- // Clean up blocked IPs
66
- for (const [key, value] of blockedIPs.entries()) {
67
- if (value.unblockTime < now) {
68
- // Keep block history but remove the block
69
- blockedIPs.delete(key);
105
+
106
+ // Clean expired blocks
107
+ for (const [key, blockInfo] of blockedClients.entries()) {
108
+ if (blockInfo.unblockTime < now) {
109
+ blockedClients.delete(key)
110
+ cleanedBlocks++
70
111
  }
71
112
  }
72
- };
73
113
 
74
- // Run cleanup every minute
75
- setInterval(cleanup, 60 * 1000);
114
+ if (cleanedTrackers > 0 || cleanedBlocks > 0) {
115
+ log(
116
+ \`Cleanup: \${cleanedTrackers} trackers, \${cleanedBlocks} blocks removed\`
117
+ )
118
+ }
119
+ }
120
+
121
+ // Run cleanup every 2 minutes
122
+ const cleanupInterval = setInterval(cleanup, 2 * 60 * 1000)
76
123
 
77
- // Function to determine next block duration based on history
78
- const calculateNextBlockDuration = (
79
- blockHistory: { duration: number; timestamp: number }[]
124
+ // Cleanup on process exit
125
+ if (typeof process !== 'undefined') {
126
+ process.on('exit', () => clearInterval(cleanupInterval))
127
+ }
128
+
129
+ /**
130
+ * Calculate next block duration based on violation history
131
+ */
132
+ const calculateBlockDuration = (
133
+ blockHistory: Array<{ duration: number; timestamp: number }>
80
134
  ): number => {
81
- const now = Date.now();
82
-
83
- // Filter out old blocks (older than 30 days)
135
+ const now = Date.now()
136
+ const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000
137
+
138
+ // Only consider blocks in last 30 days
84
139
  const recentBlocks = blockHistory.filter(
85
- block => now - block.timestamp < 30 * 24 * 60 * 60 * 1000
86
- );
87
-
88
- // Count blocks by duration type
89
- const twentyMinBlocks = recentBlocks.filter(
90
- b => b.duration === initialBlockMs
91
- ).length;
92
- const oneDayBlocks = recentBlocks.filter(
93
- b => b.duration === 24 * 60 * 60 * 1000
94
- ).length;
95
- const sevenDayBlocks = recentBlocks.filter(
96
- b => b.duration === 7 * 24 * 60 * 60 * 1000
97
- ).length;
98
- const oneMonthBlocks = recentBlocks.filter(
99
- b => b.duration === 30 * 24 * 60 * 60 * 1000
100
- ).length;
101
-
102
- // Progressive blocking logic
103
- if (oneMonthBlocks >= 3) {
104
- // 1 year block
105
- return 365 * 24 * 60 * 60 * 1000;
106
- } else if (sevenDayBlocks >= 3) {
107
- // 1 month block
108
- return 30 * 24 * 60 * 60 * 1000;
109
- } else if (oneDayBlocks >= 3) {
110
- // 7 days block
111
- return 7 * 24 * 60 * 60 * 1000;
112
- } else if (twentyMinBlocks >= 5) {
113
- // 1 day block
114
- return 24 * 60 * 60 * 1000;
115
- } else {
116
- // Default 20 minutes block
117
- return initialBlockMs;
140
+ block => block.timestamp > thirtyDaysAgo
141
+ )
142
+
143
+ if (recentBlocks.length === 0) {
144
+ return initialBlockMs // First offense: initial block duration
118
145
  }
119
- };
120
146
 
121
- // Function to get block message based on duration
122
- const getBlockMessage = (duration: number): string | object => {
123
- if (typeof message === 'string') {
124
- return message;
147
+ // Count violations by severity
148
+ const blockCounts = {
149
+ initial: 0, // 15-20 minutes
150
+ day: 0, // 1 day
151
+ week: 0, // 7 days
152
+ month: 0, // 30 days
153
+ year: 0 // 1 year
125
154
  }
126
-
127
- let blockDurationText = '';
128
- if (duration >= 365 * 24 * 60 * 60 * 1000) {
129
- blockDurationText = '1 year';
130
- } else if (duration >= 30 * 24 * 60 * 60 * 1000) {
131
- blockDurationText = '1 month';
132
- } else if (duration >= 7 * 24 * 60 * 60 * 1000) {
133
- blockDurationText = '7 days';
134
- } else if (duration >= 24 * 60 * 60 * 1000) {
135
- blockDurationText = '1 day';
155
+
156
+ recentBlocks.forEach(block => {
157
+ if (block.duration >= 365 * 24 * 60 * 60 * 1000) {
158
+ blockCounts.year++
159
+ } else if (block.duration >= 30 * 24 * 60 * 60 * 1000) {
160
+ blockCounts.month++
161
+ } else if (block.duration >= 7 * 24 * 60 * 60 * 1000) {
162
+ blockCounts.week++
163
+ } else if (block.duration >= 24 * 60 * 60 * 1000) {
164
+ blockCounts.day++
165
+ } else {
166
+ blockCounts.initial++
167
+ }
168
+ })
169
+
170
+ // Progressive escalation logic
171
+ if (blockCounts.month >= 3) {
172
+ return 365 * 24 * 60 * 60 * 1000 // 1 year
173
+ } else if (blockCounts.week >= 3) {
174
+ return 30 * 24 * 60 * 60 * 1000 // 30 days
175
+ } else if (blockCounts.day >= 3) {
176
+ return 7 * 24 * 60 * 60 * 1000 // 7 days
177
+ } else if (blockCounts.initial >= 5) {
178
+ return 24 * 60 * 60 * 1000 // 1 day
136
179
  } else {
137
- blockDurationText = \`\${initialBlockMs ? initialBlockMs / 60000 : 20} minutes\`;
180
+ return initialBlockMs // Initial block duration
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Format block duration for user-friendly message
186
+ */
187
+ const formatDuration = (durationMs: number): string => {
188
+ const minutes = Math.floor(durationMs / 60000)
189
+ const hours = Math.floor(minutes / 60)
190
+ const days = Math.floor(hours / 24)
191
+
192
+ if (days >= 365) return '1 year'
193
+ if (days >= 30) return '30 days'
194
+ if (days >= 7) return '7 days'
195
+ if (days >= 1) return \`\${days} day\${days > 1 ? 's' : ''}\`
196
+ if (hours >= 1) return \`\${hours} hour\${hours > 1 ? 's' : ''}\`
197
+ return \`\${minutes} minute\${minutes > 1 ? 's' : ''}\`
198
+ }
199
+
200
+ /**
201
+ * Get block message with duration
202
+ */
203
+ const getBlockMessage = (durationMs: number): any => {
204
+ if (typeof message === 'string') {
205
+ return message
138
206
  }
139
-
207
+
208
+ const duration = formatDuration(durationMs)
140
209
  return {
141
210
  ...message,
142
- message: \`Too many requests. You are blocked for \${blockDurationText}.\`
143
- };
144
- };
211
+ message: \`Too many requests. You are blocked for \${duration}.\`,
212
+ retryAfter: Math.ceil(durationMs / 1000)
213
+ }
214
+ }
145
215
 
146
- // Helper function to set rate limit headers
147
- const setRateLimitHeaders = (
216
+ /**
217
+ * Set standard rate limit headers
218
+ */
219
+ const setHeaders = (
148
220
  res: Response,
149
221
  limit: number,
150
222
  remaining: number,
151
223
  resetTime: number,
152
224
  retryAfter?: number
153
225
  ) => {
154
- res.setHeader('X-RateLimit-Limit', limit.toString());
155
- res.setHeader('X-RateLimit-Remaining', remaining.toString());
156
- res.setHeader('X-RateLimit-Reset', Math.ceil(resetTime / 1000).toString());
157
- if (retryAfter) {
158
- res.setHeader('Retry-After', retryAfter.toString());
226
+ res.setHeader('X-RateLimit-Limit', limit.toString())
227
+ res.setHeader('X-RateLimit-Remaining', Math.max(0, remaining).toString())
228
+ res.setHeader('X-RateLimit-Reset', Math.ceil(resetTime / 1000).toString())
229
+
230
+ if (retryAfter !== undefined) {
231
+ res.setHeader('Retry-After', Math.ceil(retryAfter).toString())
159
232
  }
160
- };
233
+ }
161
234
 
235
+ /**
236
+ * Main middleware function
237
+ */
162
238
  return (req: Request, res: Response, next: NextFunction) => {
163
- const key = keyGenerator(req);
164
- const now = Date.now();
165
-
166
- // Check if IP is blocked
167
- if (blockedIPs.has(key)) {
168
- const blockInfo = blockedIPs.get(key)!;
169
- if (blockInfo.unblockTime > now) {
170
- const retryAfter = Math.ceil((blockInfo.unblockTime - now) / 1000);
171
- setRateLimitHeaders(
172
- res,
173
- initialMax,
174
- 0,
175
- blockInfo.unblockTime,
176
- retryAfter
177
- );
178
- res.status(429).json(getBlockMessage(blockInfo.unblockTime - now));
179
- return;
180
- } else {
181
- // Unblock if time has passed
182
- blockedIPs.delete(key);
183
- }
239
+ // Check if this request should be skipped
240
+ if (skip(req)) {
241
+ return next()
242
+ }
243
+
244
+ // Prevent double-counting the same request
245
+ // @ts-ignore
246
+ if (req[RATE_LIMIT_CHECKED]) {
247
+ log(\`Request already checked, skipping...\`)
248
+ return next()
249
+ }
250
+ // @ts-ignore
251
+ req[RATE_LIMIT_CHECKED] = true
252
+
253
+ const key = keyGenerator(req)
254
+ const now = Date.now()
255
+
256
+ log(\`Request from \${key}\`)
257
+
258
+ // Check if client is currently blocked
259
+ const blockInfo = blockedClients.get(key)
260
+ if (blockInfo && blockInfo.unblockTime > now) {
261
+ const remainingTime = blockInfo.unblockTime - now
262
+ const retryAfterSeconds = Math.ceil(remainingTime / 1000)
263
+
264
+ log(\`Blocked client \${key} - \${formatDuration(remainingTime)} remaining\`)
265
+
266
+ setHeaders(res, maxRequests, 0, blockInfo.unblockTime, retryAfterSeconds)
267
+ return res.status(429).json(getBlockMessage(remainingTime))
184
268
  }
185
-
186
- // Get or create request count info
187
- let countInfo = requestCounts.get(key);
188
- if (!countInfo || countInfo.resetTime < now) {
189
- // New window
190
- countInfo = {
191
- count: 1,
192
- resetTime: now + initialWindowMs
193
- };
194
- requestCounts.set(key, countInfo);
195
- // Set headers for new window
196
- setRateLimitHeaders(res, initialMax, initialMax - 1, countInfo.resetTime);
197
- next();
198
- return;
269
+
270
+ // Client is no longer blocked, remove from blocked list
271
+ if (blockInfo) {
272
+ blockedClients.delete(key)
273
+ log(\`Unblocked client \${key}\`)
199
274
  }
200
-
201
- // Increment count
202
- countInfo.count++;
203
-
204
- // Set headers for existing window
205
- setRateLimitHeaders(
206
- res,
207
- initialMax,
208
- Math.max(0, initialMax - countInfo.count),
209
- countInfo.resetTime
210
- );
211
-
212
- // Check if we should skip counting this request
213
- const isSuccess = res.statusCode < 400;
214
- if (
215
- (skipSuccessfulRequests && isSuccess) ||
216
- (skipFailedRequests && !isSuccess)
217
- ) {
218
- next();
219
- return;
275
+
276
+ // Get or initialize request tracker
277
+ let tracker = requestTrackers.get(key)
278
+
279
+ if (!tracker || tracker.resetTime <= now) {
280
+ // Start new tracking window
281
+ tracker = {
282
+ count: 0,
283
+ resetTime: now + windowMs,
284
+ windowStart: now
285
+ }
286
+ requestTrackers.set(key, tracker)
287
+ log(\`New window for \${key}\`)
220
288
  }
221
-
289
+
290
+ // Increment request count
291
+ tracker.count++
292
+ const remaining = maxRequests - tracker.count
293
+
294
+ log(\`Client \${key}: \${tracker.count}/\${maxRequests} requests\`)
295
+
296
+ // Set rate limit headers
297
+ setHeaders(res, maxRequests, remaining, tracker.resetTime)
298
+
222
299
  // Check if limit exceeded
223
- if (countInfo.count > initialMax) {
224
- // Get existing block history or create new one
225
- let blockHistory: { duration: number; timestamp: number }[] = [];
226
- if (blockedIPs.has(key)) {
227
- blockHistory = blockedIPs.get(key)!.blockHistory;
228
- }
229
-
230
- // Calculate next block duration based on history
231
- const blockDuration = calculateNextBlockDuration(blockHistory);
232
-
233
- // Add this block to history
300
+ if (tracker.count > maxRequests) {
301
+ // Get or initialize block history
302
+ const existingBlock = blockedClients.get(key)
303
+ const blockHistory = existingBlock?.blockHistory || []
304
+
305
+ // Calculate new block duration
306
+ const blockDuration = calculateBlockDuration(blockHistory)
307
+ const unblockTime = now + blockDuration
308
+
309
+ // Add this violation to history
234
310
  blockHistory.push({
235
311
  duration: blockDuration,
236
312
  timestamp: now
237
- });
238
-
239
- // Block the IP
240
- const unblockTime = now + blockDuration;
241
- blockedIPs.set(key, {
313
+ })
314
+
315
+ // Keep only last 30 days of history
316
+ const filteredHistory = blockHistory.filter(
317
+ block => now - block.timestamp < 30 * 24 * 60 * 60 * 1000
318
+ )
319
+
320
+ // Block the client
321
+ blockedClients.set(key, {
242
322
  unblockTime,
243
- blockHistory
244
- });
245
-
246
- // Remove from request counts to save memory
247
- requestCounts.delete(key);
248
-
249
- // Set headers for blocked IP
250
- const retryAfter = Math.ceil(blockDuration / 1000);
251
- setRateLimitHeaders(res, initialMax, 0, unblockTime, retryAfter);
252
- res.status(429).json(getBlockMessage(blockDuration));
253
- return;
323
+ blockCount: (existingBlock?.blockCount || 0) + 1,
324
+ lastBlockDuration: blockDuration,
325
+ blockHistory: filteredHistory
326
+ })
327
+
328
+ // Remove from active trackers
329
+ requestTrackers.delete(key)
330
+
331
+ const retryAfterSeconds = Math.ceil(blockDuration / 1000)
332
+
333
+ log(
334
+ \`BLOCKED \${key} for \${formatDuration(blockDuration)} (violation #\${filteredHistory.length})\`
335
+ )
336
+
337
+ setHeaders(res, maxRequests, 0, unblockTime, retryAfterSeconds)
338
+ return res.status(429).json(getBlockMessage(blockDuration))
339
+ }
340
+
341
+ // Allow request to proceed
342
+ next()
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Create user-specific rate limiter (requires authentication middleware first)
348
+ */
349
+ export const createUserRateLimiter = (options: RateLimitOptions = {}) => {
350
+ return createProgressiveRateLimiter({
351
+ ...options,
352
+ keyGenerator: (req: Request) => {
353
+ // @ts-ignore - assuming req.user exists from auth middleware
354
+ const userId = req.user?.id || req.user?._id
355
+ if (userId) {
356
+ return \`user:\${userId}\`
357
+ }
358
+ // Fallback to IP if no user
359
+ const forwarded = req.headers['x-forwarded-for'] as string
360
+ if (forwarded) return \`ip:\${forwarded.split(',')[0].trim()}\`
361
+ return \`ip:\${req.ip || req.socket?.remoteAddress || 'unknown'}\`
362
+ }
363
+ })
364
+ }
365
+
366
+ /**
367
+ * Create endpoint-specific rate limiter
368
+ */
369
+ export const createEndpointRateLimiter = (
370
+ endpoint: string,
371
+ options: RateLimitOptions = {}
372
+ ) => {
373
+ return createProgressiveRateLimiter({
374
+ ...options,
375
+ keyGenerator: (req: Request) => {
376
+ const baseKey = options.keyGenerator
377
+ ? options.keyGenerator(req)
378
+ : req.ip || 'unknown'
379
+ return \`\${endpoint}:\${baseKey}\`
254
380
  }
255
-
256
- next();
257
- };
258
- };
381
+ })
382
+ }
259
383
  `;
260
384
  await createFile(path.join(projectPath, "src/app/middlewares", "rateLimitingHandler.ts"), rateLimitingHandlerTemplate);
261
385
  // Success message with green checkmark and text
@@ -1 +1 @@
1
- {"version":3,"file":"create_RateLimiting_Handler_Guard.js","sourceRoot":"","sources":["../../../../src/lib/src/middlewares/create_RateLimiting_Handler_Guard.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;;;GAIG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAG,KAAK,EACpD,WAAmB,EACJ,EAAE;IACjB,IAAI,CAAC;QACH,MAAM,2BAA2B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwPvC,CAAC;QAEE,MAAM,UAAU,CACd,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,qBAAqB,EAAE,wBAAwB,CAAC,EACvE,2BAA2B,CAC5B,CAAC;QAEF,gDAAgD;QAChD,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,KAAK,CAAC,mDAAmD,CAAC,CACjE,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,wCAAwC;QACxC,OAAO,CAAC,KAAK,CACX,KAAK,CAAC,GAAG,CAAC,8CAA8C,CAAC,EACzD,GAAG,CACJ,CAAC;QACF,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC,CAAC"}
1
+ {"version":3,"file":"create_RateLimiting_Handler_Guard.js","sourceRoot":"","sources":["../../../../src/lib/src/middlewares/create_RateLimiting_Handler_Guard.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;;;GAIG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAG,KAAK,EACpD,WAAmB,EACJ,EAAE;IACjB,IAAI,CAAC;QACH,MAAM,2BAA2B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoXvC,CAAC;QAEE,MAAM,UAAU,CACd,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,qBAAqB,EAAE,wBAAwB,CAAC,EACvE,2BAA2B,CAC5B,CAAC;QAEF,gDAAgD;QAChD,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,KAAK,CAAC,mDAAmD,CAAC,CACjE,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,wCAAwC;QACxC,OAAO,CAAC,KAAK,CACX,KAAK,CAAC,GAAG,CAAC,8CAA8C,CAAC,EACzD,GAAG,CACJ,CAAC;QACF,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-project-builder",
3
- "version": "1.0.27",
3
+ "version": "1.0.28",
4
4
  "description": "A powerful and professional Express.js project generator CLI that instantly scaffolds a production-ready backend with TypeScript, modular architecture, and built-in support for MongoDB (Mongoose) or PostgreSQL (Prisma). Includes authentication, error handling, rate limiting, file upload, caching, and utility functions—so you can focus on building features instead of boilerplate. Perfect for kickstarting your next Express.js API project with best practices and modern tools.",
5
5
  "type": "module",
6
6
  "main": "dist/bin/index.js",