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.
@@ -0,0 +1,1353 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const crypto = require('crypto');
4
+
5
+ class AuthManager {
6
+ constructor(app) {
7
+ this.app = app;
8
+ this.config = {
9
+ database: {
10
+ type: 'sqlite',
11
+ sqlite: {
12
+ path: './data/auth.db'
13
+ },
14
+ mysql: {
15
+ host: 'localhost',
16
+ port: 3306,
17
+ database: 'veko_auth',
18
+ username: 'root',
19
+ password: ''
20
+ }
21
+ },
22
+ session: {
23
+ secret: this.generateSecretKey(),
24
+ maxAge: 24 * 60 * 60 * 1000,
25
+ secure: process.env.NODE_ENV === 'production',
26
+ httpOnly: true,
27
+ sameSite: 'strict'
28
+ },
29
+ security: {
30
+ maxLoginAttempts: 5,
31
+ lockoutDuration: 15 * 60 * 1000,
32
+ sessionRotation: true,
33
+ csrfProtection: true,
34
+ rateLimit: {
35
+ windowMs: 15 * 60 * 1000,
36
+ max: 10
37
+ }
38
+ },
39
+ routes: {
40
+ api: {
41
+ login: '/api/auth/login',
42
+ logout: '/api/auth/logout',
43
+ register: '/api/auth/register',
44
+ check: '/api/auth/check',
45
+ profile: '/api/auth/profile'
46
+ },
47
+ web: {
48
+ enabled: true,
49
+ login: '/auth/login',
50
+ logout: '/auth/logout',
51
+ register: '/auth/register',
52
+ dashboard: '/auth/dashboard'
53
+ }
54
+ },
55
+ redirects: {
56
+ afterLogin: '/auth/dashboard',
57
+ afterLogout: '/auth/login',
58
+ loginRequired: '/auth/login'
59
+ },
60
+ password: {
61
+ minLength: 8,
62
+ requireSpecial: true,
63
+ requireNumbers: true,
64
+ requireUppercase: true,
65
+ requireLowercase: true
66
+ },
67
+ views: {
68
+ enabled: true,
69
+ autoCreate: true
70
+ }
71
+ };
72
+ this.db = null;
73
+ this.isEnabled = false;
74
+ this.loginAttempts = new Map();
75
+ this.rateLimitStore = new Map();
76
+ }
77
+
78
+ generateSecretKey() {
79
+ return crypto.randomBytes(64).toString('hex');
80
+ }
81
+
82
+ // Validation stricte des entrées
83
+ validateInput(data, schema) {
84
+ const errors = [];
85
+
86
+ for (const [field, rules] of Object.entries(schema)) {
87
+ const value = data[field];
88
+
89
+ if (rules.required && (!value || value.toString().trim() === '')) {
90
+ errors.push(`Le champ ${field} est requis`);
91
+ continue;
92
+ }
93
+
94
+ if (value) {
95
+ // Validation de type
96
+ if (rules.type && typeof value !== rules.type) {
97
+ errors.push(`Le champ ${field} doit être de type ${rules.type}`);
98
+ continue;
99
+ }
100
+
101
+ // Validation de longueur
102
+ if (rules.minLength && value.length < rules.minLength) {
103
+ errors.push(`Le champ ${field} doit contenir au moins ${rules.minLength} caractères`);
104
+ }
105
+
106
+ if (rules.maxLength && value.length > rules.maxLength) {
107
+ errors.push(`Le champ ${field} ne peut pas dépasser ${rules.maxLength} caractères`);
108
+ }
109
+
110
+ // Validation par regex
111
+ if (rules.pattern && !rules.pattern.test(value)) {
112
+ errors.push(`Le format du champ ${field} est invalide`);
113
+ }
114
+
115
+ // Validation personnalisée
116
+ if (rules.custom && !rules.custom(value)) {
117
+ errors.push(rules.customMessage || `Le champ ${field} est invalide`);
118
+ }
119
+ }
120
+ }
121
+
122
+ return {
123
+ isValid: errors.length === 0,
124
+ errors
125
+ };
126
+ }
127
+
128
+ // Nettoyage sécurisé des entrées
129
+ sanitizeInput(input) {
130
+ if (typeof input !== 'string') return '';
131
+
132
+ return input
133
+ .trim()
134
+ .replace(/[<>\"'&]/g, (match) => {
135
+ const entities = {
136
+ '<': '&lt;',
137
+ '>': '&gt;',
138
+ '"': '&quot;',
139
+ "'": '&#x27;',
140
+ '&': '&amp;'
141
+ };
142
+ return entities[match];
143
+ })
144
+ .slice(0, 1000); // Limite de longueur
145
+ }
146
+
147
+ // Protection CSRF
148
+ generateCSRFToken() {
149
+ return crypto.randomBytes(32).toString('hex');
150
+ }
151
+
152
+ validateCSRFToken(req) {
153
+ const token = req.body._csrf || req.headers['x-csrf-token'];
154
+ const sessionToken = req.session.csrfToken;
155
+
156
+ return token && sessionToken && crypto.timingSafeEqual(
157
+ Buffer.from(token),
158
+ Buffer.from(sessionToken)
159
+ );
160
+ }
161
+
162
+ async init(config = {}) {
163
+ try {
164
+ this.config = this.mergeConfig(this.config, config);
165
+
166
+ // Validation de la configuration
167
+ this.validateConfig();
168
+
169
+ await this.installDependencies();
170
+ await this.initDatabase();
171
+ this.setupSessions();
172
+ this.setupSecurity();
173
+ this.setupApiRoutes();
174
+
175
+ if (this.config.routes.web.enabled) {
176
+ this.setupWebRoutes();
177
+ }
178
+
179
+ this.setupMiddlewares();
180
+
181
+ if (this.config.views.enabled && this.config.views.autoCreate) {
182
+ this.setupViews();
183
+ }
184
+
185
+ this.isEnabled = true;
186
+ this.logSystemInfo();
187
+
188
+ } catch (error) {
189
+ console.error('❌ Erreur lors de l\'initialisation de l\'authentification:', error.message);
190
+ throw error;
191
+ }
192
+ }
193
+
194
+ validateConfig() {
195
+ // Validation des paramètres critiques
196
+ if (this.config.password.minLength < 8) {
197
+ throw new Error('La longueur minimale du mot de passe doit être d\'au moins 8 caractères');
198
+ }
199
+
200
+ if (!this.config.session.secret || this.config.session.secret.length < 32) {
201
+ this.config.session.secret = this.generateSecretKey();
202
+ }
203
+ }
204
+
205
+ mergeConfig(defaultConfig, userConfig) {
206
+ const result = { ...defaultConfig };
207
+
208
+ for (const key in userConfig) {
209
+ if (typeof userConfig[key] === 'object' && !Array.isArray(userConfig[key])) {
210
+ result[key] = this.mergeConfig(defaultConfig[key] || {}, userConfig[key]);
211
+ } else {
212
+ result[key] = userConfig[key];
213
+ }
214
+ }
215
+
216
+ return result;
217
+ }
218
+
219
+ async installDependencies() {
220
+ const requiredModules = {
221
+ 'express-session': '^1.17.3',
222
+ 'bcryptjs': '^2.4.3',
223
+ 'express-rate-limit': '^6.7.0',
224
+ 'helmet': '^6.1.5',
225
+ 'csurf': '^1.11.0'
226
+ };
227
+
228
+ if (this.config.database.type === 'mysql') {
229
+ requiredModules['mysql2'] = '^3.6.0';
230
+ } else if (this.config.database.type === 'sqlite') {
231
+ requiredModules['sqlite3'] = '^5.1.6';
232
+ }
233
+
234
+ for (const [moduleName, version] of Object.entries(requiredModules)) {
235
+ try {
236
+ require.resolve(moduleName);
237
+ } catch (error) {
238
+ console.log(`📦 Installation de ${moduleName}...`);
239
+ await this.app.installModule(moduleName, version);
240
+ }
241
+ }
242
+ }
243
+
244
+ async initDatabase() {
245
+ if (this.config.database.type === 'mysql') {
246
+ await this.initMySQL();
247
+ } else if (this.config.database.type === 'sqlite') {
248
+ await this.initSQLite();
249
+ } else {
250
+ throw new Error(`Type de base de données non supporté: ${this.config.database.type}`);
251
+ }
252
+
253
+ await this.createTables();
254
+ }
255
+
256
+ async initMySQL() {
257
+ const mysql = require('mysql2/promise');
258
+ const config = this.config.database.mysql;
259
+
260
+ this.db = await mysql.createConnection({
261
+ host: config.host,
262
+ port: config.port,
263
+ user: config.username,
264
+ password: config.password,
265
+ database: config.database
266
+ });
267
+
268
+ console.log('✅ Connexion MySQL établie');
269
+ }
270
+
271
+ async initSQLite() {
272
+ const sqlite3 = require('sqlite3').verbose();
273
+ const dbPath = path.resolve(this.config.database.sqlite.path);
274
+
275
+ const dbDir = path.dirname(dbPath);
276
+ if (!fs.existsSync(dbDir)) {
277
+ fs.mkdirSync(dbDir, { recursive: true });
278
+ }
279
+
280
+ this.db = new sqlite3.Database(dbPath);
281
+ console.log(`✅ Base SQLite créée: ${dbPath}`);
282
+ }
283
+
284
+ async createTables() {
285
+ const usersTable = this.config.database.type === 'mysql' ? `
286
+ CREATE TABLE IF NOT EXISTS users (
287
+ id INT AUTO_INCREMENT PRIMARY KEY,
288
+ username VARCHAR(255) UNIQUE NOT NULL,
289
+ email VARCHAR(255) UNIQUE NOT NULL,
290
+ password VARCHAR(255) NOT NULL,
291
+ role VARCHAR(50) DEFAULT 'user',
292
+ login_attempts INT DEFAULT 0,
293
+ locked_until TIMESTAMP NULL,
294
+ last_login TIMESTAMP NULL,
295
+ password_changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
296
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
297
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
298
+ INDEX idx_username (username),
299
+ INDEX idx_email (email)
300
+ )
301
+ ` : `
302
+ CREATE TABLE IF NOT EXISTS users (
303
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
304
+ username TEXT UNIQUE NOT NULL,
305
+ email TEXT UNIQUE NOT NULL,
306
+ password TEXT NOT NULL,
307
+ role TEXT DEFAULT 'user',
308
+ login_attempts INTEGER DEFAULT 0,
309
+ locked_until DATETIME NULL,
310
+ last_login DATETIME NULL,
311
+ password_changed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
312
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
313
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
314
+ )
315
+ `;
316
+
317
+ const sessionsTable = this.config.database.type === 'mysql' ? `
318
+ CREATE TABLE IF NOT EXISTS sessions (
319
+ session_id VARCHAR(128) PRIMARY KEY,
320
+ expires TIMESTAMP NOT NULL,
321
+ data TEXT,
322
+ INDEX idx_expires (expires)
323
+ )
324
+ ` : `
325
+ CREATE TABLE IF NOT EXISTS sessions (
326
+ session_id TEXT PRIMARY KEY,
327
+ expires DATETIME NOT NULL,
328
+ data TEXT
329
+ )
330
+ `;
331
+
332
+ if (this.config.database.type === 'mysql') {
333
+ await this.db.execute(usersTable);
334
+ await this.db.execute(sessionsTable);
335
+ } else {
336
+ await new Promise((resolve, reject) => {
337
+ this.db.run(usersTable, (err) => {
338
+ if (err) reject(err);
339
+ else resolve();
340
+ });
341
+ });
342
+ await new Promise((resolve, reject) => {
343
+ this.db.run(sessionsTable, (err) => {
344
+ if (err) reject(err);
345
+ else resolve();
346
+ });
347
+ });
348
+ }
349
+
350
+ console.log('✅ Tables de base de données créées');
351
+ }
352
+
353
+ setupSessions() {
354
+ const session = require('express-session');
355
+
356
+ // Configuration sécurisée des sessions
357
+ this.app.use(session({
358
+ secret: this.config.session.secret,
359
+ resave: false,
360
+ saveUninitialized: false,
361
+ name: 'veko.sid', // Nom personnalisé pour masquer l'usage d'Express
362
+ cookie: {
363
+ maxAge: this.config.session.maxAge,
364
+ secure: this.config.session.secure,
365
+ httpOnly: this.config.session.httpOnly,
366
+ sameSite: this.config.session.sameSite
367
+ },
368
+ genid: () => {
369
+ return crypto.randomUUID(); // Génération sécurisée des IDs de session
370
+ }
371
+ }));
372
+
373
+ console.log('✅ Sessions configurées avec sécurité renforcée');
374
+ }
375
+
376
+ setupSecurity() {
377
+ const helmet = require('helmet');
378
+ const rateLimit = require('express-rate-limit');
379
+
380
+ // Configuration Helmet pour la sécurité des headers
381
+ this.app.use(helmet({
382
+ contentSecurityPolicy: {
383
+ directives: {
384
+ defaultSrc: ["'self'"],
385
+ styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"],
386
+ scriptSrc: ["'self'"],
387
+ imgSrc: ["'self'", "data:", "https:"]
388
+ }
389
+ }
390
+ }));
391
+
392
+ // Rate limiting global
393
+ const limiter = rateLimit({
394
+ windowMs: this.config.security.rateLimit.windowMs,
395
+ max: this.config.security.rateLimit.max,
396
+ message: {
397
+ success: false,
398
+ message: 'Trop de tentatives, veuillez réessayer plus tard'
399
+ },
400
+ standardHeaders: true,
401
+ legacyHeaders: false
402
+ });
403
+
404
+ // Rate limiting spécifique pour l'authentification
405
+ const authLimiter = rateLimit({
406
+ windowMs: 15 * 60 * 1000, // 15 minutes
407
+ max: 5, // 5 tentatives max
408
+ skipSuccessfulRequests: true,
409
+ message: {
410
+ success: false,
411
+ message: 'Trop de tentatives de connexion, compte temporairement bloqué'
412
+ }
413
+ });
414
+
415
+ this.app.use('/api/auth', authLimiter);
416
+ this.app.use('/auth', authLimiter);
417
+
418
+ console.log('✅ Sécurité renforcée configurée');
419
+ }
420
+
421
+ setupApiRoutes() {
422
+ // Middleware de validation CSRF pour les routes sensibles
423
+ const csrfMiddleware = (req, res, next) => {
424
+ if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
425
+ if (!this.validateCSRFToken(req)) {
426
+ return res.status(403).json({
427
+ success: false,
428
+ message: 'Token CSRF invalide'
429
+ });
430
+ }
431
+ }
432
+ next();
433
+ };
434
+
435
+ // Schéma de validation pour la connexion
436
+ const loginSchema = {
437
+ username: {
438
+ required: true,
439
+ type: 'string',
440
+ minLength: 3,
441
+ maxLength: 50,
442
+ pattern: /^[a-zA-Z0-9._@-]+$/,
443
+ customMessage: 'Le nom d\'utilisateur ne peut contenir que des lettres, chiffres et . _ @ -'
444
+ },
445
+ password: {
446
+ required: true,
447
+ type: 'string',
448
+ minLength: 1,
449
+ maxLength: 128
450
+ }
451
+ };
452
+
453
+ // API - Connexion sécurisée
454
+ this.app.createRoute('post', this.config.routes.api.login, async (req, res) => {
455
+ try {
456
+ // Validation des entrées
457
+ const validation = this.validateInput(req.body, loginSchema);
458
+ if (!validation.isValid) {
459
+ return res.status(400).json({
460
+ success: false,
461
+ message: validation.errors[0],
462
+ errors: validation.errors
463
+ });
464
+ }
465
+
466
+ const { username, password } = req.body;
467
+ const cleanUsername = this.sanitizeInput(username);
468
+
469
+ // Vérification du rate limiting
470
+ if (this.isRateLimited(req.ip)) {
471
+ return res.status(429).json({
472
+ success: false,
473
+ message: 'Trop de tentatives, veuillez réessayer plus tard'
474
+ });
475
+ }
476
+
477
+ const user = await this.authenticateUser(cleanUsername, password, req.ip);
478
+
479
+ if (user) {
480
+ await this.resetLoginAttempts(user.id);
481
+
482
+ if (this.config.security.sessionRotation) {
483
+ await this.regenerateSession(req);
484
+ }
485
+
486
+ // Génération du token CSRF
487
+ req.session.csrfToken = this.generateCSRFToken();
488
+
489
+ req.session.user = {
490
+ id: user.id,
491
+ username: user.username,
492
+ email: user.email,
493
+ role: user.role
494
+ };
495
+
496
+ await this.updateLastLogin(user.id);
497
+
498
+ res.json({
499
+ success: true,
500
+ message: 'Connexion réussie',
501
+ user: req.session.user,
502
+ csrfToken: req.session.csrfToken
503
+ });
504
+ } else {
505
+ this.recordFailedAttempt(req.ip);
506
+
507
+ res.status(401).json({
508
+ success: false,
509
+ message: 'Identifiants incorrects'
510
+ });
511
+ }
512
+ } catch (error) {
513
+ console.error('Erreur lors de la connexion:', error.message);
514
+ res.status(500).json({
515
+ success: false,
516
+ message: 'Erreur serveur'
517
+ });
518
+ }
519
+ });
520
+
521
+ // Schéma de validation pour l'inscription
522
+ const registerSchema = {
523
+ username: {
524
+ required: true,
525
+ type: 'string',
526
+ minLength: 3,
527
+ maxLength: 50,
528
+ pattern: /^[a-zA-Z0-9._-]+$/,
529
+ customMessage: 'Le nom d\'utilisateur ne peut contenir que des lettres, chiffres et . _ -'
530
+ },
531
+ email: {
532
+ required: true,
533
+ type: 'string',
534
+ maxLength: 254,
535
+ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
536
+ customMessage: 'Format d\'email invalide'
537
+ },
538
+ password: {
539
+ required: true,
540
+ type: 'string',
541
+ minLength: this.config.password.minLength,
542
+ maxLength: 128,
543
+ custom: (value) => this.validatePassword(value).isValid,
544
+ customMessage: 'Le mot de passe ne respecte pas les critères de sécurité'
545
+ },
546
+ confirmPassword: {
547
+ required: true,
548
+ type: 'string'
549
+ }
550
+ };
551
+
552
+ // API - Inscription sécurisée
553
+ this.app.createRoute('post', this.config.routes.api.register, async (req, res) => {
554
+ try {
555
+ // Validation des entrées
556
+ const validation = this.validateInput(req.body, registerSchema);
557
+ if (!validation.isValid) {
558
+ return res.status(400).json({
559
+ success: false,
560
+ message: validation.errors[0],
561
+ errors: validation.errors
562
+ });
563
+ }
564
+
565
+ const { username, email, password, confirmPassword } = req.body;
566
+
567
+ // Vérification que les mots de passe correspondent
568
+ if (password !== confirmPassword) {
569
+ return res.status(400).json({
570
+ success: false,
571
+ message: 'Les mots de passe ne correspondent pas'
572
+ });
573
+ }
574
+
575
+ const cleanUsername = this.sanitizeInput(username);
576
+ const cleanEmail = this.sanitizeInput(email);
577
+
578
+ const user = await this.createUser(cleanUsername, cleanEmail, password);
579
+
580
+ if (this.config.security.sessionRotation) {
581
+ await this.regenerateSession(req);
582
+ }
583
+
584
+ req.session.csrfToken = this.generateCSRFToken();
585
+
586
+ req.session.user = {
587
+ id: user.id,
588
+ username: user.username,
589
+ email: user.email,
590
+ role: user.role
591
+ };
592
+
593
+ res.json({
594
+ success: true,
595
+ message: 'Inscription réussie',
596
+ user: req.session.user,
597
+ csrfToken: req.session.csrfToken
598
+ });
599
+
600
+ } catch (error) {
601
+ if (error.message.includes('UNIQUE constraint') || error.code === 'ER_DUP_ENTRY') {
602
+ res.status(409).json({
603
+ success: false,
604
+ message: 'Cet utilisateur existe déjà'
605
+ });
606
+ } else {
607
+ console.error('Erreur lors de l\'inscription:', error.message);
608
+ res.status(500).json({
609
+ success: false,
610
+ message: 'Erreur serveur'
611
+ });
612
+ }
613
+ }
614
+ });
615
+
616
+ // API - Déconnexion sécurisée
617
+ this.app.createRoute('post', this.config.routes.api.logout, csrfMiddleware, (req, res) => {
618
+ if (!req.session.user) {
619
+ return res.status(401).json({
620
+ success: false,
621
+ message: 'Non authentifié'
622
+ });
623
+ }
624
+
625
+ req.session.destroy((err) => {
626
+ if (err) {
627
+ res.status(500).json({
628
+ success: false,
629
+ message: 'Erreur lors de la déconnexion'
630
+ });
631
+ } else {
632
+ res.json({
633
+ success: true,
634
+ message: 'Déconnexion réussie'
635
+ });
636
+ }
637
+ });
638
+ });
639
+
640
+ // API - Vérification d'authentification sécurisée
641
+ this.app.createRoute('get', this.config.routes.api.check, (req, res) => {
642
+ // Validation de session
643
+ if (req.session && req.session.user) {
644
+ // Vérification de l'intégrité de la session
645
+ if (this.isValidSessionUser(req.session.user)) {
646
+ res.json({
647
+ authenticated: true,
648
+ user: req.session.user,
649
+ csrfToken: req.session.csrfToken
650
+ });
651
+ } else {
652
+ req.session.destroy();
653
+ res.json({
654
+ authenticated: false,
655
+ user: null
656
+ });
657
+ }
658
+ } else {
659
+ res.json({
660
+ authenticated: false,
661
+ user: null
662
+ });
663
+ }
664
+ });
665
+
666
+ // API - Profil utilisateur sécurisé
667
+ this.app.createRoute('get', this.config.routes.api.profile, this.requireAuth.bind(this), (req, res) => {
668
+ res.json({
669
+ success: true,
670
+ user: req.session.user
671
+ });
672
+ });
673
+
674
+ // Schéma de validation pour la mise à jour du profil
675
+ const profileUpdateSchema = {
676
+ email: {
677
+ required: false,
678
+ type: 'string',
679
+ maxLength: 254,
680
+ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
681
+ customMessage: 'Format d\'email invalide'
682
+ }
683
+ };
684
+
685
+ // API - Mise à jour du profil sécurisée
686
+ this.app.createRoute('put', this.config.routes.api.profile,
687
+ this.requireAuth.bind(this),
688
+ csrfMiddleware,
689
+ async (req, res) => {
690
+ try {
691
+ // Validation des entrées
692
+ const validation = this.validateInput(req.body, profileUpdateSchema);
693
+ if (!validation.isValid) {
694
+ return res.status(400).json({
695
+ success: false,
696
+ message: validation.errors[0],
697
+ errors: validation.errors
698
+ });
699
+ }
700
+
701
+ const { email } = req.body;
702
+ const userId = req.session.user.id;
703
+
704
+ if (email) {
705
+ const cleanEmail = this.sanitizeInput(email);
706
+ await this.updateUser(userId, { email: cleanEmail });
707
+ req.session.user.email = cleanEmail;
708
+ }
709
+
710
+ res.json({
711
+ success: true,
712
+ message: 'Profil mis à jour',
713
+ user: req.session.user
714
+ });
715
+ } catch (error) {
716
+ console.error('Erreur lors de la mise à jour:', error.message);
717
+ res.status(500).json({
718
+ success: false,
719
+ message: 'Erreur lors de la mise à jour'
720
+ });
721
+ }
722
+ }
723
+ );
724
+
725
+ console.log('✅ Routes API d\'authentification sécurisées configurées');
726
+ }
727
+
728
+ // Ajout d'une méthode de validation pour les paramètres de requête GET
729
+ validateQueryParams(query, schema) {
730
+ const errors = [];
731
+ const sanitized = {};
732
+ for (const [field, rules] of Object.entries(schema)) {
733
+ let value = query[field];
734
+ if (rules.required && (!value || value.toString().trim() === '')) {
735
+ errors.push(`Le paramètre ${field} est requis`);
736
+ continue;
737
+ }
738
+ if (value) {
739
+ value = this.sanitizeInput(value);
740
+ if (rules.type && typeof value !== rules.type) {
741
+ errors.push(`Le paramètre ${field} doit être de type ${rules.type}`);
742
+ continue;
743
+ }
744
+ if (rules.maxLength && value.length > rules.maxLength) {
745
+ errors.push(`Le paramètre ${field} ne peut pas dépasser ${rules.maxLength} caractères`);
746
+ }
747
+ if (rules.pattern && !rules.pattern.test(value)) {
748
+ errors.push(`Le format du paramètre ${field} est invalide`);
749
+ }
750
+ sanitized[field] = value;
751
+ }
752
+ }
753
+ return { isValid: errors.length === 0, errors, sanitized };
754
+ }
755
+
756
+ setupWebRoutes() {
757
+ // Validation pour les routes web
758
+ const webLoginSchema = {
759
+ username: {
760
+ required: true,
761
+ type: 'string',
762
+ minLength: 3,
763
+ maxLength: 50
764
+ },
765
+ password: {
766
+ required: true,
767
+ type: 'string',
768
+ minLength: 1,
769
+ maxLength: 128
770
+ }
771
+ };
772
+
773
+ // Routes EJS - Connexion (GET)
774
+ this.app.createRoute('get', this.config.routes.web.login, async (req, res) => {
775
+ // Validation des query params (ex: error)
776
+ const querySchema = {
777
+ error: { required: false, type: 'string', maxLength: 50, pattern: /^[a-z_]+$/ }
778
+ };
779
+ const validation = this.validateQueryParams(req.query, querySchema);
780
+ if (!validation.isValid) {
781
+ return res.status(400).render('auth/login', {
782
+ title: 'Connexion',
783
+ error: 'invalid_query',
784
+ csrfToken: req.session.csrfToken,
785
+ layout: false
786
+ });
787
+ }
788
+
789
+ if (req.session.user) {
790
+ return res.redirect(this.config.redirects.afterLogin);
791
+ }
792
+
793
+ // Génération du token CSRF pour les vues
794
+ if (!req.session.csrfToken) {
795
+ req.session.csrfToken = this.generateCSRFToken();
796
+ }
797
+
798
+ res.render('auth/login', {
799
+ title: 'Connexion',
800
+ error: validation.sanitized.error,
801
+ csrfToken: req.session.csrfToken,
802
+ layout: false
803
+ });
804
+ });
805
+
806
+ // Routes EJS - Connexion (POST)
807
+ this.app.createRoute('post', this.config.routes.web.login, async (req, res) => {
808
+ try {
809
+ // Validation CSRF
810
+ if (!this.validateCSRFToken(req)) {
811
+ return res.redirect(`${this.config.routes.web.login}?error=csrf_invalid`);
812
+ }
813
+
814
+ // Validation des entrées
815
+ const validation = this.validateInput(req.body, webLoginSchema);
816
+ if (!validation.isValid) {
817
+ return res.redirect(`${this.config.routes.web.login}?error=invalid_input`);
818
+ }
819
+
820
+ const { username, password } = req.body;
821
+ const cleanUsername = this.sanitizeInput(username);
822
+
823
+ const user = await this.authenticateUser(cleanUsername, password);
824
+
825
+ if (user) {
826
+ await this.resetLoginAttempts(user.id);
827
+
828
+ if (this.config.security.sessionRotation) {
829
+ await this.regenerateSession(req);
830
+ }
831
+
832
+ req.session.user = {
833
+ id: user.id,
834
+ username: user.username,
835
+ email: user.email,
836
+ role: user.role
837
+ };
838
+
839
+ await this.updateLastLogin(user.id);
840
+ res.redirect(this.config.redirects.afterLogin);
841
+ } else {
842
+ res.redirect(`${this.config.routes.web.login}?error=invalid_credentials`);
843
+ }
844
+ } catch (error) {
845
+ console.error('Erreur lors de la connexion:', error.message);
846
+ res.redirect(`${this.config.routes.web.login}?error=server_error`);
847
+ }
848
+ });
849
+
850
+ // Routes EJS - Inscription (GET)
851
+ this.app.createRoute('get', this.config.routes.web.register, async (req, res) => {
852
+ if (req.session.user) {
853
+ return res.redirect(this.config.redirects.afterLogin);
854
+ }
855
+
856
+ if (!req.session.csrfToken) {
857
+ req.session.csrfToken = this.generateCSRFToken();
858
+ }
859
+
860
+ res.render('auth/register', {
861
+ title: 'Inscription',
862
+ error: req.query.error,
863
+ csrfToken: req.session.csrfToken,
864
+ layout: false
865
+ });
866
+ });
867
+
868
+ // Schéma pour l'inscription web
869
+ const webRegisterSchema = {
870
+ username: {
871
+ required: true,
872
+ type: 'string',
873
+ minLength: 3,
874
+ maxLength: 50
875
+ },
876
+ email: {
877
+ required: true,
878
+ type: 'string',
879
+ maxLength: 254,
880
+ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
881
+ },
882
+ password: {
883
+ required: true,
884
+ type: 'string',
885
+ minLength: this.config.password.minLength,
886
+ maxLength: 128
887
+ },
888
+ confirmPassword: {
889
+ required: true,
890
+ type: 'string'
891
+ }
892
+ };
893
+
894
+ // Routes EJS - Inscription (POST)
895
+ this.app.createRoute('post', this.config.routes.web.register, async (req, res) => {
896
+ try {
897
+ // Validation CSRF
898
+ if (!this.validateCSRFToken(req)) {
899
+ return res.redirect(`${this.config.routes.web.register}?error=csrf_invalid`);
900
+ }
901
+
902
+ // Validation des entrées
903
+ const validation = this.validateInput(req.body, webRegisterSchema);
904
+ if (!validation.isValid) {
905
+ return res.redirect(`${this.config.routes.web.register}?error=invalid_input`);
906
+ }
907
+
908
+ const { username, email, password, confirmPassword } = req.body;
909
+
910
+ if (password !== confirmPassword) {
911
+ return res.redirect(`${this.config.routes.web.register}?error=password_mismatch`);
912
+ }
913
+
914
+ const passwordValidation = this.validatePassword(password);
915
+ if (!passwordValidation.isValid) {
916
+ return res.redirect(`${this.config.routes.web.register}?error=password_weak`);
917
+ }
918
+
919
+ const cleanUsername = this.sanitizeInput(username);
920
+ const cleanEmail = this.sanitizeInput(email);
921
+
922
+ const user = await this.createUser(cleanUsername, cleanEmail, password);
923
+
924
+ if (this.config.security.sessionRotation) {
925
+ await this.regenerateSession(req);
926
+ }
927
+
928
+ req.session.user = {
929
+ id: user.id,
930
+ username: user.username,
931
+ email: user.email,
932
+ role: user.role
933
+ };
934
+
935
+ res.redirect(this.config.redirects.afterLogin);
936
+
937
+ } catch (error) {
938
+ if (error.message.includes('UNIQUE constraint') || error.code === 'ER_DUP_ENTRY') {
939
+ res.redirect(`${this.config.routes.web.register}?error=user_exists`);
940
+ } else {
941
+ console.error('Erreur lors de l\'inscription:', error.message);
942
+ res.redirect(`${this.config.routes.web.register}?error=server_error`);
943
+ }
944
+ }
945
+ });
946
+
947
+ // Routes EJS - Déconnexion sécurisée
948
+ this.app.createRoute('get', this.config.routes.web.logout, (req, res) => {
949
+ if (req.session) {
950
+ req.session.destroy((err) => {
951
+ if (err) {
952
+ console.error('Erreur lors de la déconnexion:', err);
953
+ }
954
+ res.redirect(this.config.redirects.afterLogout);
955
+ });
956
+ } else {
957
+ res.redirect(this.config.redirects.afterLogout);
958
+ }
959
+ });
960
+
961
+ // Routes EJS - Dashboard sécurisé
962
+ this.app.createRoute('get', this.config.routes.web.dashboard, this.requireAuth.bind(this), (req, res) => {
963
+ res.render('auth/dashboard', {
964
+ title: 'Dashboard',
965
+ user: req.session.user,
966
+ layout: false
967
+ });
968
+ });
969
+
970
+ console.log('✅ Routes web EJS d\'authentification sécurisées configurées');
971
+ }
972
+
973
+ // Validation d'intégrité de session
974
+ isValidSessionUser(user) {
975
+ return user &&
976
+ typeof user.id === 'number' &&
977
+ typeof user.username === 'string' &&
978
+ typeof user.email === 'string' &&
979
+ typeof user.role === 'string' &&
980
+ user.username.length > 0 &&
981
+ user.email.includes('@');
982
+ }
983
+
984
+ async authenticateUser(username, password, ip) {
985
+ const bcrypt = require('bcryptjs');
986
+
987
+ // Validation préventive
988
+ if (!username || !password || typeof username !== 'string' || typeof password !== 'string') {
989
+ return null;
990
+ }
991
+
992
+ const query = this.config.database.type === 'mysql' ?
993
+ 'SELECT * FROM users WHERE (username = ? OR email = ?) AND (locked_until IS NULL OR locked_until < NOW())' :
994
+ 'SELECT * FROM users WHERE (username = ? OR email = ?) AND (locked_until IS NULL OR locked_until < datetime("now"))';
995
+
996
+ let user;
997
+
998
+ try {
999
+ if (this.config.database.type === 'mysql') {
1000
+ const [rows] = await this.db.execute(query, [username, username]);
1001
+ user = rows[0];
1002
+ } else {
1003
+ user = await new Promise((resolve, reject) => {
1004
+ this.db.get(query, [username, username], (err, row) => {
1005
+ if (err) reject(err);
1006
+ else resolve(row);
1007
+ });
1008
+ });
1009
+ }
1010
+
1011
+ if (!user) {
1012
+ return null;
1013
+ }
1014
+
1015
+ // Vérifier si le compte est verrouillé
1016
+ if (user.login_attempts >= this.config.security.maxLoginAttempts) {
1017
+ await this.lockAccount(user.id);
1018
+ return null;
1019
+ }
1020
+
1021
+ // Vérification sécurisée du mot de passe
1022
+ const isValidPassword = await bcrypt.compare(password, user.password);
1023
+
1024
+ if (!isValidPassword) {
1025
+ await this.incrementLoginAttempts(user.id);
1026
+ return null;
1027
+ }
1028
+
1029
+ return user;
1030
+ } catch (error) {
1031
+ console.error('Erreur lors de l\'authentification:', error.message);
1032
+ return null;
1033
+ }
1034
+ }
1035
+
1036
+ async createUser(username, email, password) {
1037
+ const bcrypt = require('bcryptjs');
1038
+
1039
+ // Validation du mot de passe
1040
+ const passwordValidation = this.validatePassword(password);
1041
+ if (!passwordValidation.isValid) {
1042
+ throw new Error(passwordValidation.errors[0]);
1043
+ }
1044
+
1045
+ const hashedPassword = await bcrypt.hash(password, 12); // Augmentation du salt rounds
1046
+
1047
+ const query = this.config.database.type === 'mysql' ?
1048
+ 'INSERT INTO users (username, email, password, password_changed_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)' :
1049
+ 'INSERT INTO users (username, email, password, password_changed_at) VALUES (?, ?, ?, datetime("now"))';
1050
+
1051
+ if (this.config.database.type === 'mysql') {
1052
+ const [result] = await this.db.execute(query, [username, email, hashedPassword]);
1053
+ return {
1054
+ id: result.insertId,
1055
+ username,
1056
+ email,
1057
+ role: 'user'
1058
+ };
1059
+ } else {
1060
+ return new Promise((resolve, reject) => {
1061
+ this.db.run(query, [username, email, hashedPassword], function(err) {
1062
+ if (err) reject(err);
1063
+ else resolve({
1064
+ id: this.lastID,
1065
+ username,
1066
+ email,
1067
+ role: 'user'
1068
+ });
1069
+ });
1070
+ });
1071
+ }
1072
+ }
1073
+
1074
+ // Middlewares d'authentification
1075
+ requireAuth(req, res, next) {
1076
+ if (!req.session || !req.session.user || !this.isValidSessionUser(req.session.user)) {
1077
+ if (req.xhr || req.headers.accept.indexOf('json') > -1) {
1078
+ return res.status(401).json({
1079
+ success: false,
1080
+ message: 'Authentification requise'
1081
+ });
1082
+ }
1083
+ return res.redirect(this.config.redirects.loginRequired);
1084
+ }
1085
+ next();
1086
+ }
1087
+
1088
+ requireRole(role) {
1089
+ return (req, res, next) => {
1090
+ if (!req.session.user) {
1091
+ return res.redirect(this.config.redirects.loginRequired);
1092
+ }
1093
+
1094
+ if (req.session.user.role !== role && req.session.user.role !== 'admin') {
1095
+ return res.status(403).send('Accès refusé - Permissions insuffisantes');
1096
+ }
1097
+
1098
+ next();
1099
+ };
1100
+ }
1101
+
1102
+ // API publique
1103
+ isAuthenticated(req) {
1104
+ return !!req.session.user;
1105
+ }
1106
+
1107
+ getCurrentUser(req) {
1108
+ return req.session.user || null;
1109
+ }
1110
+
1111
+ async logout(req) {
1112
+ return new Promise((resolve) => {
1113
+ req.session.destroy((err) => {
1114
+ resolve(!err);
1115
+ });
1116
+ });
1117
+ }
1118
+
1119
+ async destroy() {
1120
+ if (this.db) {
1121
+ if (this.config.database.type === 'mysql') {
1122
+ await this.db.end();
1123
+ } else {
1124
+ this.db.close();
1125
+ }
1126
+ }
1127
+ this.isEnabled = false;
1128
+ console.log('🔐 Système d\'authentification fermé');
1129
+ }
1130
+
1131
+ // Nouvelle méthode pour mettre à jour un utilisateur
1132
+ async updateUser(userId, updates) {
1133
+ const allowedFields = ['email'];
1134
+ const setClause = [];
1135
+ const values = [];
1136
+
1137
+ for (const [key, value] of Object.entries(updates)) {
1138
+ if (allowedFields.includes(key)) {
1139
+ setClause.push(`${key} = ?`);
1140
+ values.push(value);
1141
+ }
1142
+ }
1143
+
1144
+ if (setClause.length === 0) {
1145
+ throw new Error('Aucun champ valide à mettre à jour');
1146
+ }
1147
+
1148
+ values.push(userId);
1149
+ const query = `UPDATE users SET ${setClause.join(', ')} WHERE id = ?`;
1150
+
1151
+ if (this.config.database.type === 'mysql') {
1152
+ await this.db.execute(query, values);
1153
+ } else {
1154
+ await new Promise((resolve, reject) => {
1155
+ this.db.run(query, values, (err) => {
1156
+ if (err) reject(err);
1157
+ else resolve();
1158
+ });
1159
+ });
1160
+ }
1161
+ }
1162
+
1163
+ // Méthode pour désactiver/activer les routes web
1164
+ toggleWebRoutes(enabled) {
1165
+ this.config.routes.web.enabled = enabled;
1166
+
1167
+ if (enabled && this.isEnabled) {
1168
+ this.setupWebRoutes();
1169
+ console.log('✅ Routes web EJS activées');
1170
+ } else {
1171
+ console.log('❌ Routes web EJS désactivées');
1172
+ }
1173
+ }
1174
+
1175
+ // Méthode pour désactiver/activer les vues automatiques
1176
+ toggleAutoViews(enabled) {
1177
+ this.config.views.enabled = enabled;
1178
+
1179
+ if (enabled && this.isEnabled) {
1180
+ this.setupViews();
1181
+ console.log('✅ Vues automatiques activées');
1182
+ } else {
1183
+ console.log('❌ Vues automatiques désactivées');
1184
+ }
1185
+ }
1186
+
1187
+ // Méthodes de sécurité
1188
+ sanitizeInput(input) {
1189
+ if (typeof input !== 'string') return '';
1190
+ return input.trim().replace(/[<>\"']/g, '');
1191
+ }
1192
+
1193
+ validatePassword(password) {
1194
+ const config = this.config.password;
1195
+ const errors = [];
1196
+
1197
+ if (password.length < config.minLength) {
1198
+ errors.push(`Le mot de passe doit contenir au moins ${config.minLength} caractères`);
1199
+ }
1200
+
1201
+ if (config.requireUppercase && !/[A-Z]/.test(password)) {
1202
+ errors.push('Le mot de passe doit contenir au moins une majuscule');
1203
+ }
1204
+
1205
+ if (config.requireLowercase && !/[a-z]/.test(password)) {
1206
+ errors.push('Le mot de passe doit contenir au moins une minuscule');
1207
+ }
1208
+
1209
+ if (config.requireNumbers && !/\d/.test(password)) {
1210
+ errors.push('Le mot de passe doit contenir au moins un chiffre');
1211
+ }
1212
+
1213
+ if (config.requireSpecial && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
1214
+ errors.push('Le mot de passe doit contenir au moins un caractère spécial');
1215
+ }
1216
+
1217
+ return {
1218
+ isValid: errors.length === 0,
1219
+ errors
1220
+ };
1221
+ }
1222
+
1223
+ validateRegistrationData({ username, email, password, confirmPassword }) {
1224
+ if (!username || !email || !password || !confirmPassword) {
1225
+ return { isValid: false, message: 'Tous les champs sont requis' };
1226
+ }
1227
+
1228
+ if (password !== confirmPassword) {
1229
+ return { isValid: false, message: 'Les mots de passe ne correspondent pas' };
1230
+ }
1231
+
1232
+ const passwordValidation = this.validatePassword(password);
1233
+ if (!passwordValidation.isValid) {
1234
+ return { isValid: false, message: passwordValidation.errors[0] };
1235
+ }
1236
+
1237
+ // Validation email
1238
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1239
+ if (!emailRegex.test(email)) {
1240
+ return { isValid: false, message: 'Format d\'email invalide' };
1241
+ }
1242
+
1243
+ // Validation username
1244
+ if (username.length < 3 || username.length > 50) {
1245
+ return { isValid: false, message: 'Le nom d\'utilisateur doit contenir entre 3 et 50 caractères' };
1246
+ }
1247
+
1248
+ return { isValid: true };
1249
+ }
1250
+
1251
+ isRateLimited(ip) {
1252
+ const now = Date.now();
1253
+ const attempts = this.rateLimitStore.get(ip) || { count: 0, resetTime: now + this.config.security.rateLimit.windowMs };
1254
+
1255
+ if (now > attempts.resetTime) {
1256
+ this.rateLimitStore.set(ip, { count: 1, resetTime: now + this.config.security.rateLimit.windowMs });
1257
+ return false;
1258
+ }
1259
+
1260
+ if (attempts.count >= this.config.security.rateLimit.max) {
1261
+ return true;
1262
+ }
1263
+
1264
+ attempts.count++;
1265
+ this.rateLimitStore.set(ip, attempts);
1266
+ return false;
1267
+ }
1268
+
1269
+ recordFailedAttempt(ip) {
1270
+ const now = Date.now();
1271
+ const attempts = this.loginAttempts.get(ip) || { count: 0, resetTime: now + this.config.security.lockoutDuration };
1272
+
1273
+ if (now > attempts.resetTime) {
1274
+ this.loginAttempts.set(ip, { count: 1, resetTime: now + this.config.security.lockoutDuration });
1275
+ } else {
1276
+ attempts.count++;
1277
+ this.loginAttempts.set(ip, attempts);
1278
+ }
1279
+ }
1280
+
1281
+ async regenerateSession(req) {
1282
+ return new Promise((resolve, reject) => {
1283
+ req.session.regenerate((err) => {
1284
+ if (err) reject(err);
1285
+ else resolve();
1286
+ });
1287
+ });
1288
+ }
1289
+
1290
+ async resetLoginAttempts(userId) {
1291
+ const query = this.config.database.type === 'mysql' ?
1292
+ 'UPDATE users SET login_attempts = 0, locked_until = NULL WHERE id = ?' :
1293
+ 'UPDATE users SET login_attempts = 0, locked_until = NULL WHERE id = ?';
1294
+
1295
+ if (this.config.database.type === 'mysql') {
1296
+ await this.db.execute(query, [userId]);
1297
+ } else {
1298
+ await new Promise((resolve, reject) => {
1299
+ this.db.run(query, [userId], (err) => {
1300
+ if (err) reject(err);
1301
+ else resolve();
1302
+ });
1303
+ });
1304
+ }
1305
+ }
1306
+
1307
+ async updateLastLogin(userId) {
1308
+ const query = this.config.database.type === 'mysql' ?
1309
+ 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?' :
1310
+ 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?';
1311
+
1312
+ if (this.config.database.type === 'mysql') {
1313
+ await this.db.execute(query, [userId]);
1314
+ } else {
1315
+ await new Promise((resolve, reject) => {
1316
+ this.db.run(query, [userId], (err) => {
1317
+ if (err) reject(err);
1318
+ else resolve();
1319
+ });
1320
+ });
1321
+ }
1322
+ }
1323
+
1324
+ // Méthode pour verrouiller un compte après trop de tentatives de connexion
1325
+ async lockAccount(userId) {
1326
+ const lockUntil = new Date(Date.now() + this.config.security.lockoutDuration);
1327
+ const query = this.config.database.type === 'mysql' ?
1328
+ 'UPDATE users SET locked_until = ? WHERE id = ?' :
1329
+ 'UPDATE users SET locked_until = ? WHERE id = ?';
1330
+
1331
+ if (this.config.database.type === 'mysql') {
1332
+ await this.db.execute(query, [lockUntil, userId]);
1333
+ } else {
1334
+ await new Promise((resolve, reject) => {
1335
+ this.db.run(query, [lockUntil.toISOString(), userId], (err) => {
1336
+ if (err) reject(err);
1337
+ else resolve();
1338
+ });
1339
+ });
1340
+ }
1341
+ }
1342
+
1343
+ logSystemInfo() {
1344
+ console.log('✅ Système d\'authentification initialisé avec sécurité renforcée');
1345
+ console.log(`📊 Base de données: ${this.config.database.type}`);
1346
+ console.log(`🌐 Routes web EJS: ${this.config.routes.web.enabled ? 'Activées' : 'Désactivées'}`);
1347
+ console.log(`👁️ Vues automatiques: ${this.config.views.enabled ? 'Activées' : 'Désactivées'}`);
1348
+ console.log(`🔒 Sécurité: Rate limiting, Protection CSRF, Headers sécurisés`);
1349
+ console.log(`🛡️ Protection des mots de passe: Longueur min ${this.config.password.minLength}, Complexité requise`);
1350
+ }
1351
+ }
1352
+
1353
+ module.exports = AuthManager;