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,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
|
+
'<': '<',
|
|
137
|
+
'>': '>',
|
|
138
|
+
'"': '"',
|
|
139
|
+
"'": ''',
|
|
140
|
+
'&': '&'
|
|
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;
|