UNPKG

i18ntk

Version:

i18ntk (i18n Toolkit) - A comprehensive, enterprise-grade internationalization (i18n) management toolkit for JavaScript/TypeScript projects with advanced analysis, validation, and automation features

497 lines (431 loc) 14.1 kB
const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const SecurityUtils = require('./security'); const SettingsManager = require('../settings/settings-manager'); /** * Admin Authentication Module * Provides secure PIN-based authentication for administrative operations */ class AdminAuth { constructor() { this.configPath = path.join(process.cwd(), 'settings', '.i18n-admin-config.json'); this.settingsManager = SettingsManager; // Get settings from SettingsManager const securitySettings = this.settingsManager.getSecurity(); this.sessionTimeout = (securitySettings.sessionTimeout || 30) * 60 * 1000; // Convert minutes to milliseconds this.maxAttempts = securitySettings.maxFailedAttempts || 3; this.lockoutDuration = (securitySettings.lockoutDuration || 15) * 60 * 1000; // Convert minutes to milliseconds this.keepAuthenticatedUntilExit = securitySettings.keepAuthenticatedUntilExit !== false; this.activeSessions = new Map(); this.failedAttempts = new Map(); this.lockouts = new Map(); this.currentSession = null; this.sessionStartTime = null; // Clean up expired sessions every 5 minutes this.cleanupInterval = setInterval(this.cleanupExpiredSessions.bind(this), 5 * 60 * 1000); // Handle process exit to ensure session cleanup this.setupProcessHandlers(); } /** * Initialize admin authentication system */ async initialize() { try { if (!fs.existsSync(this.configPath)) { // Create default config if it doesn't exist const defaultConfig = { enabled: false, pinHash: null, salt: null, createdAt: new Date().toISOString(), lastModified: new Date().toISOString() }; await this.saveConfig(defaultConfig); } SecurityUtils.logSecurityEvent('admin_auth_initialized', 'info', 'Admin authentication system initialized'); return true; } catch (error) { SecurityUtils.logSecurityEvent('admin_auth_init_error', 'error', `Failed to initialize admin auth: ${error.message}`); return false; } } /** * Cleanup resources and stop intervals */ async cleanup() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } } /** * Load admin configuration */ async loadConfig() { try { if (!fs.existsSync(this.configPath)) { return null; } const content = await fs.promises.readFile(this.configPath, 'utf8'); return SecurityUtils.safeParseJSON(content); } catch (error) { SecurityUtils.logSecurityEvent('admin_config_load_error', 'error', `Failed to load admin config: ${error.message}`); return null; } } /** * Save admin configuration */ async saveConfig(config) { try { const content = JSON.stringify(config, null, 2); await fs.promises.writeFile(this.configPath, content, { mode: 0o600 }); // Restrict permissions SecurityUtils.logSecurityEvent('admin_config_saved', 'info', 'Admin configuration saved'); return true; } catch (error) { SecurityUtils.logSecurityEvent('admin_config_save_error', 'error', `Failed to save admin config: ${error.message}`); return false; } } /** * Set up admin PIN */ async setupPin(pin) { try { // Validate PIN format (4-6 digits) if (!/^\d{4,6}$/.test(pin)) { throw new Error('PIN must be 4-6 digits'); } // Generate salt and hash const salt = crypto.randomBytes(32).toString('hex'); const pinHash = this.hashPin(pin, salt); const config = { enabled: true, pinHash, salt, createdAt: new Date().toISOString(), lastModified: new Date().toISOString() }; const success = await this.saveConfig(config); if (success) { SecurityUtils.logSecurityEvent('admin_pin_setup', 'info', 'Admin PIN configured successfully'); } return success; } catch (error) { SecurityUtils.logSecurityEvent('admin_pin_setup_error', 'error', `Failed to setup PIN: ${error.message}`); return false; } } /** * Hash PIN with salt */ hashPin(pin, salt) { return crypto.pbkdf2Sync(pin, salt, 100000, 64, 'sha512').toString('hex'); } /** * Verify PIN */ async verifyPin(pin) { try { const config = await this.loadConfig(); if (!config || !config.enabled) { return true; // No authentication required if not enabled } // Check for lockout const clientId = 'local'; // In a real app, this would be client IP or session ID if (this.isLockedOut(clientId)) { SecurityUtils.logSecurityEvent('admin_auth_lockout', 'warning', 'Authentication attempt during lockout period'); return false; } // Validate PIN format if (!/^\d{4,6}$/.test(pin)) { this.recordFailedAttempt(clientId); SecurityUtils.logSecurityEvent('admin_auth_invalid_format', 'warning', 'Invalid PIN format attempted'); return false; } // Verify PIN const pinHash = this.hashPin(pin, config.salt); const isValid = pinHash === config.pinHash; if (isValid) { this.clearFailedAttempts(clientId); SecurityUtils.logSecurityEvent('admin_auth_success', 'info', 'Admin authentication successful'); return true; } else { this.recordFailedAttempt(clientId); SecurityUtils.logSecurityEvent('admin_auth_failure', 'warning', 'Admin authentication failed'); return false; } } catch (error) { SecurityUtils.logSecurityEvent('admin_auth_error', 'error', `Authentication error: ${error.message}`); return false; } } /** * Check if admin PIN is configured */ async isPinConfigured() { const config = await this.loadConfig(); return config && config.enabled && config.pinHash; } /** * Check if authentication is required */ async isAuthRequired() { // Check if admin PIN is enabled in settings if (!this.settingsManager.isAdminPinEnabled()) { return false; } const config = await this.loadConfig(); return config && config.enabled; } /** * Check if authentication is required for a specific script */ async isAuthRequiredForScript(scriptName) { // Check if admin PIN is enabled globally if (!this.settingsManager.isAdminPinEnabled()) { return false; } // Check if admin PIN is actually configured const config = await this.loadConfig(); if (!config || !config.enabled || !config.pinHash) { return false; // Don't require PIN if admin PIN is not configured } // Check if PIN protection is enabled const pinProtection = this.settingsManager.getSetting('security.pinProtection'); if (!pinProtection || !pinProtection.enabled) { return false; // Don't require PIN if protection is disabled } // Check if this specific script requires protection const protectedScripts = pinProtection.protectedScripts || {}; return protectedScripts[scriptName] !== false; // Default to true if not explicitly set } /** * Setup process handlers for session cleanup */ setupProcessHandlers() { const cleanup = () => { this.clearCurrentSession(); if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } }; // Handle various exit scenarios process.on('exit', cleanup); process.on('SIGINT', () => { cleanup(); process.exit(0); }); process.on('SIGTERM', () => { cleanup(); process.exit(0); }); process.on('uncaughtException', (error) => { SecurityUtils.logSecurityEvent('uncaught_exception', 'error', error.message); cleanup(); process.exit(1); }); } /** * Create a new authenticated session */ async createSession(sessionId = null) { if (!sessionId) { sessionId = this.generateSessionId(); } const session = { id: sessionId, created: new Date().toISOString(), lastActivity: new Date().toISOString(), expires: new Date(Date.now() + this.sessionTimeout).toISOString() }; this.activeSessions.set(sessionId, session); this.currentSession = session; this.sessionStartTime = new Date(); SecurityUtils.logSecurityEvent('session_created', 'info', `Session ${sessionId} created`); return sessionId; } /** * Validate current session */ async validateSession(sessionId) { if (!sessionId || !this.currentSession) { return false; } if (sessionId !== this.currentSession.id) { return false; } const session = this.activeSessions.get(sessionId); if (!session) { this.clearCurrentSession(); return false; } const now = new Date(); const expires = new Date(session.expires); if (now > expires) { this.activeSessions.delete(sessionId); this.clearCurrentSession(); SecurityUtils.logSecurityEvent('session_expired', 'info', `Session ${sessionId} expired`); return false; } // Update last activity session.lastActivity = now.toISOString(); session.expires = new Date(now.getTime() + this.sessionTimeout).toISOString(); this.activeSessions.set(sessionId, session); return true; } /** * Clear current session */ clearCurrentSession() { if (this.currentSession) { this.activeSessions.delete(this.currentSession.id); SecurityUtils.logSecurityEvent('session_cleared', 'info', `Session ${this.currentSession.id} cleared`); } this.currentSession = null; this.sessionStartTime = null; } /** * Check if currently authenticated */ isCurrentlyAuthenticated() { return this.currentSession !== null; } /** * Get current session info */ getCurrentSessionInfo() { if (!this.currentSession) { return null; } return { sessionId: this.currentSession.id, started: this.sessionStartTime, expires: new Date(this.currentSession.expires), duration: Date.now() - this.sessionStartTime.getTime() }; } /** * Generate secure session ID */ generateSessionId() { return crypto.randomBytes(16).toString('hex'); } /** * Disable admin authentication */ async disableAuth() { try { const config = await this.loadConfig(); if (config) { config.enabled = false; config.pinHash = null; config.salt = null; config.lastModified = new Date().toISOString(); const success = await this.saveConfig(config); if (success) { SecurityUtils.logSecurityEvent('admin_auth_disabled', 'info', 'Admin authentication disabled'); } return success; } return true; } catch (error) { SecurityUtils.logSecurityEvent('admin_auth_disable_error', 'error', `Failed to disable auth: ${error.message}`); return false; } } /** * Record failed authentication attempt */ recordFailedAttempt(clientId) { const now = Date.now(); const attempts = this.failedAttempts.get(clientId) || []; // Remove old attempts (older than lockout duration) const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration); recentAttempts.push(now); this.failedAttempts.set(clientId, recentAttempts); } /** * Clear failed attempts for client */ clearFailedAttempts(clientId) { this.failedAttempts.delete(clientId); } /** * Check if client is locked out */ isLockedOut(clientId) { const attempts = this.failedAttempts.get(clientId) || []; const now = Date.now(); // Remove old attempts const recentAttempts = attempts.filter(time => now - time < this.lockoutDuration); this.failedAttempts.set(clientId, recentAttempts); return recentAttempts.length >= this.maxAttempts; } /** * Create authenticated session */ createSession() { const sessionId = crypto.randomBytes(32).toString('hex'); const expiresAt = Date.now() + this.sessionTimeout; this.activeSessions.set(sessionId, { createdAt: Date.now(), expiresAt, lastActivity: Date.now() }); SecurityUtils.logSecurityEvent('admin_session_created', 'info', 'Admin session created'); return sessionId; } /** * Validate session */ validateSession(sessionId) { const session = this.activeSessions.get(sessionId); if (!session) { return false; } const now = Date.now(); if (now > session.expiresAt) { this.activeSessions.delete(sessionId); SecurityUtils.logSecurityEvent('admin_session_expired', 'info', 'Admin session expired'); return false; } // Update last activity session.lastActivity = now; return true; } /** * Destroy session */ destroySession(sessionId) { const deleted = this.activeSessions.delete(sessionId); if (deleted) { SecurityUtils.logSecurityEvent('admin_session_destroyed', 'info', 'Admin session destroyed'); } return deleted; } /** * Clean up expired sessions */ cleanupExpiredSessions() { const now = Date.now(); let cleaned = 0; for (const [sessionId, session] of this.activeSessions.entries()) { if (now > session.expiresAt) { this.activeSessions.delete(sessionId); cleaned++; } } if (cleaned > 0) { SecurityUtils.logSecurityEvent('admin_sessions_cleaned', 'info', `Cleaned up ${cleaned} expired sessions`); } } /** * Clean up expired sessions (alias for backward compatibility) */ cleanupSessions() { return this.cleanupExpiredSessions(); } } module.exports = AdminAuth;