vako 1.3.0
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/CHANGELOG.md +63 -0
- package/README.md +1944 -0
- package/bin/commands/quick-setup.js +111 -0
- package/bin/commands/setup-executor.js +203 -0
- package/bin/commands/setup.js +737 -0
- package/bin/create-veko-app.js +75 -0
- package/bin/veko-update.js +205 -0
- package/bin/veko.js +188 -0
- package/error/error.ejs +382 -0
- package/index.js +36 -0
- package/lib/adapters/nextjs-adapter.js +241 -0
- package/lib/app.js +749 -0
- package/lib/core/auth-manager.js +1353 -0
- package/lib/core/auto-updater.js +1118 -0
- package/lib/core/logger.js +97 -0
- package/lib/core/module-installer.js +86 -0
- package/lib/dev/dev-server.js +292 -0
- package/lib/layout/layout-manager.js +834 -0
- package/lib/plugin-manager.js +1795 -0
- package/lib/routing/route-manager.js +1000 -0
- package/package.json +231 -0
- package/templates/public/css/style.css +2 -0
- package/templates/public/js/main.js +1 -0
- package/tsconfig.json +50 -0
- package/types/index.d.ts +238 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const validator = require('validator');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
class RouteManager {
|
|
7
|
+
constructor(app, options) {
|
|
8
|
+
this.app = app;
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.routeMap = new Map();
|
|
11
|
+
this.dynamicRoutes = new Map();
|
|
12
|
+
|
|
13
|
+
// Limite le nombre de routes dynamiques pour éviter les attaques
|
|
14
|
+
this.maxDynamicRoutes = options.maxDynamicRoutes || 1000;
|
|
15
|
+
this.rateLimitCache = new Map();
|
|
16
|
+
this.allowedMethods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'];
|
|
17
|
+
|
|
18
|
+
// Cache pour optimisation
|
|
19
|
+
this.methodsCache = new Map();
|
|
20
|
+
this.pathValidationCache = new Map();
|
|
21
|
+
|
|
22
|
+
// Configuration de sécurité
|
|
23
|
+
this.securityConfig = {
|
|
24
|
+
maxParamLength: options.maxParamLength || 1000,
|
|
25
|
+
maxPathLength: options.maxPathLength || 500,
|
|
26
|
+
rateLimitWindow: options.rateLimitWindow || 60000,
|
|
27
|
+
rateLimitMax: options.rateLimitMax || 100,
|
|
28
|
+
enableSecurityHeaders: options.enableSecurityHeaders !== false,
|
|
29
|
+
contentSecurityPolicy: options.contentSecurityPolicy || "default-src 'self'"
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Démarrer le nettoyage périodique
|
|
33
|
+
this.startCacheCleanup();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validation sécurisée et optimisée des paramètres
|
|
37
|
+
validateRouteInput(method, path, handler) {
|
|
38
|
+
// Cache key pour éviter les revalidations
|
|
39
|
+
const cacheKey = `${method}:${path}:${typeof handler}`;
|
|
40
|
+
if (this.pathValidationCache.has(cacheKey)) {
|
|
41
|
+
const cached = this.pathValidationCache.get(cacheKey);
|
|
42
|
+
return { ...cached, handler }; // Handler peut changer
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Validation de la méthode HTTP
|
|
46
|
+
if (!method || typeof method !== 'string') {
|
|
47
|
+
throw new Error('Méthode HTTP invalide');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const normalizedMethod = method.toLowerCase();
|
|
51
|
+
if (!this.allowedMethods.includes(normalizedMethod)) {
|
|
52
|
+
throw new Error(`Méthode HTTP non autorisée: ${method}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validation du chemin
|
|
56
|
+
if (!path || typeof path !== 'string') {
|
|
57
|
+
throw new Error('Chemin de route invalide');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validation contre les attaques par traversée de chemin et injections
|
|
61
|
+
if (this.containsDangerousPatterns(path)) {
|
|
62
|
+
throw new Error('Chemin de route contient des caractères dangereux');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Limite la longueur du chemin
|
|
66
|
+
if (path.length > this.securityConfig.maxPathLength) {
|
|
67
|
+
throw new Error(`Chemin de route trop long (max: ${this.securityConfig.maxPathLength})`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validation du handler
|
|
71
|
+
if (!handler || (typeof handler !== 'function' && !Array.isArray(handler))) {
|
|
72
|
+
throw new Error('Handler de route invalide');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(handler)) {
|
|
76
|
+
handler.forEach((h, index) => {
|
|
77
|
+
if (typeof h !== 'function') {
|
|
78
|
+
throw new Error(`Handler ${index} n'est pas une fonction`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = { method: normalizedMethod, path: this.sanitizePath(path) };
|
|
84
|
+
|
|
85
|
+
// Cache le résultat (sans le handler)
|
|
86
|
+
this.pathValidationCache.set(cacheKey, result);
|
|
87
|
+
|
|
88
|
+
return { ...result, handler };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Détection optimisée de patterns dangereux
|
|
92
|
+
containsDangerousPatterns(path) {
|
|
93
|
+
const dangerousPatterns = [
|
|
94
|
+
/\.\./, // Path traversal
|
|
95
|
+
/[\\]/, // Backslashes
|
|
96
|
+
/[<>\"'`]/, // HTML/JS injection
|
|
97
|
+
/javascript:/i, // JavaScript protocol
|
|
98
|
+
/data:/i, // Data URLs
|
|
99
|
+
/vbscript:/i, // VBScript
|
|
100
|
+
/on\w+=/i, // Event handlers
|
|
101
|
+
/eval\s*\(/i, // eval calls
|
|
102
|
+
/expression\s*\(/i, // CSS expressions
|
|
103
|
+
/url\s*\(/i // CSS URLs
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
return dangerousPatterns.some(pattern => pattern.test(path));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Nettoyage sécurisé et optimisé du chemin
|
|
110
|
+
sanitizePath(routePath) {
|
|
111
|
+
// Normalise et nettoie le chemin
|
|
112
|
+
let cleaned = routePath.trim();
|
|
113
|
+
|
|
114
|
+
// Supprime les caractères de contrôle
|
|
115
|
+
cleaned = cleaned.replace(/[\x00-\x1f\x7f-\x9f]/g, '');
|
|
116
|
+
|
|
117
|
+
// Décode les entités HTML/URL
|
|
118
|
+
try {
|
|
119
|
+
cleaned = decodeURIComponent(cleaned);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// Si le décodage échoue, utiliser la chaîne originale
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Assure que le chemin commence par /
|
|
125
|
+
if (!cleaned.startsWith('/')) {
|
|
126
|
+
cleaned = '/' + cleaned;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Normalise les slashes multiples
|
|
130
|
+
cleaned = cleaned.replace(/\/+/g, '/');
|
|
131
|
+
|
|
132
|
+
// Retire les trailing slashes sauf pour la racine
|
|
133
|
+
if (cleaned.length > 1 && cleaned.endsWith('/')) {
|
|
134
|
+
cleaned = cleaned.slice(0, -1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return cleaned;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Rate limiting amélioré avec sliding window
|
|
141
|
+
checkRateLimit(clientId = 'default', customLimits = {}) {
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
const windowMs = customLimits.window || this.securityConfig.rateLimitWindow;
|
|
144
|
+
const maxRequests = customLimits.max || this.securityConfig.rateLimitMax;
|
|
145
|
+
|
|
146
|
+
if (!this.rateLimitCache.has(clientId)) {
|
|
147
|
+
this.rateLimitCache.set(clientId, {
|
|
148
|
+
count: 1,
|
|
149
|
+
resetTime: now + windowMs,
|
|
150
|
+
requests: [now]
|
|
151
|
+
});
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const limit = this.rateLimitCache.get(clientId);
|
|
156
|
+
|
|
157
|
+
// Nettoyage des anciennes requêtes (sliding window)
|
|
158
|
+
limit.requests = limit.requests.filter(time => time > now - windowMs);
|
|
159
|
+
|
|
160
|
+
if (limit.requests.length >= maxRequests) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
limit.requests.push(now);
|
|
165
|
+
limit.count = limit.requests.length;
|
|
166
|
+
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Méthode async corrigée
|
|
171
|
+
async createRoute(method, path, handler, options = {}) {
|
|
172
|
+
try {
|
|
173
|
+
// Validation sécurisée des entrées
|
|
174
|
+
const validated = this.validateRouteInput(method, path, handler);
|
|
175
|
+
method = validated.method;
|
|
176
|
+
path = validated.path;
|
|
177
|
+
handler = validated.handler;
|
|
178
|
+
|
|
179
|
+
// Vérifie les limites de création de routes
|
|
180
|
+
if (this.dynamicRoutes.size >= this.maxDynamicRoutes) {
|
|
181
|
+
throw new Error('Limite de routes dynamiques atteinte');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Rate limiting pour la création de routes
|
|
185
|
+
const clientId = options.clientId || 'route-creation';
|
|
186
|
+
if (!this.checkRateLimit(clientId)) {
|
|
187
|
+
throw new Error('Trop de tentatives de création de routes');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Hook de sécurité avant création
|
|
191
|
+
if (this.app.plugins) {
|
|
192
|
+
await this.app.plugins.executeHook('route:security-check', method, path, handler, options);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (this.routeExists(method, path)) {
|
|
196
|
+
this.app.logger.log('warning', 'Route already exists', `${method.toUpperCase()} ${path}`);
|
|
197
|
+
return this.app;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Wrapper sécurisé pour le handler
|
|
201
|
+
const secureHandler = this.createSecureHandler(handler, method, path, options);
|
|
202
|
+
|
|
203
|
+
if (Array.isArray(secureHandler)) {
|
|
204
|
+
this.app.app[method](path, ...secureHandler);
|
|
205
|
+
} else {
|
|
206
|
+
this.app.app[method](path, secureHandler);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const routeKey = `${method}:${path}`;
|
|
210
|
+
this.dynamicRoutes.set(routeKey, {
|
|
211
|
+
method,
|
|
212
|
+
path,
|
|
213
|
+
handler: secureHandler,
|
|
214
|
+
options: this.sanitizeOptions(options),
|
|
215
|
+
createdAt: new Date().toISOString(),
|
|
216
|
+
createdBy: options.createdBy || 'system',
|
|
217
|
+
routeId: this.generateRouteId(method, path)
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
this.app.logger.log('create', 'Route created dynamically', `${method.toUpperCase()} ${path}`);
|
|
221
|
+
|
|
222
|
+
if (this.app.plugins) {
|
|
223
|
+
await this.app.plugins.executeHook('route:created', method, path, secureHandler, options);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (this.app.options.isDev && this.app.devServer) {
|
|
227
|
+
this.app.devServer.broadcast({
|
|
228
|
+
type: 'route-created',
|
|
229
|
+
method: method.toUpperCase(),
|
|
230
|
+
path,
|
|
231
|
+
timestamp: new Date().toISOString()
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return this.app;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
this.app.logger.log('error', 'Error creating route', `${method?.toUpperCase()} ${path} → ${error.message}`);
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Crée un wrapper sécurisé amélioré pour les handlers
|
|
243
|
+
createSecureHandler(handler, method, path, options = {}) {
|
|
244
|
+
const wrapHandler = (originalHandler) => {
|
|
245
|
+
return async (req, res, next) => {
|
|
246
|
+
const startTime = Date.now();
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// Headers de sécurité configurables
|
|
250
|
+
if (this.securityConfig.enableSecurityHeaders) {
|
|
251
|
+
this.setSecurityHeaders(res);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Validation des paramètres d'entrée
|
|
255
|
+
this.validateRequestInput(req);
|
|
256
|
+
|
|
257
|
+
// Rate limiting par route si configuré
|
|
258
|
+
if (options.rateLimit) {
|
|
259
|
+
const clientKey = `${req.ip}:${method}:${path}`;
|
|
260
|
+
if (!this.checkRateLimit(clientKey, options.rateLimit)) {
|
|
261
|
+
return res.status(429).json({ error: 'Trop de requêtes' });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Log de sécurité avec plus de détails
|
|
266
|
+
this.app.logger.log('security', 'Route accessed',
|
|
267
|
+
`${method.toUpperCase()} ${path} from ${req.ip} (${req.get('User-Agent') || 'Unknown'})`
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Exécute le handler original avec timeout
|
|
271
|
+
const timeoutMs = options.timeout || 30000;
|
|
272
|
+
const handlerPromise = Promise.resolve(originalHandler(req, res, next));
|
|
273
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
274
|
+
setTimeout(() => reject(new Error('Handler timeout')), timeoutMs)
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
await Promise.race([handlerPromise, timeoutPromise]);
|
|
278
|
+
|
|
279
|
+
// Log de performance
|
|
280
|
+
const duration = Date.now() - startTime;
|
|
281
|
+
if (duration > 1000) {
|
|
282
|
+
this.app.logger.log('performance', 'Slow route',
|
|
283
|
+
`${method.toUpperCase()} ${path} took ${duration}ms`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
} catch (error) {
|
|
288
|
+
const duration = Date.now() - startTime;
|
|
289
|
+
|
|
290
|
+
this.app.logger.log('error', 'Handler error',
|
|
291
|
+
`${method.toUpperCase()} ${path} → ${error.message} (${duration}ms)`
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Réponse d'erreur sécurisée
|
|
295
|
+
if (!res.headersSent) {
|
|
296
|
+
this.sendSecureErrorResponse(res, error);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (Array.isArray(handler)) {
|
|
303
|
+
return handler.map(h => wrapHandler(h));
|
|
304
|
+
} else {
|
|
305
|
+
return wrapHandler(handler);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Headers de sécurité configurables
|
|
310
|
+
setSecurityHeaders(res) {
|
|
311
|
+
const headers = {
|
|
312
|
+
'X-Content-Type-Options': 'nosniff',
|
|
313
|
+
'X-Frame-Options': 'DENY',
|
|
314
|
+
'X-XSS-Protection': '1; mode=block',
|
|
315
|
+
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
|
|
316
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
317
|
+
'Content-Security-Policy': this.securityConfig.contentSecurityPolicy
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
Object.entries(headers).forEach(([name, value]) => {
|
|
321
|
+
if (!res.getHeader(name)) {
|
|
322
|
+
res.setHeader(name, value);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Réponse d'erreur sécurisée
|
|
328
|
+
sendSecureErrorResponse(res, error) {
|
|
329
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
330
|
+
const errorId = this.generateErrorId();
|
|
331
|
+
|
|
332
|
+
// Log l'erreur avec un ID unique
|
|
333
|
+
this.app.logger.log('error', 'Request error', `ID: ${errorId} - ${error.message}`);
|
|
334
|
+
|
|
335
|
+
const response = {
|
|
336
|
+
error: 'Une erreur est survenue',
|
|
337
|
+
errorId,
|
|
338
|
+
timestamp: new Date().toISOString()
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
if (isDev) {
|
|
342
|
+
response.details = error.message;
|
|
343
|
+
response.stack = error.stack;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const statusCode = error.statusCode || error.status || 500;
|
|
347
|
+
res.status(statusCode).json(response);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Validation améliorée des données de requête
|
|
351
|
+
validateRequestInput(req) {
|
|
352
|
+
const maxParamLength = this.securityConfig.maxParamLength;
|
|
353
|
+
|
|
354
|
+
// Validation avec limite de profondeur pour éviter les attaques par récursion
|
|
355
|
+
if (req.body) {
|
|
356
|
+
this.validateObject(req.body, 'body', maxParamLength, 0, 5);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (req.query) {
|
|
360
|
+
this.validateObject(req.query, 'query', maxParamLength, 0, 3);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (req.params) {
|
|
364
|
+
this.validateObject(req.params, 'params', maxParamLength, 0, 2);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Validation des headers sensibles
|
|
368
|
+
this.validateHeaders(req);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Validation récursive avec protection contre les attaques
|
|
372
|
+
validateObject(obj, type, maxLength, depth = 0, maxDepth = 5) {
|
|
373
|
+
if (depth > maxDepth) {
|
|
374
|
+
throw new Error(`Objet ${type} trop profond`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const keys = Object.keys(obj);
|
|
378
|
+
if (keys.length > 100) {
|
|
379
|
+
throw new Error(`Trop de propriétés dans ${type}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
383
|
+
// Validation de la clé
|
|
384
|
+
if (key.length > 100) {
|
|
385
|
+
throw new Error(`Nom de propriété ${type}.${key} trop long`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (typeof value === 'string') {
|
|
389
|
+
if (value.length > maxLength) {
|
|
390
|
+
throw new Error(`Paramètre ${type}.${key} trop long`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Détection améliorée de tentatives d'injection
|
|
394
|
+
if (this.containsDangerousPatterns(value)) {
|
|
395
|
+
throw new Error(`Paramètre ${type}.${key} contient du contenu suspect`);
|
|
396
|
+
}
|
|
397
|
+
} else if (value && typeof value === 'object') {
|
|
398
|
+
this.validateObject(value, `${type}.${key}`, maxLength, depth + 1, maxDepth);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Validation des headers
|
|
404
|
+
validateHeaders(req) {
|
|
405
|
+
const dangerousHeaders = ['x-forwarded-host', 'x-original-url', 'x-rewrite-url'];
|
|
406
|
+
|
|
407
|
+
dangerousHeaders.forEach(header => {
|
|
408
|
+
const value = req.get(header);
|
|
409
|
+
if (value && this.containsDangerousPatterns(value)) {
|
|
410
|
+
this.app.logger.log('security', 'Dangerous header detected', `${header}: ${value}`);
|
|
411
|
+
throw new Error('En-tête de requête suspect');
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Génération d'ID unique pour les routes
|
|
417
|
+
generateRouteId(method, path) {
|
|
418
|
+
return crypto.createHash('md5').update(`${method}:${path}:${Date.now()}`).digest('hex').substring(0, 8);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Génération d'ID unique pour les erreurs
|
|
422
|
+
generateErrorId() {
|
|
423
|
+
return crypto.randomBytes(4).toString('hex').toUpperCase();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Nettoyage des options amélioré
|
|
427
|
+
sanitizeOptions(options) {
|
|
428
|
+
const sanitized = {};
|
|
429
|
+
const allowedKeys = [
|
|
430
|
+
'description', 'middleware', 'rateLimit', 'auth', 'createdBy',
|
|
431
|
+
'timeout', 'clientId', 'security', 'cache'
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
allowedKeys.forEach(key => {
|
|
435
|
+
if (options[key] !== undefined) {
|
|
436
|
+
// Validation spécifique par type d'option
|
|
437
|
+
switch (key) {
|
|
438
|
+
case 'timeout':
|
|
439
|
+
sanitized[key] = Math.min(Math.max(parseInt(options[key]) || 30000, 1000), 300000);
|
|
440
|
+
break;
|
|
441
|
+
case 'rateLimit':
|
|
442
|
+
if (typeof options[key] === 'object') {
|
|
443
|
+
sanitized[key] = {
|
|
444
|
+
window: Math.min(options[key].window || 60000, 3600000),
|
|
445
|
+
max: Math.min(options[key].max || 100, 10000)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
default:
|
|
450
|
+
sanitized[key] = options[key];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
return sanitized;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Nettoyage périodique optimisé
|
|
459
|
+
startCacheCleanup() {
|
|
460
|
+
// Nettoyage toutes les 5 minutes
|
|
461
|
+
setInterval(() => {
|
|
462
|
+
this.cleanupCache();
|
|
463
|
+
}, 300000);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
cleanupCache() {
|
|
467
|
+
const now = Date.now();
|
|
468
|
+
let cleaned = 0;
|
|
469
|
+
|
|
470
|
+
// Nettoyage du cache rate limiting
|
|
471
|
+
if (this.rateLimitCache) {
|
|
472
|
+
for (const [key, value] of this.rateLimitCache.entries()) {
|
|
473
|
+
if (value.resetTime && now > value.resetTime) {
|
|
474
|
+
this.rateLimitCache.delete(key);
|
|
475
|
+
cleaned++;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Nettoyage du cache de validation des chemins
|
|
481
|
+
if (this.pathValidationCache && this.pathValidationCache.size > 1000) {
|
|
482
|
+
this.pathValidationCache.clear();
|
|
483
|
+
cleaned += 1000;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Nettoyage du cache des méthodes
|
|
487
|
+
if (this.methodsCache && this.methodsCache.size > 1000) {
|
|
488
|
+
this.methodsCache.clear();
|
|
489
|
+
cleaned += 1000;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (cleaned > 0) {
|
|
493
|
+
this.app.logger.log('maintenance', 'Cache cleaned', `${cleaned} entries removed`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async createRoute(method, path, handler, options = {}) {
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
// Validation sécurisée des entrées
|
|
501
|
+
const validated = this.validateRouteInput(method, path, handler);
|
|
502
|
+
method = validated.method;
|
|
503
|
+
path = validated.path;
|
|
504
|
+
handler = validated.handler;
|
|
505
|
+
|
|
506
|
+
// Vérifie les limites de création de routes
|
|
507
|
+
if (this.dynamicRoutes.size >= this.maxDynamicRoutes) {
|
|
508
|
+
throw new Error('Limite de routes dynamiques atteinte');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Rate limiting pour la création de routes
|
|
512
|
+
if (!this.checkRateLimit()) {
|
|
513
|
+
throw new Error('Trop de tentatives de création de routes');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Hook de sécurité avant création
|
|
517
|
+
if (this.app.plugins) {
|
|
518
|
+
await this.app.plugins.executeHook('route:security-check', method, path, handler, options);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (this.routeExists(method, path)) {
|
|
522
|
+
this.app.logger.log('warning', 'Route already exists', `${method.toUpperCase()} ${path}`);
|
|
523
|
+
return this.app;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Wrapper sécurisé pour le handler
|
|
527
|
+
const secureHandler = this.createSecureHandler(handler, method, path);
|
|
528
|
+
|
|
529
|
+
if (Array.isArray(secureHandler)) {
|
|
530
|
+
this.app.app[method](path, ...secureHandler);
|
|
531
|
+
} else {
|
|
532
|
+
this.app.app[method](path, secureHandler);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const routeKey = `${method}:${path}`;
|
|
536
|
+
this.dynamicRoutes.set(routeKey, {
|
|
537
|
+
method,
|
|
538
|
+
path,
|
|
539
|
+
handler: secureHandler,
|
|
540
|
+
options: this.sanitizeOptions(options),
|
|
541
|
+
createdAt: new Date().toISOString(),
|
|
542
|
+
createdBy: options.createdBy || 'system'
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
this.app.logger.log('create', 'Route created dynamically', `${method.toUpperCase()} ${path}`);
|
|
546
|
+
|
|
547
|
+
if (this.app.plugins) {
|
|
548
|
+
await this.app.plugins.executeHook('route:created', method, path, secureHandler, options);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (this.app.options.isDev && this.app.devServer) {
|
|
552
|
+
this.app.devServer.broadcast({
|
|
553
|
+
type: 'route-created',
|
|
554
|
+
method: method.toUpperCase(),
|
|
555
|
+
path,
|
|
556
|
+
timestamp: new Date().toISOString()
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return this.app;
|
|
561
|
+
} catch (error) {
|
|
562
|
+
this.app.logger.log('error', 'Error creating route', `${method?.toUpperCase()} ${path} → ${error.message}`);
|
|
563
|
+
throw error; // Re-throw pour une meilleure gestion d'erreur
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Crée un wrapper sécurisé pour les handlers
|
|
568
|
+
createSecureHandler(handler, method, path) {
|
|
569
|
+
const wrapHandler = (originalHandler) => {
|
|
570
|
+
return async (req, res, next) => {
|
|
571
|
+
try {
|
|
572
|
+
// Ajoute des headers de sécurité
|
|
573
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
574
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
575
|
+
|
|
576
|
+
// Validation des paramètres d'entrée
|
|
577
|
+
this.validateRequestInput(req);
|
|
578
|
+
|
|
579
|
+
// Log de sécurité
|
|
580
|
+
this.app.logger.log('security', 'Route accessed', `${method.toUpperCase()} ${path} from ${req.ip}`);
|
|
581
|
+
|
|
582
|
+
// Exécute le handler original
|
|
583
|
+
await originalHandler(req, res, next);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
this.app.logger.log('error', 'Handler error', `${method.toUpperCase()} ${path} → ${error.message}`);
|
|
586
|
+
|
|
587
|
+
// Ne pas exposer les détails d'erreur en production
|
|
588
|
+
if (process.env.NODE_ENV === 'production') {
|
|
589
|
+
res.status(500).json({ error: 'Erreur serveur interne' });
|
|
590
|
+
} else {
|
|
591
|
+
res.status(500).json({ error: error.message, stack: error.stack });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
if (Array.isArray(handler)) {
|
|
598
|
+
return handler.map(h => wrapHandler(h));
|
|
599
|
+
} else {
|
|
600
|
+
return wrapHandler(handler);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Validation des données de requête
|
|
605
|
+
validateRequestInput(req) {
|
|
606
|
+
// Limite la taille des paramètres
|
|
607
|
+
const maxParamLength = 1000;
|
|
608
|
+
|
|
609
|
+
if (req.body) {
|
|
610
|
+
this.validateObject(req.body, 'body', maxParamLength);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (req.query) {
|
|
614
|
+
this.validateObject(req.query, 'query', maxParamLength);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (req.params) {
|
|
618
|
+
this.validateObject(req.params, 'params', maxParamLength);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
validateObject(obj, type, maxLength) {
|
|
623
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
624
|
+
if (typeof value === 'string') {
|
|
625
|
+
if (value.length > maxLength) {
|
|
626
|
+
throw new Error(`Paramètre ${type}.${key} trop long`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Détection de tentatives d'injection
|
|
630
|
+
if (/[<>\"'`]|javascript:|data:|vbscript:|onload|onerror/i.test(value)) {
|
|
631
|
+
throw new Error(`Paramètre ${type}.${key} contient du contenu suspect`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Nettoyage des options
|
|
638
|
+
sanitizeOptions(options) {
|
|
639
|
+
const sanitized = {};
|
|
640
|
+
|
|
641
|
+
const allowedKeys = ['description', 'middleware', 'rateLimit', 'auth', 'createdBy'];
|
|
642
|
+
|
|
643
|
+
for (const key of allowedKeys) {
|
|
644
|
+
if (options[key] !== undefined) {
|
|
645
|
+
sanitized[key] = options[key];
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return sanitized;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async deleteRoute(method, path) {
|
|
653
|
+
try {
|
|
654
|
+
method = method.toLowerCase();
|
|
655
|
+
const routeKey = `${method}:${path}`;
|
|
656
|
+
|
|
657
|
+
if (!this.dynamicRoutes.has(routeKey) && !this.routeExists(method, path)) {
|
|
658
|
+
this.app.logger.log('warning', 'Route not found', `${method.toUpperCase()} ${path}`);
|
|
659
|
+
return this.app;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
this.removeRouteFromRouter(method, path);
|
|
663
|
+
this.dynamicRoutes.delete(routeKey);
|
|
664
|
+
|
|
665
|
+
this.app.logger.log('delete', 'Route deleted dynamically', `${method.toUpperCase()} ${path}`);
|
|
666
|
+
|
|
667
|
+
if (this.app.options.isDev && this.app.devServer) {
|
|
668
|
+
this.app.devServer.broadcast({
|
|
669
|
+
type: 'route-deleted',
|
|
670
|
+
method: method.toUpperCase(),
|
|
671
|
+
path,
|
|
672
|
+
timestamp: new Date().toISOString()
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return this.app;
|
|
677
|
+
} catch (error) {
|
|
678
|
+
this.app.logger.log('error', 'Error deleting route', `${method?.toUpperCase()} ${path} → ${error.message}`);
|
|
679
|
+
return this.app;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async updateRoute(method, path, newHandler) {
|
|
684
|
+
try {
|
|
685
|
+
await this.deleteRoute(method, path);
|
|
686
|
+
await this.createRoute(method, path, newHandler);
|
|
687
|
+
|
|
688
|
+
this.app.logger.log('reload', 'Route updated', `${method.toUpperCase()} ${path}`);
|
|
689
|
+
return this.app;
|
|
690
|
+
} catch (error) {
|
|
691
|
+
this.app.logger.log('error', 'Error updating route', `${method?.toUpperCase()} ${path} → ${error.message}`);
|
|
692
|
+
return this.app;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
routeExists(method, path) {
|
|
697
|
+
if (!this.app.app._router) return false;
|
|
698
|
+
|
|
699
|
+
return this.app.app._router.stack.some(layer => {
|
|
700
|
+
if (layer.route) {
|
|
701
|
+
const routeMethods = Object.keys(layer.route.methods);
|
|
702
|
+
return layer.route.path === path && routeMethods.includes(method.toLowerCase());
|
|
703
|
+
}
|
|
704
|
+
return false;
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
removeRouteFromRouter(method, path) {
|
|
709
|
+
if (!this.app.app._router) return;
|
|
710
|
+
|
|
711
|
+
this.app.app._router.stack = this.app.app._router.stack.filter(layer => {
|
|
712
|
+
if (layer.route) {
|
|
713
|
+
const routeMethods = Object.keys(layer.route.methods);
|
|
714
|
+
const shouldRemove = layer.route.path === path && routeMethods.includes(method.toLowerCase());
|
|
715
|
+
|
|
716
|
+
if (shouldRemove) {
|
|
717
|
+
this.app.logger.log('dev', 'Route removed from Express router', `🗑️ ${method.toUpperCase()} ${path}`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return !shouldRemove;
|
|
721
|
+
}
|
|
722
|
+
return true;
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
loadRoutes(routesDir = this.options.routesDir) {
|
|
727
|
+
const routesPath = path.join(process.cwd(), routesDir);
|
|
728
|
+
|
|
729
|
+
if (!fs.existsSync(routesPath)) {
|
|
730
|
+
this.app.logger.log('warning', 'Routes directory not found', `📁 ${routesDir}`);
|
|
731
|
+
this.createRoutesDirectory(routesPath);
|
|
732
|
+
return this.app;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
this.app.logger.log('info', 'Scanning routes...', `📂 ${routesDir}`);
|
|
736
|
+
this.scanDirectory(routesPath, routesPath);
|
|
737
|
+
|
|
738
|
+
// Vérifier si la route / existe
|
|
739
|
+
if (!this.routeExists('get', '/')) {
|
|
740
|
+
this.app.logger.log('warning', 'No root route found', 'Create routes/index.js to define the home page');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return this.app;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
scanDirectory(dirPath, basePath) {
|
|
747
|
+
const files = fs.readdirSync(dirPath);
|
|
748
|
+
|
|
749
|
+
files.forEach(file => {
|
|
750
|
+
const filePath = path.join(dirPath, file);
|
|
751
|
+
const stat = fs.statSync(filePath);
|
|
752
|
+
|
|
753
|
+
if (stat.isDirectory()) {
|
|
754
|
+
this.scanDirectory(filePath, basePath);
|
|
755
|
+
} else if (file.endsWith('.js')) {
|
|
756
|
+
this.loadRouteFile(filePath, basePath);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
loadRouteFile(filePath, basePath) {
|
|
762
|
+
try {
|
|
763
|
+
// Validation du chemin de fichier pour éviter les attaques par traversée
|
|
764
|
+
const resolvedPath = path.resolve(filePath);
|
|
765
|
+
const resolvedBase = path.resolve(basePath);
|
|
766
|
+
|
|
767
|
+
if (!resolvedPath.startsWith(resolvedBase)) {
|
|
768
|
+
throw new Error('Tentative d\'accès à un fichier en dehors du répertoire autorisé');
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Vérifie l'extension du fichier
|
|
772
|
+
if (!filePath.endsWith('.js')) {
|
|
773
|
+
throw new Error('Seuls les fichiers .js sont autorisés');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
delete require.cache[require.resolve(filePath)];
|
|
777
|
+
const routeModule = require(filePath);
|
|
778
|
+
|
|
779
|
+
const relativePath = path.relative(basePath, filePath);
|
|
780
|
+
const routePath = this.filePathToRoute(relativePath);
|
|
781
|
+
|
|
782
|
+
this.routeMap.set(filePath, routePath);
|
|
783
|
+
|
|
784
|
+
if (typeof routeModule === 'function') {
|
|
785
|
+
// Wrapper sécurisé pour les fonctions de route
|
|
786
|
+
const secureModule = (app) => {
|
|
787
|
+
try {
|
|
788
|
+
routeModule(app);
|
|
789
|
+
} catch (error) {
|
|
790
|
+
this.app.logger.log('error', 'Route module error', error.message);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
secureModule(this.app.app);
|
|
794
|
+
} else if (routeModule.router) {
|
|
795
|
+
this.app.app.use(routePath, routeModule.router);
|
|
796
|
+
} else if (routeModule.get || routeModule.post || routeModule.put || routeModule.delete || routeModule.patch) {
|
|
797
|
+
this.setupRouteHandlers(routePath, routeModule);
|
|
798
|
+
} else {
|
|
799
|
+
this.app.logger.log('warning', 'Invalid route module', `${path.basename(filePath)} - No valid exports found`);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const fileName = path.basename(filePath);
|
|
804
|
+
this.app.logger.log('route', 'Route loaded', `${fileName} → ${routePath}`);
|
|
805
|
+
} catch (error) {
|
|
806
|
+
const fileName = path.basename(filePath);
|
|
807
|
+
this.app.logger.log('error', 'Failed to load', `${fileName} → ${error.message}`);
|
|
808
|
+
// En production, ne pas arrêter l'application pour un fichier de route défaillant
|
|
809
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
createRoutesDirectory(routesPath) {
|
|
816
|
+
try {
|
|
817
|
+
// Créer le dossier routes
|
|
818
|
+
fs.mkdirSync(routesPath, { recursive: true });
|
|
819
|
+
this.app.logger.log('create', 'Routes directory created', `📁 ${path.relative(process.cwd(), routesPath)}`);
|
|
820
|
+
|
|
821
|
+
// Créer le fichier index.js avec une route par défaut
|
|
822
|
+
const indexPath = path.join(routesPath, 'index.js');
|
|
823
|
+
const defaultIndexContent = `// Route principale de l'application
|
|
824
|
+
module.exports = {
|
|
825
|
+
get: (req, res) => {
|
|
826
|
+
res.render('index', {
|
|
827
|
+
title: 'Veko.js - Ultra modern framework',
|
|
828
|
+
message: 'Welcome to Veko.js! 🚀',
|
|
829
|
+
description: 'Your application is running successfully.'
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
};`;
|
|
833
|
+
|
|
834
|
+
fs.writeFileSync(indexPath, defaultIndexContent, 'utf8');
|
|
835
|
+
this.app.logger.log('create', 'Default index route created', `📄 ${path.relative(process.cwd(), indexPath)}`);
|
|
836
|
+
|
|
837
|
+
// Créer également une vue index.ejs par défaut si elle n'existe pas
|
|
838
|
+
this.createDefaultIndexView();
|
|
839
|
+
|
|
840
|
+
} catch (error) {
|
|
841
|
+
this.app.logger.log('error', 'Error creating routes directory', error.message);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
createDefaultIndexView() {
|
|
846
|
+
const viewsPath = path.join(process.cwd(), this.app.options.viewsDir);
|
|
847
|
+
const indexViewPath = path.join(viewsPath, 'index.ejs');
|
|
848
|
+
|
|
849
|
+
if (!fs.existsSync(indexViewPath)) {
|
|
850
|
+
// Créer le dossier views s'il n'existe pas
|
|
851
|
+
if (!fs.existsSync(viewsPath)) {
|
|
852
|
+
fs.mkdirSync(viewsPath, { recursive: true });
|
|
853
|
+
this.app.logger.log('create', 'Views directory created', `📁 ${path.relative(process.cwd(), viewsPath)}`);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const defaultViewContent = `<% layout.css = ['/css/home.css'] %>
|
|
857
|
+
<% layout.js = ['/js/home.js'] %>
|
|
858
|
+
|
|
859
|
+
<div class="hero">
|
|
860
|
+
<div class="hero-content">
|
|
861
|
+
<h1><%= title %></h1>
|
|
862
|
+
<p class="lead"><%= message %></p>
|
|
863
|
+
<p><%= description %></p>
|
|
864
|
+
|
|
865
|
+
<div class="features">
|
|
866
|
+
<div class="feature">
|
|
867
|
+
<h3>🚀 Ultra Rapide</h3>
|
|
868
|
+
<p>Framework optimisé pour les performances</p>
|
|
869
|
+
</div>
|
|
870
|
+
<div class="feature">
|
|
871
|
+
<h3>🔥 Hot Reload</h3>
|
|
872
|
+
<p>Rechargement automatique en développement</p>
|
|
873
|
+
</div>
|
|
874
|
+
<div class="feature">
|
|
875
|
+
<h3>🎨 Layouts</h3>
|
|
876
|
+
<p>Système de mise en page intégré</p>
|
|
877
|
+
</div>
|
|
878
|
+
<div class="feature">
|
|
879
|
+
<h3>🔌 Plugins</h3>
|
|
880
|
+
<p>Architecture extensible avec plugins</p>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
|
|
884
|
+
<div class="actions">
|
|
885
|
+
<a href="/docs" class="btn btn-primary">Documentation</a>
|
|
886
|
+
<a href="/examples" class="btn btn-secondary">Exemples</a>
|
|
887
|
+
</div>
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
|
|
891
|
+
<% layout.section('scripts', \`
|
|
892
|
+
<script>
|
|
893
|
+
console.log('🎉 Veko.js app loaded successfully!');
|
|
894
|
+
</script>
|
|
895
|
+
\`) %>`;
|
|
896
|
+
|
|
897
|
+
fs.writeFileSync(indexViewPath, defaultViewContent, 'utf8');
|
|
898
|
+
this.app.logger.log('create', 'Default index view created', `📄 ${path.relative(process.cwd(), indexViewPath)}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
filePathToRoute(filePath) {
|
|
903
|
+
let route = filePath
|
|
904
|
+
.replace(/\\/g, '/')
|
|
905
|
+
.replace(/\.js$/, '')
|
|
906
|
+
.replace(/\/index$/, '')
|
|
907
|
+
.replace(/\[([^\]]+)\]/g, ':$1');
|
|
908
|
+
|
|
909
|
+
if (!route.startsWith('/')) {
|
|
910
|
+
route = '/' + route;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Si le fichier est index.js à la racine, la route devient '/'
|
|
914
|
+
if (route === '' || route === '/') {
|
|
915
|
+
route = '/';
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return route;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
setupRouteHandlers(routePath, handlers) {
|
|
922
|
+
if (handlers.get) this.app.app.get(routePath, handlers.get);
|
|
923
|
+
if (handlers.post) this.app.app.post(routePath, handlers.post);
|
|
924
|
+
if (handlers.put) this.app.app.put(routePath, handlers.put);
|
|
925
|
+
if (handlers.delete) this.app.app.delete(routePath, handlers.delete);
|
|
926
|
+
if (handlers.patch) this.app.app.patch(routePath, handlers.patch);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
listRoutes() {
|
|
930
|
+
const routes = [];
|
|
931
|
+
|
|
932
|
+
this.routeMap.forEach((routePath, filePath) => {
|
|
933
|
+
routes.push({
|
|
934
|
+
type: 'file',
|
|
935
|
+
path: routePath,
|
|
936
|
+
source: path.relative(process.cwd(), filePath),
|
|
937
|
+
methods: this.getRouteMethods(routePath)
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
this.dynamicRoutes.forEach((routeInfo, routeKey) => {
|
|
942
|
+
routes.push({
|
|
943
|
+
type: 'dynamic',
|
|
944
|
+
path: routeInfo.path,
|
|
945
|
+
method: routeInfo.method.toUpperCase(),
|
|
946
|
+
createdAt: routeInfo.createdAt
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
return routes;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Optimisation: cache des méthodes de route
|
|
954
|
+
getRouteMethods(routePath) {
|
|
955
|
+
const cacheKey = `methods:${routePath}`;
|
|
956
|
+
|
|
957
|
+
if (this.methodsCache && this.methodsCache.has(cacheKey)) {
|
|
958
|
+
return this.methodsCache.get(cacheKey);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (!this.app.app._router) return [];
|
|
962
|
+
|
|
963
|
+
const methods = new Set();
|
|
964
|
+
this.app.app._router.stack.forEach(layer => {
|
|
965
|
+
if (layer.route && layer.route.path === routePath) {
|
|
966
|
+
Object.keys(layer.route.methods).forEach(method => {
|
|
967
|
+
methods.add(method.toUpperCase());
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
const result = Array.from(methods);
|
|
973
|
+
|
|
974
|
+
// Cache le résultat
|
|
975
|
+
if (!this.methodsCache) {
|
|
976
|
+
this.methodsCache = new Map();
|
|
977
|
+
}
|
|
978
|
+
this.methodsCache.set(cacheKey, result);
|
|
979
|
+
|
|
980
|
+
return result;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Nettoyage périodique du cache
|
|
984
|
+
cleanupCache() {
|
|
985
|
+
if (this.rateLimitCache) {
|
|
986
|
+
const now = Date.now();
|
|
987
|
+
for (const [key, value] of this.rateLimitCache.entries()) {
|
|
988
|
+
if (now > value.resetTime) {
|
|
989
|
+
this.rateLimitCache.delete(key);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (this.methodsCache && this.methodsCache.size > 1000) {
|
|
995
|
+
this.methodsCache.clear();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
module.exports = RouteManager;
|