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 +10 -23
- package/dist/lib/src/createAppFile.d.ts.map +1 -1
- package/dist/lib/src/createAppFile.js +15 -25
- package/dist/lib/src/createAppFile.js.map +1 -1
- package/dist/lib/src/middlewares/create_RateLimiting_Handler_Guard.d.ts.map +1 -1
- package/dist/lib/src/middlewares/create_RateLimiting_Handler_Guard.js +323 -199
- package/dist/lib/src/middlewares/create_RateLimiting_Handler_Guard.js.map +1 -1
- package/package.json +1 -1
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
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
initialBlockMs: 15 * 60 * 1000, //
|
|
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
|
-
//
|
|
407
|
+
// Get real IP from proxy headers
|
|
410
408
|
const forwarded = req.headers["x-forwarded-for"] as string;
|
|
411
|
-
if (forwarded)
|
|
412
|
-
|
|
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",
|
|
414
|
+
router.get("/products", globalRateLimiter, ProductControllers.getAllProducts);
|
|
428
415
|
|
|
429
416
|
// Apply rate limiter globally to all API routes
|
|
430
|
-
app.use("/v1/api/",
|
|
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,
|
|
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
|
-
//
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
initialBlockMs: 15 * 60 * 1000, //
|
|
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
|
-
//
|
|
36
|
-
const forwarded = req.headers['x-forwarded-for'] as string
|
|
37
|
-
if (forwarded)
|
|
38
|
-
|
|
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
|
-
|
|
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',
|
|
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('/',
|
|
111
|
+
app.get('/', globalRateLimiter, homeRoute);
|
|
122
112
|
// Health check endpoint
|
|
123
|
-
app.get('/health',
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
initialBlockMs = 20 * 60 * 1000, // 20 minutes
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Cleanup old entries to prevent memory leaks
|
|
92
|
+
*/
|
|
55
93
|
const cleanup = () => {
|
|
56
|
-
const now = Date.now()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
66
|
-
for (const [key,
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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 =>
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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 \${
|
|
143
|
-
|
|
144
|
-
|
|
211
|
+
message: \`Too many requests. You are blocked for \${duration}.\`,
|
|
212
|
+
retryAfter: Math.ceil(durationMs / 1000)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
145
215
|
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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 (
|
|
224
|
-
// Get
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
//
|
|
240
|
-
const
|
|
241
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|