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/lib/app.js ADDED
@@ -0,0 +1,749 @@
1
+ const express = require('express');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const helmet = require('helmet'); // Sécurité headers
5
+ const rateLimit = require('express-rate-limit'); // Protection DDoS
6
+ const validator = require('validator'); // Validation d'entrées
7
+
8
+ const ModuleInstaller = require('./core/module-installer');
9
+ const Logger = require('./core/logger');
10
+ const LayoutManager = require('./layout/layout-manager');
11
+ const RouteManager = require('./routing/route-manager');
12
+ const DevServer = require('./dev/dev-server');
13
+ const PluginManager = require('./plugin-manager');
14
+ const AuthManager = require('./core/auth-manager');
15
+
16
+ // Vérification de l'existence de l'auto-updater de manière sécurisée
17
+ let AutoUpdater = null;
18
+ try {
19
+ AutoUpdater = require('./core/auto-updater');
20
+ } catch (error) {
21
+ // L'auto-updater n'est pas disponible, mais l'application peut continuer
22
+ console.warn('Auto-updater non disponible:', error.message);
23
+ }
24
+
25
+ class App {
26
+ constructor(options = {}) {
27
+ // Validation des options d'entrée
28
+ this.validateOptions(options);
29
+
30
+ // Configuration par défaut
31
+ this.options = {
32
+ port: this.sanitizePort(options.port) || 3000,
33
+ wsPort: this.sanitizePort(options.wsPort) || 3008,
34
+ viewsDir: this.sanitizePath(options.viewsDir) || 'views',
35
+ staticDir: this.sanitizePath(options.staticDir) || 'public',
36
+ routesDir: this.sanitizePath(options.routesDir) || 'routes',
37
+ isDev: Boolean(options.isDev),
38
+ watchDirs: this.sanitizePaths(options.watchDirs) || ['views', 'routes', 'public'],
39
+ errorLog: this.sanitizePath(options.errorLog) || 'error.log',
40
+ showStack: process.env.NODE_ENV !== 'production' && Boolean(options.showStack),
41
+ autoInstall: Boolean(options.autoInstall ?? true),
42
+ // Configuration sécurisée par défaut
43
+ security: {
44
+ helmet: true,
45
+ rateLimit: {
46
+ windowMs: 15 * 60 * 1000, // 15 minutes
47
+ max: 100, // limit each IP to 100 requests per windowMs
48
+ message: 'Trop de requêtes, veuillez réessayer plus tard.'
49
+ },
50
+ cors: {
51
+ origin: process.env.ALLOWED_ORIGINS?.split(',') || false,
52
+ credentials: true
53
+ },
54
+ ...options.security
55
+ },
56
+ layouts: {
57
+ enabled: true,
58
+ layoutsDir: this.sanitizePath(options.layouts?.layoutsDir) || 'views/layouts',
59
+ defaultLayout: this.sanitizeString(options.layouts?.defaultLayout) || 'main',
60
+ extension: this.sanitizeString(options.layouts?.extension) || '.ejs',
61
+ sections: this.sanitizeArray(options.layouts?.sections) || ['head', 'header', 'content', 'footer', 'scripts'],
62
+ cache: process.env.NODE_ENV === 'production',
63
+ ...options.layouts
64
+ },
65
+ plugins: {
66
+ enabled: Boolean(options.plugins?.enabled ?? true),
67
+ autoLoad: Boolean(options.plugins?.autoLoad ?? true),
68
+ pluginsDir: this.sanitizePath(options.plugins?.pluginsDir) || 'plugins',
69
+ whitelist: options.plugins?.whitelist || [], // Plugins autorisés
70
+ ...options.plugins
71
+ },
72
+ prefetch: {
73
+ enabled: Boolean(options.prefetch?.enabled ?? true),
74
+ maxConcurrent: Math.min(Math.max(1, options.prefetch?.maxConcurrent || 3), 10),
75
+ notifyUser: Boolean(options.prefetch?.notifyUser ?? true),
76
+ cacheRoutes: Boolean(options.prefetch?.cacheRoutes ?? true),
77
+ prefetchDelay: Math.max(100, options.prefetch?.prefetchDelay || 1000),
78
+ ...options.prefetch
79
+ },
80
+ // Configuration de l'auto-updater
81
+ autoUpdater: {
82
+ enabled: Boolean(options.autoUpdater?.enabled ?? true) && AutoUpdater !== null,
83
+ checkOnStart: Boolean(options.autoUpdater?.checkOnStart ?? true),
84
+ autoUpdate: Boolean(options.autoUpdater?.autoUpdate ?? false),
85
+ updateChannel: options.autoUpdater?.updateChannel || 'stable',
86
+ securityUpdates: Boolean(options.autoUpdater?.securityUpdates ?? true),
87
+ showNotifications: Boolean(options.autoUpdater?.showNotifications ?? true),
88
+ backupCount: Math.max(1, options.autoUpdater?.backupCount || 5),
89
+ checkInterval: Math.max(300000, options.autoUpdater?.checkInterval || 3600000), // min 5 min
90
+ ...options.autoUpdater
91
+ }
92
+ };
93
+
94
+ this.app = express();
95
+ this.express = this.app;
96
+
97
+ // Initialize components
98
+ this.logger = new Logger();
99
+ this.layoutManager = new LayoutManager(this, this.options.layouts);
100
+ this.routeManager = new RouteManager(this, this.options);
101
+
102
+ // Système d'authentification
103
+ this.auth = new AuthManager(this);
104
+
105
+ // Système d'auto-updater (si disponible)
106
+ if (this.options.autoUpdater.enabled && AutoUpdater) {
107
+ this.autoUpdater = AutoUpdater;
108
+ this.autoUpdaterActive = false;
109
+ }
110
+
111
+ if (this.options.isDev) {
112
+ this.devServer = new DevServer(this, this.options);
113
+ }
114
+
115
+ if (this.options.plugins.enabled) {
116
+ this.plugins = new PluginManager(this, this.options.plugins);
117
+ }
118
+
119
+ this.init();
120
+ }
121
+
122
+ async ensureModules() {
123
+ if (this.options.autoInstall !== false) {
124
+ try {
125
+ await ModuleInstaller.checkAndInstall();
126
+ ModuleInstaller.createPackageJsonIfNeeded();
127
+ } catch (error) {
128
+ this.logger.log('error', 'Erreur lors de la vérification des modules', error.message);
129
+ }
130
+ }
131
+ }
132
+
133
+ async installModule(moduleName, version = 'latest') {
134
+ return await ModuleInstaller.installModule(moduleName, version);
135
+ }
136
+
137
+ log(type, message, details = '') {
138
+ this.logger.log(type, message, details);
139
+ }
140
+
141
+ // 🚀 Initialisation de l'auto-updater non bloquante avec meilleure gestion des erreurs
142
+ async initAutoUpdater() {
143
+ if (!this.options.autoUpdater.enabled || !this.autoUpdater) return;
144
+
145
+ try {
146
+ this.log('info', 'Initialisation de l\'auto-updater', '🔄');
147
+
148
+ // Tester si l'auto-updater a les méthodes nécessaires
149
+ if (typeof this.autoUpdater.init !== 'function') {
150
+ throw new Error('Module auto-updater invalide ou incomplet');
151
+ }
152
+
153
+ // Configure l'auto-updater avec les options de l'app
154
+ this.autoUpdater.config = {
155
+ ...this.autoUpdater.defaultConfig || {},
156
+ autoCheck: this.options.autoUpdater.checkOnStart,
157
+ autoUpdate: this.options.autoUpdater.autoUpdate,
158
+ updateChannel: this.options.autoUpdater.updateChannel,
159
+ securityCheck: this.options.autoUpdater.securityUpdates,
160
+ notifications: this.options.autoUpdater.showNotifications,
161
+ backupCount: this.options.autoUpdater.backupCount,
162
+ checkInterval: this.options.autoUpdater.checkInterval
163
+ };
164
+
165
+ // Initialiser l'auto-updater de manière non bloquante
166
+ this.autoUpdater.init().then(() => {
167
+ this.autoUpdaterActive = true;
168
+ this.log('success', 'Auto-updater initialisé', '✅');
169
+
170
+ // Vérification initiale si demandée, mais sans bloquer
171
+ if (this.options.autoUpdater.checkOnStart) {
172
+ // Utiliser setTimeout pour garantir que la vérification n'est pas bloquante
173
+ setTimeout(() => {
174
+ this.checkForUpdates(true).catch(err => {
175
+ this.log('error', 'Erreur lors de la vérification initiale', err.message);
176
+ });
177
+ }, 2000); // Délai pour permettre au serveur de démarrer d'abord
178
+ }
179
+ }).catch(error => {
180
+ this.log('error', 'Erreur lors de l\'initialisation de l\'auto-updater', error.message);
181
+ this.autoUpdaterActive = false;
182
+ });
183
+
184
+ } catch (error) {
185
+ // Capturer les erreurs mais ne pas bloquer l'application
186
+ this.log('error', 'Erreur lors de la configuration de l\'auto-updater', error.message);
187
+ this.autoUpdaterActive = false;
188
+ }
189
+ }
190
+
191
+ // 🔍 Vérification des mises à jour avec gestion d'erreurs améliorée
192
+ async checkForUpdates(silent = false) {
193
+ if (!this.autoUpdaterActive) return null;
194
+
195
+ try {
196
+ const updateInfo = await Promise.race([
197
+ this.autoUpdater.checkForUpdates(silent),
198
+ // Timeout après 5 secondes pour ne pas bloquer
199
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout lors de la vérification')), 5000))
200
+ ]);
201
+
202
+ if (updateInfo.hasUpdate && !silent) {
203
+ this.log('warning', 'Mise à jour disponible', `${updateInfo.currentVersion} → ${updateInfo.latestVersion}`);
204
+
205
+ // Notification pour les mises à jour de sécurité
206
+ if (updateInfo.security) {
207
+ this.log('error', 'MISE À JOUR DE SÉCURITÉ CRITIQUE', '🔒 Mise à jour fortement recommandée');
208
+
209
+ // Mise à jour automatique pour les correctifs de sécurité si activée
210
+ if (this.options.autoUpdater.securityUpdates && this.options.autoUpdater.autoUpdate) {
211
+ this.log('info', 'Mise à jour de sécurité automatique', '🚀 Démarrage...');
212
+ this.performUpdate(updateInfo).catch(err => {
213
+ this.log('error', 'Échec de la mise à jour de sécurité', err.message);
214
+ });
215
+ }
216
+ }
217
+
218
+ // Mise à jour automatique normale si activée
219
+ if (this.options.autoUpdater.autoUpdate && !updateInfo.security) {
220
+ this.log('info', 'Mise à jour automatique', '🚀 Démarrage...');
221
+ this.performUpdate(updateInfo).catch(err => {
222
+ this.log('error', 'Échec de la mise à jour automatique', err.message);
223
+ });
224
+ }
225
+ } else if (updateInfo.needsInstall && !silent) {
226
+ this.log('warning', 'Veko non installé correctement', '⚠️ Réinstallation requise');
227
+ } else if (!updateInfo.hasUpdate && !silent) {
228
+ this.log('success', 'Veko à jour', `✅ Version ${updateInfo.currentVersion || 'inconnue'}`);
229
+ }
230
+
231
+ return updateInfo;
232
+
233
+ } catch (error) {
234
+ // Log l'erreur mais continue l'exécution
235
+ this.log('error', 'Erreur lors de la vérification des mises à jour', error.message);
236
+ return { hasUpdate: false, error: error.message };
237
+ }
238
+ }
239
+
240
+ // 🚀 Exécution de la mise à jour
241
+ async performUpdate(updateInfo) {
242
+ if (!this.autoUpdaterActive) {
243
+ throw new Error('Auto-updater non actif');
244
+ }
245
+
246
+ try {
247
+ this.log('info', 'Début de la mise à jour', `🚀 ${updateInfo.latestVersion}`);
248
+
249
+ // Hook avant mise à jour
250
+ if (this.plugins) {
251
+ await this.plugins.executeHook('app:before-update', this, updateInfo);
252
+ }
253
+
254
+ const success = await this.autoUpdater.performUpdate(updateInfo);
255
+
256
+ if (success) {
257
+ this.log('success', 'Mise à jour terminée', '✅ Redémarrage requis');
258
+
259
+ // Hook après mise à jour réussie
260
+ if (this.plugins) {
261
+ await this.plugins.executeHook('app:after-update', this, updateInfo);
262
+ }
263
+
264
+ // Notification optionnelle de redémarrage
265
+ if (this.options.autoUpdater.showNotifications) {
266
+ console.log('\n' + '═'.repeat(60));
267
+ console.log('\x1b[32m\x1b[1m🎉 MISE À JOUR VEKO RÉUSSIE!\x1b[0m');
268
+ console.log('\x1b[33m⚠️ Redémarrez l\'application pour appliquer les changements\x1b[0m');
269
+ console.log('═'.repeat(60) + '\n');
270
+ }
271
+
272
+ return true;
273
+ } else {
274
+ this.log('error', 'Échec de la mise à jour', '❌');
275
+ return false;
276
+ }
277
+
278
+ } catch (error) {
279
+ this.log('error', 'Erreur durant la mise à jour', error.message);
280
+
281
+ // Hook en cas d'erreur
282
+ if (this.plugins) {
283
+ await this.plugins.executeHook('app:update-error', this, error);
284
+ }
285
+
286
+ return false;
287
+ }
288
+ }
289
+
290
+ // 🔄 Rollback vers une version précédente
291
+ async rollbackUpdate(backupPath = null) {
292
+ if (!this.autoUpdaterActive) {
293
+ throw new Error('Auto-updater non actif');
294
+ }
295
+
296
+ try {
297
+ this.log('info', 'Début du rollback', '🔄');
298
+
299
+ const success = await this.autoUpdater.rollback(backupPath);
300
+
301
+ if (success) {
302
+ this.log('success', 'Rollback terminé', '✅');
303
+ return true;
304
+ } else {
305
+ this.log('error', 'Échec du rollback', '❌');
306
+ return false;
307
+ }
308
+
309
+ } catch (error) {
310
+ this.log('error', 'Erreur durant le rollback', error.message);
311
+ return false;
312
+ }
313
+ }
314
+
315
+ // 📊 Informations sur l'auto-updater
316
+ getAutoUpdaterInfo() {
317
+ if (!this.autoUpdaterActive) {
318
+ return { active: false, message: 'Auto-updater désactivé' };
319
+ }
320
+
321
+ return {
322
+ active: true,
323
+ currentVersion: this.autoUpdater.getCurrentVersion(),
324
+ config: this.autoUpdater.config,
325
+ stats: this.autoUpdater.stats
326
+ };
327
+ }
328
+
329
+ // 📋 Route d'administration pour l'auto-updater
330
+ setupAutoUpdaterRoutes() {
331
+ if (!this.autoUpdaterActive) return;
332
+
333
+ // Route pour vérifier les mises à jour
334
+ this.app.get('/_veko/updates/check', async (req, res) => {
335
+ try {
336
+ const updateInfo = await this.checkForUpdates(true);
337
+ res.json(updateInfo);
338
+ } catch (error) {
339
+ res.status(500).json({ error: error.message });
340
+ }
341
+ });
342
+
343
+ // Route pour déclencher une mise à jour
344
+ this.app.post('/_veko/updates/perform', async (req, res) => {
345
+ try {
346
+ const updateInfo = await this.checkForUpdates(true);
347
+ if (updateInfo.hasUpdate) {
348
+ const success = await this.performUpdate(updateInfo);
349
+ res.json({ success, updateInfo });
350
+ } else {
351
+ res.json({ success: false, message: 'Aucune mise à jour disponible' });
352
+ }
353
+ } catch (error) {
354
+ res.status(500).json({ error: error.message });
355
+ }
356
+ });
357
+
358
+ // Route pour les statistiques
359
+ this.app.get('/_veko/updates/stats', (req, res) => {
360
+ res.json(this.getAutoUpdaterInfo());
361
+ });
362
+
363
+ // Route pour effectuer un rollback
364
+ this.app.post('/_veko/updates/rollback', async (req, res) => {
365
+ try {
366
+ const { backupPath } = req.body;
367
+ const success = await this.rollbackUpdate(backupPath);
368
+ res.json({ success });
369
+ } catch (error) {
370
+ res.status(500).json({ error: error.message });
371
+ }
372
+ });
373
+
374
+ this.log('info', 'Routes auto-updater configurées', '🔗 /_veko/updates/*');
375
+ }
376
+
377
+ async init() {
378
+ this.setupExpress();
379
+
380
+ // Initialisation asynchrone et non bloquante de l'auto-updater
381
+ this.initAutoUpdater().catch(err => {
382
+ // L'erreur est déjà enregistrée dans la méthode initAutoUpdater
383
+ });
384
+
385
+ if (this.plugins) {
386
+ this.plugins.executeHook('app:init', this);
387
+ }
388
+
389
+ if (this.options.isDev) {
390
+ this.devServer.setup();
391
+ }
392
+
393
+ // Configuration des routes d'administration seulement si l'auto-updater est activé
394
+ if (this.autoUpdaterActive) {
395
+ this.setupAutoUpdaterRoutes();
396
+ }
397
+ }
398
+
399
+ setupExpress() {
400
+ // Configuration sécurisée des headers
401
+ if (this.options.security.helmet) {
402
+ this.app.use(helmet({
403
+ contentSecurityPolicy: {
404
+ directives: {
405
+ defaultSrc: ["'self'"],
406
+ styleSrc: ["'self'", "'unsafe-inline'"],
407
+ scriptSrc: ["'self'"],
408
+ imgSrc: ["'self'", "data:", "https:"],
409
+ },
410
+ },
411
+ hsts: process.env.NODE_ENV === 'production'
412
+ }));
413
+ }
414
+
415
+ // Rate limiting
416
+ if (this.options.security.rateLimit) {
417
+ const limiter = rateLimit(this.options.security.rateLimit);
418
+ this.app.use(limiter);
419
+ }
420
+
421
+ this.app.set('view engine', 'ejs');
422
+ this.app.set('views', [
423
+ path.join(process.cwd(), this.options.viewsDir),
424
+ path.join(process.cwd(), this.options.layouts.layoutsDir),
425
+ path.join(__dirname, '..', 'views'),
426
+ path.join(__dirname, '..', 'error')
427
+ ]);
428
+
429
+ // Configuration sécurisée du parsing
430
+ this.app.use(express.json({
431
+ limit: '10mb',
432
+ verify: (req, res, buf) => {
433
+ // Vérification de la taille et du contenu
434
+ if (buf.length > 10485760) { // 10MB
435
+ throw new Error('Payload trop volumineux');
436
+ }
437
+ }
438
+ }));
439
+
440
+ this.app.use(express.urlencoded({
441
+ extended: true,
442
+ limit: '10mb',
443
+ parameterLimit: 100
444
+ }));
445
+
446
+ // Serveur de fichiers statiques sécurisé
447
+ const staticDir = this.options.staticDir;
448
+ this.app.use(express.static(path.join(process.cwd(), staticDir), {
449
+ dotfiles: 'deny',
450
+ index: false,
451
+ maxAge: process.env.NODE_ENV === 'production' ? '1d' : 0
452
+ }));
453
+
454
+ // Middleware de sécurité personnalisé
455
+ this.app.use(this.securityMiddleware());
456
+
457
+ if (this.options.layouts?.enabled) {
458
+ this.app.use(this.layoutManager.middleware());
459
+ }
460
+
461
+ if (this.options.isDev) {
462
+ this.app.use(this.devServer.middleware());
463
+ }
464
+
465
+ this.logger.log('success', 'Express configuration initialized', '⚡ Ready to start');
466
+ }
467
+
468
+ // Middleware de sécurité personnalisé
469
+ securityMiddleware() {
470
+ return (req, res, next) => {
471
+ // Protection XSS
472
+ res.setHeader('X-XSS-Protection', '1; mode=block');
473
+
474
+ // Masquer les informations du serveur
475
+ res.removeHeader('X-Powered-By');
476
+
477
+ // Validation des headers
478
+ const suspiciousHeaders = ['x-forwarded-host', 'x-real-ip'];
479
+ for (const header of suspiciousHeaders) {
480
+ if (req.headers[header] && !this.isValidHeader(req.headers[header])) {
481
+ return res.status(400).json({ error: 'En-tête suspect détecté' });
482
+ }
483
+ }
484
+
485
+ next();
486
+ };
487
+ }
488
+
489
+ isValidHeader(value) {
490
+ // Validation basique des headers
491
+ return typeof value === 'string' &&
492
+ value.length < 1000 &&
493
+ !/[<>\"']/.test(value);
494
+ }
495
+
496
+ /**
497
+ * Active le système d'authentification
498
+ * @param {Object} config - Configuration de l'authentification
499
+ */
500
+ async enableAuth(config = {}) {
501
+ await this.auth.init(config);
502
+ return this;
503
+ }
504
+
505
+ /**
506
+ * Vérifie si l'authentification est activée
507
+ */
508
+ isAuthEnabled() {
509
+ return this.auth.isEnabled;
510
+ }
511
+
512
+ /**
513
+ * Middleware pour protéger une route
514
+ */
515
+ requireAuth() {
516
+ if (!this.auth.isEnabled) {
517
+ throw new Error('Le système d\'authentification n\'est pas activé');
518
+ }
519
+ return this.auth.requireAuth.bind(this.auth);
520
+ }
521
+
522
+ /**
523
+ * Middleware pour protéger une route avec un rôle spécifique
524
+ */
525
+ requireRole(role) {
526
+ if (!this.auth.isEnabled) {
527
+ throw new Error('Le système d\'authentification n\'est pas activé');
528
+ }
529
+ return this.auth.requireRole(role);
530
+ }
531
+
532
+ // Delegate route methods to RouteManager
533
+ createRoute(method, path, handler, options = {}) {
534
+ return this.routeManager.createRoute(method, path, handler, options);
535
+ }
536
+
537
+ deleteRoute(method, path) {
538
+ return this.routeManager.deleteRoute(method, path);
539
+ }
540
+
541
+ updateRoute(method, path, newHandler) {
542
+ return this.routeManager.updateRoute(method, path, newHandler);
543
+ }
544
+
545
+ loadRoutes(routesDir = this.options.routesDir) {
546
+ return this.routeManager.loadRoutes(routesDir);
547
+ }
548
+
549
+ listRoutes() {
550
+ return this.routeManager.listRoutes();
551
+ }
552
+
553
+ // Delegate layout methods to LayoutManager
554
+ createLayout(layoutName, content = null) {
555
+ return this.layoutManager.createLayout(layoutName, content);
556
+ }
557
+
558
+ deleteLayout(layoutName) {
559
+ return this.layoutManager.deleteLayout(layoutName);
560
+ }
561
+
562
+ listLayouts() {
563
+ return this.layoutManager.listLayouts();
564
+ }
565
+
566
+ use(middleware) {
567
+ this.app.use(middleware);
568
+ return this;
569
+ }
570
+
571
+ listen(port = this.options.port, callback) {
572
+ return this.app.listen(port, async () => {
573
+ console.log('\n' + '═'.repeat(60));
574
+ console.log(`\x1b[35m\x1b[1m
575
+ ╔══════════════════════════════════════════════════════╗
576
+ ║ 🚀 VEKO.JS 🚀 ║
577
+ ╚══════════════════════════════════════════════════════╝\x1b[0m`);
578
+
579
+ this.logger.log('server', 'Server started successfully', `🌐 http://localhost:${port}`);
580
+
581
+ // Affichage des informations auto-updater seulement si actif
582
+ if (this.autoUpdaterActive) {
583
+ try {
584
+ const autoUpdaterInfo = this.getAutoUpdaterInfo();
585
+ if (autoUpdaterInfo.currentVersion) {
586
+ this.logger.log('info', 'Auto-updater active', `🔄 Version Veko: ${autoUpdaterInfo.currentVersion}`);
587
+ this.logger.log('info', 'Canal de mise à jour', `📢 ${autoUpdaterInfo.config.updateChannel}`);
588
+ }
589
+
590
+ // Programme les vérifications automatiques de manière sécurisée
591
+ if (this.autoUpdater.config && this.autoUpdater.config.autoCheck) {
592
+ this.scheduleAutoUpdates();
593
+ }
594
+ } catch (err) {
595
+ this.logger.log('warn', 'Auto-updater disponible mais pas complètement initialisé', '⚠️');
596
+ }
597
+ }
598
+
599
+ if (this.options.isDev) {
600
+ this.logger.log('dev', 'Development mode active', `🔥 Smart hot reload on port ${this.options.wsPort}`);
601
+ }
602
+
603
+ if (this.plugins) {
604
+ const stats = this.plugins.getStats();
605
+ this.logger.log('info', 'Plugin system', `🔌 ${stats.active}/${stats.total} plugins active`);
606
+ await this.plugins.executeHook('app:start', this, port);
607
+ }
608
+
609
+ console.log('═'.repeat(60) + '\n');
610
+
611
+ if (callback && typeof callback === 'function') {
612
+ callback();
613
+ }
614
+ });
615
+ }
616
+
617
+ // ⏰ Programmation des vérifications automatiques avec protection
618
+ scheduleAutoUpdates() {
619
+ if (!this.autoUpdaterActive) return;
620
+
621
+ try {
622
+ const interval = this.autoUpdater.config.checkInterval || 3600000;
623
+
624
+ setInterval(() => {
625
+ this.checkForUpdates(true).catch(error => {
626
+ // Capture les erreurs sans bloquer le timer
627
+ this.log('error', 'Erreur vérification automatique', error.message);
628
+ });
629
+ }, interval);
630
+
631
+ this.log('info', 'Vérifications automatiques programmées', `⏰ Toutes les ${Math.round(interval / 60000)} minutes`);
632
+ } catch (error) {
633
+ this.log('error', 'Erreur lors de la programmation des vérifications', error.message);
634
+ }
635
+ }
636
+
637
+ startDev(port = this.options.port) {
638
+ this.options.isDev = true;
639
+ if (!this.devServer) {
640
+ this.devServer = new DevServer(this, this.options);
641
+ this.devServer.setup();
642
+ }
643
+ this.loadRoutes();
644
+ return this.listen(port);
645
+ }
646
+
647
+ async stop() {
648
+ if (this.plugins) {
649
+ this.plugins.executeHook('app:stop', this);
650
+ }
651
+
652
+ if (this.devServer) {
653
+ this.devServer.stop();
654
+ }
655
+
656
+ // Fermer l'authentification si activée
657
+ if (this.auth.isEnabled) {
658
+ await this.auth.destroy();
659
+ }
660
+
661
+ // Nettoyage de l'auto-updater de manière sécurisée
662
+ if (this.autoUpdaterActive && this.autoUpdater && typeof this.autoUpdater.closeReadline === 'function') {
663
+ try {
664
+ this.autoUpdater.closeReadline();
665
+ this.log('info', 'Auto-updater arrêté', '🔄');
666
+ } catch (err) {
667
+ this.log('error', 'Erreur lors de l\'arrêt de l\'auto-updater', err.message);
668
+ }
669
+ }
670
+
671
+ this.logger.log('server', 'Server stopped', '🛑 Goodbye!');
672
+ }
673
+
674
+ // Méthodes de validation et sanitisation
675
+ validateOptions(options) {
676
+ if (typeof options !== 'object' || options === null) {
677
+ throw new Error('Les options doivent être un objet');
678
+ }
679
+
680
+ // Validation du port
681
+ if (options.port !== undefined && !this.isValidPort(options.port)) {
682
+ throw new Error('Le port doit être un nombre entre 1 et 65535');
683
+ }
684
+
685
+ // Validation du port WebSocket
686
+ if (options.wsPort !== undefined && !this.isValidPort(options.wsPort)) {
687
+ throw new Error('Le port WebSocket doit être un nombre entre 1 et 65535');
688
+ }
689
+
690
+ // Validation des chemins
691
+ const pathOptions = ['viewsDir', 'staticDir', 'routesDir', 'errorLog'];
692
+ for (const pathOption of pathOptions) {
693
+ if (options[pathOption] !== undefined && !this.isValidPath(options[pathOption])) {
694
+ throw new Error(`${pathOption} doit être un chemin valide`);
695
+ }
696
+ }
697
+
698
+ // Validation des tableaux
699
+ if (options.watchDirs !== undefined && !Array.isArray(options.watchDirs)) {
700
+ throw new Error('watchDirs doit être un tableau');
701
+ }
702
+ }
703
+
704
+ isValidPort(port) {
705
+ const portNumber = parseInt(port, 10);
706
+ return !isNaN(portNumber) && portNumber >= 1 && portNumber <= 65535;
707
+ }
708
+
709
+ isValidPath(path) {
710
+ if (typeof path !== 'string') return false;
711
+ // Empêcher les chemins dangereux
712
+ const dangerousPatterns = ['../', '..\\', '<', '>', '|', '?', '*'];
713
+ return !dangerousPatterns.some(pattern => path.includes(pattern)) && path.length > 0;
714
+ }
715
+
716
+ sanitizePort(port) {
717
+ if (port === undefined || port === null) return null;
718
+ const portNumber = parseInt(port, 10);
719
+ return this.isValidPort(portNumber) ? portNumber : null;
720
+ }
721
+
722
+ sanitizePath(path) {
723
+ if (typeof path !== 'string') return null;
724
+ // Nettoyer et valider le chemin
725
+ const cleanPath = validator.escape(path.trim());
726
+ return this.isValidPath(cleanPath) ? cleanPath : null;
727
+ }
728
+
729
+ sanitizePaths(paths) {
730
+ if (!Array.isArray(paths)) return null;
731
+ return paths
732
+ .map(path => this.sanitizePath(path))
733
+ .filter(path => path !== null);
734
+ }
735
+
736
+ sanitizeString(str) {
737
+ if (typeof str !== 'string') return null;
738
+ return validator.escape(str.trim());
739
+ }
740
+
741
+ sanitizeArray(arr) {
742
+ if (!Array.isArray(arr)) return null;
743
+ return arr
744
+ .filter(item => typeof item === 'string')
745
+ .map(item => validator.escape(item.trim()));
746
+ }
747
+ }
748
+
749
+ module.exports = App;