i18ntk 2.4.0 → 2.5.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.
@@ -1,576 +1,594 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const crypto = require('crypto');
4
- const SecurityUtils = require('./security');
5
- const configManager = require('./config-manager');
6
-
7
- /**
8
- * Admin Authentication Module
9
- * Provides secure PIN-based authentication for administrative operations
10
- */
11
- class AdminAuth {
12
- constructor() {
13
- const packageRoot = path.resolve(__dirname, '..');
14
- this.configPath = path.join(packageRoot, '.i18n-admin-config.json');
15
-
16
- // Get settings from config manager
17
- const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
18
- const securitySettings = settings.security || {};
19
- this.sessionTimeout = (securitySettings.sessionTimeout || 30) * 60 * 1000; // Convert minutes to milliseconds
20
- this.maxAttempts = securitySettings.maxFailedAttempts || 3;
21
- this.lockoutDuration = (securitySettings.lockoutDuration || 15) * 60 * 1000; // Convert minutes to milliseconds
22
- this.keepAuthenticatedUntilExit = securitySettings.keepAuthenticatedUntilExit !== false;
23
-
24
- this.activeSessions = new Map();
25
- this.failedAttempts = new Map();
26
- this.lockouts = new Map();
27
- this.currentSession = null;
28
- this.sessionStartTime = null;
29
-
30
- // Clean up expired sessions every 5 minutes
31
- this.cleanupInterval = setInterval(this.cleanupExpiredSessions.bind(this), 5 * 60 * 1000);
32
-
33
- // Handle process exit to ensure session cleanup
34
- this.setupProcessHandlers();
35
- }
36
-
37
- /**
38
- * Initialize admin authentication system
39
- */
40
- async initialize() {
41
- try {
42
- if (!SecurityUtils.safeExistsSync(this.configPath)) {
43
- // Create default config if it doesn't exist
44
- const defaultConfig = {
45
- enabled: false,
46
- pinHash: null,
47
- salt: null,
48
- createdAt: new Date().toISOString(),
49
- lastModified: new Date().toISOString()
50
- };
51
- await this.saveConfig(defaultConfig);
52
- }
53
-
54
- SecurityUtils.logSecurityEvent(
55
- 'admin_auth_initialized',
56
- 'info',
57
- { message: 'Admin authentication system initialized' }
58
- );
59
- return true;
60
- } catch (error) {
61
- SecurityUtils.logSecurityEvent(
62
- 'admin_auth_init_error',
63
- 'error',
64
- { message: `Failed to initialize admin auth: ${error.message}` }
65
- );
66
- return false;
67
- }
68
- }
69
-
70
- /**
71
- * Cleanup resources and stop intervals
72
- */
73
- async cleanup() {
74
- if (this.cleanupInterval) {
75
- clearInterval(this.cleanupInterval);
76
- this.cleanupInterval = null;
77
- }
78
- }
79
-
80
- /**
81
- * Load admin configuration
82
- */
83
- async loadConfig() {
84
- try {
85
- if (!SecurityUtils.safeExistsSync(this.configPath)) {
86
- return null;
87
- }
88
-
89
- const content = await fs.promises.readFile(this.configPath, 'utf8');
90
- return SecurityUtils.safeParseJSON(content);
91
- } catch (error) {
92
- SecurityUtils.logSecurityEvent(
93
- 'admin_config_load_error',
94
- 'error',
95
- { message: `Failed to load admin config: ${error.message}` }
96
- );
97
- return null;
98
- }
99
- }
100
-
101
- /**
102
- * Save admin configuration
103
- */
104
- async saveConfig(config) {
105
- try {
106
- const content = JSON.stringify(config, null, 2);
107
- await fs.promises.writeFile(this.configPath, content, { mode: 0o600 }); // Restrict permissions
108
- SecurityUtils.logSecurityEvent(
109
- 'admin_config_saved',
110
- 'info',
111
- { message: 'Admin configuration saved' }
112
- );
113
- return true;
114
- } catch (error) {
115
- SecurityUtils.logSecurityEvent(
116
- 'admin_config_save_error',
117
- 'error',
118
- { message: `Failed to save admin config: ${error.message}` }
119
- );
120
- return false;
121
- }
122
- }
123
-
124
- /**
125
- * Set up admin PIN
126
- */
127
- async setupPin(pin) {
128
- try {
129
- // Validate PIN format (4-6 digits)
130
- if (!/^\d{4,6}$/.test(pin)) {
131
- throw new Error('PIN must be 4-6 digits');
132
- }
133
-
134
- // Generate salt and hash
135
- const salt = crypto.randomBytes(32).toString('hex');
136
- const pinHash = this.hashPin(pin, salt);
137
-
138
- const config = {
139
- enabled: true,
140
- pinHash,
141
- salt,
142
- createdAt: new Date().toISOString(),
143
- lastModified: new Date().toISOString()
144
- };
145
-
146
- const success = await this.saveConfig(config);
147
- if (success) {
148
- // Reset failed attempts on successful PIN setup
149
- this.failedAttempts.clear();
150
- SecurityUtils.logSecurityEvent(
151
- 'admin_pin_setup',
152
- 'info',
153
- { message: 'Admin PIN configured successfully' }
154
- );
155
- }
156
- return success;
157
- } catch (error) {
158
- SecurityUtils.logSecurityEvent(
159
- 'admin_pin_setup_error',
160
- 'error',
161
- { message: `Failed to setup PIN: ${error.message}` }
162
- );
163
- return false;
164
- }
165
- }
166
-
167
- /**
168
- * Hash PIN with salt
169
- */
170
- hashPin(pin, salt) {
171
- return crypto.pbkdf2Sync(pin, salt, 100000, 64, 'sha512').toString('hex');
172
- }
173
-
174
- /**
175
- * Verify PIN
176
- */
177
- async verifyPin(pin) {
178
- try {
179
- const config = await this.loadConfig();
180
- if (!config || !config.enabled) {
181
- return true; // No authentication required if not enabled
182
- }
183
-
184
- // Check for lockout
185
- const clientId = 'local'; // In a real app, this would be client IP or session ID
186
- if (this.isLockedOut(clientId)) {
187
- SecurityUtils.logSecurityEvent(
188
- 'admin_auth_lockout',
189
- 'warning',
190
- { message: 'Authentication attempt during lockout period' }
191
- );
192
- return false;
193
- }
194
-
195
- // Validate PIN format
196
- if (!/^\d{4,6}$/.test(pin)) {
197
- this.recordFailedAttempt(clientId);
198
- SecurityUtils.logSecurityEvent(
199
- 'admin_auth_invalid_format',
200
- 'warning',
201
- { message: 'Invalid PIN format attempted' }
202
- );
203
- return false;
204
- }
205
-
206
- // Verify PIN
207
- const pinHash = this.hashPin(pin, config.salt);
208
- const isValid = pinHash === config.pinHash;
209
-
210
- if (isValid) {
211
- this.clearFailedAttempts(clientId);
212
- SecurityUtils.logSecurityEvent(
213
- 'admin_auth_success',
214
- 'info',
215
- { message: 'Admin authentication successful' }
216
- );
217
- return true;
218
- } else {
219
- this.recordFailedAttempt(clientId);
220
- SecurityUtils.logSecurityEvent(
221
- 'admin_auth_failure',
222
- 'warning',
223
- { message: 'Admin authentication failed' }
224
- );
225
- return false;
226
- }
227
- } catch (error) {
228
- SecurityUtils.logSecurityEvent(
229
- 'admin_auth_error',
230
- 'error',
231
- { message: `Authentication error: ${error.message}` }
232
- );
233
- return false;
234
- }
235
- }
236
-
237
- /**
238
- * Check if admin PIN is configured
239
- */
240
- async isPinConfigured() {
241
- const config = await this.loadConfig();
242
- return config && config.enabled && config.pinHash;
243
- }
244
-
245
- /**
246
- * Check if authentication is required
247
- */
248
- async isAuthRequired() {
249
- // Check if admin PIN is enabled in settings
250
- const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
251
- if (!(settings.security?.adminPinEnabled)) {
252
- return false;
253
- }
254
-
255
- const config = await this.loadConfig();
256
- return config && config.enabled;
257
- }
258
-
259
- /**
260
- * Check if authentication is required for a specific script
261
- */
262
- async isAuthRequiredForScript(scriptName) {
263
- // Check if admin PIN is enabled globally
264
- const globalSettings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
265
- if (!(globalSettings.security?.adminPinEnabled)) {
266
- return false;
267
- }
268
-
269
- // Check if admin PIN is actually configured
270
- const config = await this.loadConfig();
271
- if (!config || !config.enabled || !config.pinHash) {
272
- return false; // Don't require PIN if admin PIN is not configured
273
- }
274
-
275
- // Check if PIN protection is enabled
276
- const pinProtection = globalSettings.security?.pinProtection;
277
- if (!pinProtection || !pinProtection.enabled) {
278
- return false; // Don't require PIN if protection is disabled
279
- }
280
-
281
- // Check if this specific script requires protection
282
- const protectedScripts = pinProtection.protectedScripts || {};
283
- return protectedScripts[scriptName] !== false; // Default to true if not explicitly set
284
- }
285
-
286
- /**
287
- * Setup process handlers for session cleanup
288
- */
289
- setupProcessHandlers() {
290
- const cleanup = () => {
291
- this.clearCurrentSession();
292
- if (this.cleanupInterval) {
293
- clearInterval(this.cleanupInterval);
294
- }
295
- };
296
-
297
- // Handle various exit scenarios
298
- process.on('exit', cleanup);
299
- process.on('SIGINT', () => {
300
- cleanup();
301
- process.exit(0);
302
- });
303
- process.on('SIGTERM', () => {
304
- cleanup();
305
- process.exit(0);
306
- });
307
- process.on('uncaughtException', (error) => {
308
- SecurityUtils.logSecurityEvent('uncaught_exception', 'error', error.message);
309
- cleanup();
310
- process.exit(1);
311
- });
312
- }
313
-
314
- /**
315
- * Create a new authenticated session
316
- */
317
- async createSession(sessionId = null) {
318
- if (!sessionId) {
319
- sessionId = this.generateSessionId();
320
- }
321
-
322
- const session = {
323
- id: sessionId,
324
- created: new Date().toISOString(),
325
- lastActivity: new Date().toISOString(),
326
- expires: new Date(Date.now() + this.sessionTimeout).toISOString()
327
- };
328
-
329
- this.activeSessions.set(sessionId, session);
330
- this.currentSession = session;
331
- this.sessionStartTime = new Date();
332
-
333
- SecurityUtils.logSecurityEvent(
334
- 'session_created',
335
- 'info',
336
- { message: `Session ${sessionId} created` }
337
- );
338
- return sessionId;
339
- }
340
-
341
- /**
342
- * Validate current session
343
- */
344
- async validateSession(sessionId) {
345
- if (!sessionId || !this.currentSession) {
346
- return false;
347
- }
348
-
349
- if (sessionId !== this.currentSession.id) {
350
- return false;
351
- }
352
-
353
- const session = this.activeSessions.get(sessionId);
354
- if (!session) {
355
- this.clearCurrentSession();
356
- return false;
357
- }
358
-
359
- const now = new Date();
360
- const expires = new Date(session.expires);
361
-
362
- if (now > expires) {
363
- this.activeSessions.delete(sessionId);
364
- this.clearCurrentSession();
365
- SecurityUtils.logSecurityEvent(
366
- 'session_expired',
367
- 'info',
368
- { message: `Session ${sessionId} expired` }
369
- );
370
- return false;
371
- }
372
-
373
- // Update last activity
374
- session.lastActivity = now.toISOString();
375
- session.expires = new Date(now.getTime() + this.sessionTimeout).toISOString();
376
- this.activeSessions.set(sessionId, session);
377
-
378
- return true;
379
- }
380
-
381
- /**
382
- * Clear current session
383
- */
384
- clearCurrentSession() {
385
- if (this.currentSession) {
386
- this.activeSessions.delete(this.currentSession.id);
387
- SecurityUtils.logSecurityEvent(
388
- 'session_cleared',
389
- 'info',
390
- { message: `Session ${this.currentSession.id} cleared` }
391
- );
392
- }
393
- this.currentSession = null;
394
- this.sessionStartTime = null;
395
- }
396
-
397
- /**
398
- * Check if currently authenticated
399
- */
400
- isCurrentlyAuthenticated() {
401
- return this.currentSession !== null;
402
- }
403
-
404
- /**
405
- * Get current session info
406
- */
407
- getCurrentSessionInfo() {
408
- if (!this.currentSession) {
409
- return null;
410
- }
411
-
412
- return {
413
- sessionId: this.currentSession.id,
414
- started: this.sessionStartTime,
415
- expires: new Date(this.currentSession.expires),
416
- duration: Date.now() - this.sessionStartTime.getTime()
417
- };
418
- }
419
-
420
- /**
421
- * Generate secure session ID
422
- */
423
- generateSessionId() {
424
- return crypto.randomBytes(16).toString('hex');
425
- }
426
-
427
- /**
428
- * Disable admin authentication (completely removes PIN)
429
- */
430
- async disableAuth() {
431
- try {
432
- const config = await this.loadConfig();
433
- if (config) {
434
- config.enabled = false;
435
- config.pinHash = null;
436
- config.salt = null;
437
- config.lastModified = new Date().toISOString();
438
- const success = await this.saveConfig(config);
439
- if (success) {
440
- SecurityUtils.logSecurityEvent(
441
- 'admin_auth_disabled',
442
- 'info',
443
- { message: 'Admin authentication disabled' }
444
- );
445
- }
446
- return success;
447
- }
448
- return true;
449
- } catch (error) {
450
- SecurityUtils.logSecurityEvent(
451
- 'admin_auth_disable_error',
452
- 'error',
453
- { message: `Failed to disable auth: ${error.message}` }
454
- );
455
- return false;
456
- }
457
- }
458
-
459
- /**
460
- * Disable PIN protection (keeps PIN for future re-enable)
461
- */
462
- async disablePinProtection() {
463
- try {
464
- const config = await this.loadConfig();
465
- if (config) {
466
- config.enabled = false;
467
- config.lastModified = new Date().toISOString();
468
- const success = await this.saveConfig(config);
469
- if (success) {
470
- SecurityUtils.logSecurityEvent('pin_protection_disabled', 'info', 'PIN protection disabled (PIN retained)');
471
- }
472
- return success;
473
- }
474
- return true;
475
- } catch (error) {
476
- SecurityUtils.logSecurityEvent('pin_protection_disable_error', 'error', `Failed to disable PIN protection: ${error.message}`);
477
- return false;
478
- }
479
- }
480
-
481
- /**
482
- * Enable PIN protection (requires PIN to be already set)
483
- */
484
- async enablePinProtection() {
485
- try {
486
- const config = await this.loadConfig();
487
- if (config && config.pinHash) {
488
- config.enabled = true;
489
- config.lastModified = new Date().toISOString();
490
- const success = await this.saveConfig(config);
491
- if (success) {
492
- SecurityUtils.logSecurityEvent('pin_protection_enabled', 'info', 'PIN protection enabled');
493
- }
494
- return success;
495
- }
496
- return false;
497
- } catch (error) {
498
- SecurityUtils.logSecurityEvent('pin_protection_enable_error', 'error', `Failed to enable PIN protection: ${error.message}`);
499
- return false;
500
- }
501
- }
502
-
503
- /**
504
- * Record failed authentication attempt
505
- */
506
- recordFailedAttempt(clientId) {
507
- const now = Date.now();
508
- const attempts = this.failedAttempts.get(clientId) || [];
509
-
510
- // Remove old attempts (older than lockout duration)
511
- const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration);
512
- recentAttempts.push(now);
513
-
514
- this.failedAttempts.set(clientId, recentAttempts);
515
- }
516
-
517
- /**
518
- * Clear failed attempts for client
519
- */
520
- clearFailedAttempts(clientId) {
521
- this.failedAttempts.delete(clientId);
522
- }
523
-
524
- /**
525
- * Check if client is locked out
526
- */
527
- isLockedOut(clientId) {
528
- const attempts = this.failedAttempts.get(clientId) || [];
529
- const now = Date.now();
530
-
531
- // Remove old attempts
532
- const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration);
533
- this.failedAttempts.set(clientId, recentAttempts);
534
-
535
- return recentAttempts.length >= this.maxAttempts;
536
- }
537
-
538
- /**
539
- * Destroy session
540
- */
541
- destroySession(sessionId) {
542
- const deleted = this.activeSessions.delete(sessionId);
543
- if (deleted) {
544
- SecurityUtils.logSecurityEvent('admin_session_destroyed', 'info', 'Admin session destroyed');
545
- }
546
- return deleted;
547
- }
548
-
549
- /**
550
- * Clean up expired sessions
551
- */
552
- cleanupExpiredSessions() {
553
- const now = Date.now();
554
- let cleaned = 0;
555
-
556
- for (const [sessionId, session] of this.activeSessions.entries()) {
557
- if (now > session.expiresAt) {
558
- this.activeSessions.delete(sessionId);
559
- cleaned++;
560
- }
561
- }
562
-
563
- if (cleaned > 0) {
564
- SecurityUtils.logSecurityEvent('admin_sessions_cleaned', 'info', `Cleaned up ${cleaned} expired sessions`);
565
- }
566
- }
567
-
568
- /**
569
- * Clean up expired sessions (alias for backward compatibility)
570
- */
571
- cleanupSessions() {
572
- return this.cleanupExpiredSessions();
573
- }
574
- }
575
-
576
- module.exports = AdminAuth;
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const SecurityUtils = require('./security');
5
+ const configManager = require('./config-manager');
6
+
7
+ /**
8
+ * Admin Authentication Module
9
+ * Provides secure PIN-based authentication for administrative operations
10
+ */
11
+ class AdminAuth {
12
+ constructor() {
13
+ const packageRoot = path.resolve(__dirname, '..');
14
+ this.configPath = path.join(packageRoot, '.i18n-admin-config.json');
15
+
16
+ // Get settings from config manager
17
+ const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
18
+ const securitySettings = settings.security || {};
19
+ this.sessionTimeout = (securitySettings.sessionTimeout || 30) * 60 * 1000; // Convert minutes to milliseconds
20
+ this.maxAttempts = securitySettings.maxFailedAttempts || 3;
21
+ this.lockoutDuration = (securitySettings.lockoutDuration || 15) * 60 * 1000; // Convert minutes to milliseconds
22
+ this.keepAuthenticatedUntilExit = securitySettings.keepAuthenticatedUntilExit !== false;
23
+
24
+ this.activeSessions = new Map();
25
+ this.failedAttempts = new Map();
26
+ this.lockouts = new Map();
27
+ this.currentSession = null;
28
+ this.sessionStartTime = null;
29
+
30
+ // Clean up expired sessions every 5 minutes
31
+ this.cleanupInterval = setInterval(this.cleanupExpiredSessions.bind(this), 5 * 60 * 1000);
32
+ if (typeof this.cleanupInterval.unref === 'function') {
33
+ this.cleanupInterval.unref();
34
+ }
35
+
36
+ // Handle process exit to ensure session cleanup
37
+ this.setupProcessHandlers();
38
+ }
39
+
40
+ /**
41
+ * Initialize admin authentication system
42
+ */
43
+ async initialize() {
44
+ try {
45
+ if (!SecurityUtils.safeExistsSync(this.configPath)) {
46
+ // Create default config if it doesn't exist
47
+ const defaultConfig = {
48
+ enabled: false,
49
+ pinHash: null,
50
+ salt: null,
51
+ createdAt: new Date().toISOString(),
52
+ lastModified: new Date().toISOString()
53
+ };
54
+ await this.saveConfig(defaultConfig);
55
+ }
56
+
57
+ SecurityUtils.logSecurityEvent(
58
+ 'admin_auth_initialized',
59
+ 'info',
60
+ { message: 'Admin authentication system initialized' }
61
+ );
62
+ return true;
63
+ } catch (error) {
64
+ SecurityUtils.logSecurityEvent(
65
+ 'admin_auth_init_error',
66
+ 'error',
67
+ { message: `Failed to initialize admin auth: ${error.message}` }
68
+ );
69
+ return false;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Cleanup resources and stop intervals
75
+ */
76
+ async cleanup() {
77
+ if (this.cleanupInterval) {
78
+ clearInterval(this.cleanupInterval);
79
+ this.cleanupInterval = null;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Load admin configuration
85
+ */
86
+ async loadConfig() {
87
+ try {
88
+ if (!SecurityUtils.safeExistsSync(this.configPath)) {
89
+ return null;
90
+ }
91
+
92
+ const content = await fs.promises.readFile(this.configPath, 'utf8');
93
+ return SecurityUtils.safeParseJSON(content);
94
+ } catch (error) {
95
+ SecurityUtils.logSecurityEvent(
96
+ 'admin_config_load_error',
97
+ 'error',
98
+ { message: `Failed to load admin config: ${error.message}` }
99
+ );
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Save admin configuration
106
+ */
107
+ async saveConfig(config) {
108
+ try {
109
+ const content = JSON.stringify(config, null, 2);
110
+ await fs.promises.writeFile(this.configPath, content, { mode: 0o600 }); // Restrict permissions
111
+ SecurityUtils.logSecurityEvent(
112
+ 'admin_config_saved',
113
+ 'info',
114
+ { message: 'Admin configuration saved' }
115
+ );
116
+ return true;
117
+ } catch (error) {
118
+ SecurityUtils.logSecurityEvent(
119
+ 'admin_config_save_error',
120
+ 'error',
121
+ { message: `Failed to save admin config: ${error.message}` }
122
+ );
123
+ return false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Set up admin PIN
129
+ */
130
+ async setupPin(pin) {
131
+ try {
132
+ // Validate PIN format (4-6 digits)
133
+ if (!/^\d{4,6}$/.test(pin)) {
134
+ throw new Error('PIN must be 4-6 digits');
135
+ }
136
+
137
+ // Generate salt and hash
138
+ const salt = crypto.randomBytes(32).toString('hex');
139
+ const pinHash = this.hashPin(pin, salt);
140
+
141
+ const config = {
142
+ enabled: true,
143
+ pinHash,
144
+ salt,
145
+ createdAt: new Date().toISOString(),
146
+ lastModified: new Date().toISOString()
147
+ };
148
+
149
+ const success = await this.saveConfig(config);
150
+ if (success) {
151
+ // Reset failed attempts on successful PIN setup
152
+ this.failedAttempts.clear();
153
+ SecurityUtils.logSecurityEvent(
154
+ 'admin_pin_setup',
155
+ 'info',
156
+ { message: 'Admin PIN configured successfully' }
157
+ );
158
+ }
159
+ return success;
160
+ } catch (error) {
161
+ SecurityUtils.logSecurityEvent(
162
+ 'admin_pin_setup_error',
163
+ 'error',
164
+ { message: `Failed to setup PIN: ${error.message}` }
165
+ );
166
+ return false;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Hash PIN with salt
172
+ */
173
+ hashPin(pin, salt) {
174
+ return crypto.pbkdf2Sync(pin, salt, 100000, 64, 'sha512').toString('hex');
175
+ }
176
+
177
+ timingSafeHexEqual(leftHex, rightHex) {
178
+ if (typeof leftHex !== 'string' || typeof rightHex !== 'string') {
179
+ return false;
180
+ }
181
+
182
+ const left = Buffer.from(leftHex, 'hex');
183
+ const right = Buffer.from(rightHex, 'hex');
184
+ if (left.length === 0 || left.length !== right.length) {
185
+ return false;
186
+ }
187
+
188
+ return crypto.timingSafeEqual(left, right);
189
+ }
190
+
191
+ /**
192
+ * Verify PIN
193
+ */
194
+ async verifyPin(pin) {
195
+ try {
196
+ const config = await this.loadConfig();
197
+ if (!config || !config.enabled) {
198
+ return true; // No authentication required if not enabled
199
+ }
200
+
201
+ // Check for lockout
202
+ const clientId = 'local'; // In a real app, this would be client IP or session ID
203
+ if (this.isLockedOut(clientId)) {
204
+ SecurityUtils.logSecurityEvent(
205
+ 'admin_auth_lockout',
206
+ 'warning',
207
+ { message: 'Authentication attempt during lockout period' }
208
+ );
209
+ return false;
210
+ }
211
+
212
+ // Validate PIN format
213
+ if (!/^\d{4,6}$/.test(pin)) {
214
+ this.recordFailedAttempt(clientId);
215
+ SecurityUtils.logSecurityEvent(
216
+ 'admin_auth_invalid_format',
217
+ 'warning',
218
+ { message: 'Invalid PIN format attempted' }
219
+ );
220
+ return false;
221
+ }
222
+
223
+ // Verify PIN
224
+ const pinHash = this.hashPin(pin, config.salt);
225
+ const isValid = this.timingSafeHexEqual(pinHash, config.pinHash);
226
+
227
+ if (isValid) {
228
+ this.clearFailedAttempts(clientId);
229
+ SecurityUtils.logSecurityEvent(
230
+ 'admin_auth_success',
231
+ 'info',
232
+ { message: 'Admin authentication successful' }
233
+ );
234
+ return true;
235
+ } else {
236
+ this.recordFailedAttempt(clientId);
237
+ SecurityUtils.logSecurityEvent(
238
+ 'admin_auth_failure',
239
+ 'warning',
240
+ { message: 'Admin authentication failed' }
241
+ );
242
+ return false;
243
+ }
244
+ } catch (error) {
245
+ SecurityUtils.logSecurityEvent(
246
+ 'admin_auth_error',
247
+ 'error',
248
+ { message: `Authentication error: ${error.message}` }
249
+ );
250
+ return false;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Check if admin PIN is configured
256
+ */
257
+ async isPinConfigured() {
258
+ const config = await this.loadConfig();
259
+ return config && config.enabled && config.pinHash;
260
+ }
261
+
262
+ /**
263
+ * Check if authentication is required
264
+ */
265
+ async isAuthRequired() {
266
+ // Check if admin PIN is enabled in settings
267
+ const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
268
+ if (!(settings.security?.adminPinEnabled)) {
269
+ return false;
270
+ }
271
+
272
+ const config = await this.loadConfig();
273
+ return config && config.enabled;
274
+ }
275
+
276
+ /**
277
+ * Check if authentication is required for a specific script
278
+ */
279
+ async isAuthRequiredForScript(scriptName) {
280
+ // Check if admin PIN is enabled globally
281
+ const globalSettings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
282
+ if (!(globalSettings.security?.adminPinEnabled)) {
283
+ return false;
284
+ }
285
+
286
+ // Check if admin PIN is actually configured
287
+ const config = await this.loadConfig();
288
+ if (!config || !config.enabled || !config.pinHash) {
289
+ return false; // Don't require PIN if admin PIN is not configured
290
+ }
291
+
292
+ // Check if PIN protection is enabled
293
+ const pinProtection = globalSettings.security?.pinProtection;
294
+ if (!pinProtection || !pinProtection.enabled) {
295
+ return false; // Don't require PIN if protection is disabled
296
+ }
297
+
298
+ // Check if this specific script requires protection
299
+ const protectedScripts = pinProtection.protectedScripts || {};
300
+ return protectedScripts[scriptName] !== false; // Default to true if not explicitly set
301
+ }
302
+
303
+ /**
304
+ * Setup process handlers for session cleanup
305
+ */
306
+ setupProcessHandlers() {
307
+ const cleanup = () => {
308
+ this.clearCurrentSession();
309
+ if (this.cleanupInterval) {
310
+ clearInterval(this.cleanupInterval);
311
+ }
312
+ };
313
+
314
+ // Handle various exit scenarios
315
+ process.on('exit', cleanup);
316
+ process.on('SIGINT', () => {
317
+ cleanup();
318
+ process.exit(0);
319
+ });
320
+ process.on('SIGTERM', () => {
321
+ cleanup();
322
+ process.exit(0);
323
+ });
324
+ process.on('uncaughtException', (error) => {
325
+ SecurityUtils.logSecurityEvent('uncaught_exception', 'error', error.message);
326
+ cleanup();
327
+ process.exit(1);
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Create a new authenticated session
333
+ */
334
+ async createSession(sessionId = null) {
335
+ if (!sessionId) {
336
+ sessionId = this.generateSessionId();
337
+ }
338
+
339
+ const session = {
340
+ id: sessionId,
341
+ created: new Date().toISOString(),
342
+ lastActivity: new Date().toISOString(),
343
+ expires: new Date(Date.now() + this.sessionTimeout).toISOString()
344
+ };
345
+
346
+ this.activeSessions.set(sessionId, session);
347
+ this.currentSession = session;
348
+ this.sessionStartTime = new Date();
349
+
350
+ SecurityUtils.logSecurityEvent(
351
+ 'session_created',
352
+ 'info',
353
+ { message: `Session ${sessionId} created` }
354
+ );
355
+ return sessionId;
356
+ }
357
+
358
+ /**
359
+ * Validate current session
360
+ */
361
+ async validateSession(sessionId) {
362
+ if (!sessionId || !this.currentSession) {
363
+ return false;
364
+ }
365
+
366
+ if (sessionId !== this.currentSession.id) {
367
+ return false;
368
+ }
369
+
370
+ const session = this.activeSessions.get(sessionId);
371
+ if (!session) {
372
+ this.clearCurrentSession();
373
+ return false;
374
+ }
375
+
376
+ const now = new Date();
377
+ const expires = new Date(session.expires);
378
+
379
+ if (now > expires) {
380
+ this.activeSessions.delete(sessionId);
381
+ this.clearCurrentSession();
382
+ SecurityUtils.logSecurityEvent(
383
+ 'session_expired',
384
+ 'info',
385
+ { message: `Session ${sessionId} expired` }
386
+ );
387
+ return false;
388
+ }
389
+
390
+ // Update last activity
391
+ session.lastActivity = now.toISOString();
392
+ session.expires = new Date(now.getTime() + this.sessionTimeout).toISOString();
393
+ this.activeSessions.set(sessionId, session);
394
+
395
+ return true;
396
+ }
397
+
398
+ /**
399
+ * Clear current session
400
+ */
401
+ clearCurrentSession() {
402
+ if (this.currentSession) {
403
+ this.activeSessions.delete(this.currentSession.id);
404
+ SecurityUtils.logSecurityEvent(
405
+ 'session_cleared',
406
+ 'info',
407
+ { message: `Session ${this.currentSession.id} cleared` }
408
+ );
409
+ }
410
+ this.currentSession = null;
411
+ this.sessionStartTime = null;
412
+ }
413
+
414
+ /**
415
+ * Check if currently authenticated
416
+ */
417
+ isCurrentlyAuthenticated() {
418
+ return this.currentSession !== null;
419
+ }
420
+
421
+ /**
422
+ * Get current session info
423
+ */
424
+ getCurrentSessionInfo() {
425
+ if (!this.currentSession) {
426
+ return null;
427
+ }
428
+
429
+ return {
430
+ sessionId: this.currentSession.id,
431
+ started: this.sessionStartTime,
432
+ expires: new Date(this.currentSession.expires),
433
+ duration: Date.now() - this.sessionStartTime.getTime()
434
+ };
435
+ }
436
+
437
+ /**
438
+ * Generate secure session ID
439
+ */
440
+ generateSessionId() {
441
+ return crypto.randomBytes(16).toString('hex');
442
+ }
443
+
444
+ /**
445
+ * Disable admin authentication (completely removes PIN)
446
+ */
447
+ async disableAuth() {
448
+ try {
449
+ const config = await this.loadConfig();
450
+ if (config) {
451
+ config.enabled = false;
452
+ config.pinHash = null;
453
+ config.salt = null;
454
+ config.lastModified = new Date().toISOString();
455
+ const success = await this.saveConfig(config);
456
+ if (success) {
457
+ SecurityUtils.logSecurityEvent(
458
+ 'admin_auth_disabled',
459
+ 'info',
460
+ { message: 'Admin authentication disabled' }
461
+ );
462
+ }
463
+ return success;
464
+ }
465
+ return true;
466
+ } catch (error) {
467
+ SecurityUtils.logSecurityEvent(
468
+ 'admin_auth_disable_error',
469
+ 'error',
470
+ { message: `Failed to disable auth: ${error.message}` }
471
+ );
472
+ return false;
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Disable PIN protection (keeps PIN for future re-enable)
478
+ */
479
+ async disablePinProtection() {
480
+ try {
481
+ const config = await this.loadConfig();
482
+ if (config) {
483
+ config.enabled = false;
484
+ config.lastModified = new Date().toISOString();
485
+ const success = await this.saveConfig(config);
486
+ if (success) {
487
+ SecurityUtils.logSecurityEvent('pin_protection_disabled', 'info', 'PIN protection disabled (PIN retained)');
488
+ }
489
+ return success;
490
+ }
491
+ return true;
492
+ } catch (error) {
493
+ SecurityUtils.logSecurityEvent('pin_protection_disable_error', 'error', `Failed to disable PIN protection: ${error.message}`);
494
+ return false;
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Enable PIN protection (requires PIN to be already set)
500
+ */
501
+ async enablePinProtection() {
502
+ try {
503
+ const config = await this.loadConfig();
504
+ if (config && config.pinHash) {
505
+ config.enabled = true;
506
+ config.lastModified = new Date().toISOString();
507
+ const success = await this.saveConfig(config);
508
+ if (success) {
509
+ SecurityUtils.logSecurityEvent('pin_protection_enabled', 'info', 'PIN protection enabled');
510
+ }
511
+ return success;
512
+ }
513
+ return false;
514
+ } catch (error) {
515
+ SecurityUtils.logSecurityEvent('pin_protection_enable_error', 'error', `Failed to enable PIN protection: ${error.message}`);
516
+ return false;
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Record failed authentication attempt
522
+ */
523
+ recordFailedAttempt(clientId) {
524
+ const now = Date.now();
525
+ const attempts = this.failedAttempts.get(clientId) || [];
526
+
527
+ // Remove old attempts (older than lockout duration)
528
+ const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration);
529
+ recentAttempts.push(now);
530
+
531
+ this.failedAttempts.set(clientId, recentAttempts);
532
+ }
533
+
534
+ /**
535
+ * Clear failed attempts for client
536
+ */
537
+ clearFailedAttempts(clientId) {
538
+ this.failedAttempts.delete(clientId);
539
+ }
540
+
541
+ /**
542
+ * Check if client is locked out
543
+ */
544
+ isLockedOut(clientId) {
545
+ const attempts = this.failedAttempts.get(clientId) || [];
546
+ const now = Date.now();
547
+
548
+ // Remove old attempts
549
+ const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration);
550
+ this.failedAttempts.set(clientId, recentAttempts);
551
+
552
+ return recentAttempts.length >= this.maxAttempts;
553
+ }
554
+
555
+ /**
556
+ * Destroy session
557
+ */
558
+ destroySession(sessionId) {
559
+ const deleted = this.activeSessions.delete(sessionId);
560
+ if (deleted) {
561
+ SecurityUtils.logSecurityEvent('admin_session_destroyed', 'info', 'Admin session destroyed');
562
+ }
563
+ return deleted;
564
+ }
565
+
566
+ /**
567
+ * Clean up expired sessions
568
+ */
569
+ cleanupExpiredSessions() {
570
+ const now = Date.now();
571
+ let cleaned = 0;
572
+
573
+ for (const [sessionId, session] of this.activeSessions.entries()) {
574
+ const expiresAt = session.expiresAt || new Date(session.expires).getTime();
575
+ if (!Number.isFinite(expiresAt) || now > expiresAt) {
576
+ this.activeSessions.delete(sessionId);
577
+ cleaned++;
578
+ }
579
+ }
580
+
581
+ if (cleaned > 0) {
582
+ SecurityUtils.logSecurityEvent('admin_sessions_cleaned', 'info', `Cleaned up ${cleaned} expired sessions`);
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Clean up expired sessions (alias for backward compatibility)
588
+ */
589
+ cleanupSessions() {
590
+ return this.cleanupExpiredSessions();
591
+ }
592
+ }
593
+
594
+ module.exports = AdminAuth;