@ufdevsllc/authme2.0
Version:
SDK for license management and remote monitoring with automatic system tracking, license validation, and remote control capabilities
726 lines (632 loc) • 24.3 kB
JavaScript
/**
* Centralized Error Handling and Logging System
* Provides comprehensive error handling, logging, and retry mechanisms
*/
class ErrorHandler {
constructor() {
this.databaseManager = null; // Will be injected if needed
this.logLevels = {
ERROR: 'error',
WARN: 'warn',
INFO: 'info',
DEBUG: 'debug'
};
this.maxRetries = 3;
this.baseRetryDelay = 1000; // 1 second
this.errorCounts = new Map();
this.circuitBreakers = new Map();
}
/**
* Initialize error handler with database connection
* @param {Object} databaseManager - Optional database manager instance
* @returns {Promise<void>}
*/
async initialize(databaseManager = null) {
if (databaseManager) {
this.databaseManager = databaseManager;
}
if (this.databaseManager) {
try {
await this.databaseManager.initMonitoringConnection();
} catch (error) {
console.error('Failed to initialize error handler database connection:', error.message);
// Don't throw here - error handler should work even without database
}
}
}
/**
* Handle and log errors with context
* @param {Error} error - The error to handle
* @param {Object} context - Additional context information
* @param {string} severity - Error severity level
* @returns {Promise<Object>} Error handling result
*/
async handleError(error, context = {}, severity = this.logLevels.ERROR) {
const errorId = this._generateErrorId();
const timestamp = new Date();
const errorInfo = {
id: errorId,
message: error.message || 'Unknown error',
stack: error.stack,
name: error.name || 'Error',
code: error.code,
severity,
timestamp,
context: this._sanitizeContext(context),
component: context.component || 'unknown',
operation: context.operation || 'unknown',
licenseKey: context.licenseKey ? this._maskLicenseKey(context.licenseKey) : null
};
// Log to console
this._logToConsole(errorInfo);
// Track error frequency
this._trackErrorFrequency(errorInfo);
// Log to database if available
try {
await this._logToDatabase(errorInfo);
} catch (dbError) {
console.error('Failed to log error to database:', dbError.message);
}
// Check if circuit breaker should be triggered
this._checkCircuitBreaker(errorInfo);
return {
errorId,
handled: true,
severity,
timestamp,
shouldRetry: this._shouldRetry(error, context),
circuitBreakerTriggered: this._isCircuitBreakerOpen(errorInfo.component)
};
}
/**
* Handle license validation errors with specific messaging
* @param {Error} error - License validation error
* @param {string} licenseKey - License key being validated
* @param {Object} validationContext - Validation context
* @returns {Promise<Object>} Formatted license error
*/
async handleLicenseError(error, licenseKey, validationContext = {}) {
const context = {
component: 'license-validator',
operation: 'license-validation',
licenseKey,
...validationContext
};
const result = await this.handleError(error, context, this.logLevels.ERROR);
// Create user-friendly license error messages
const licenseErrorMessages = {
'LICENSE_NOT_FOUND': 'The provided license key was not found. Please verify your license key.',
'LICENSE_EXPIRED': 'Your license has expired. Please renew your license to continue.',
'LICENSE_INACTIVE': 'Your license has been deactivated. Please contact support.',
'DISTRIBUTION_LIMIT_EXCEEDED': 'License distribution limit has been reached. Please upgrade your license.',
'VALIDATION_ERROR': 'License validation failed due to a technical error. Please try again.',
'CONNECTION_ERROR': 'Unable to validate license due to connection issues. Please check your internet connection.'
};
const errorType = this._determineLicenseErrorType(error);
const userMessage = licenseErrorMessages[errorType] || 'License validation failed. Please contact support.';
return {
...result,
errorType,
userMessage,
technicalMessage: error.message,
canRetry: ['CONNECTION_ERROR', 'VALIDATION_ERROR'].includes(errorType)
};
}
/**
* Handle database operation errors with retry logic
* @param {Error} error - Database error
* @param {Object} operation - Database operation details
* @param {number} attemptNumber - Current attempt number
* @returns {Promise<Object>} Database error handling result
*/
async handleDatabaseError(error, operation = {}, attemptNumber = 1) {
const context = {
component: 'database-manager',
operation: operation.type || 'database-operation',
collection: operation.collection,
attemptNumber,
maxRetries: this.maxRetries
};
const result = await this.handleError(error, context);
// Determine if this is a retryable database error
const retryableErrors = [
'MongoNetworkError',
'MongoTimeoutError',
'MongoServerSelectionError',
'ECONNRESET',
'ETIMEDOUT',
'ENOTFOUND'
];
const isRetryable = retryableErrors.some(retryableError =>
error.name === retryableError ||
error.code === retryableError ||
error.message.includes(retryableError)
);
const shouldRetry = isRetryable && attemptNumber < this.maxRetries;
const retryDelay = shouldRetry ? this._calculateRetryDelay(attemptNumber) : 0;
return {
...result,
isRetryable,
shouldRetry,
retryDelay,
attemptNumber,
maxRetries: this.maxRetries
};
}
/**
* Execute operation with retry logic and error handling
* @param {Function} operation - Operation to execute
* @param {Object} context - Operation context
* @param {Object} options - Retry options
* @returns {Promise<*>} Operation result
*/
async executeWithRetry(operation, context = {}, options = {}) {
const {
maxRetries = this.maxRetries,
baseDelay = this.baseRetryDelay,
backoffMultiplier = 2,
maxDelay = 30000
} = options;
let lastError;
let attemptNumber = 1;
while (attemptNumber <= maxRetries + 1) {
try {
const result = await operation();
// Log successful retry if this wasn't the first attempt
if (attemptNumber > 1) {
await this.logInfo(`Operation succeeded on attempt ${attemptNumber}`, {
...context,
operation: 'retry-success',
attemptNumber,
totalAttempts: attemptNumber
});
}
return result;
} catch (error) {
lastError = error;
const errorResult = await this.handleError(error, {
...context,
attemptNumber,
maxRetries: maxRetries + 1
});
// If this is the last attempt or error is not retryable, throw
if (attemptNumber > maxRetries || !errorResult.shouldRetry) {
throw error;
}
// Calculate delay for next attempt
const delay = Math.min(
baseDelay * Math.pow(backoffMultiplier, attemptNumber - 1),
maxDelay
);
await this.logWarn(`Retrying operation in ${delay}ms (attempt ${attemptNumber + 1}/${maxRetries + 1})`, {
...context,
operation: 'retry-attempt',
attemptNumber,
delay,
error: error.message
});
await this._sleep(delay);
attemptNumber++;
}
}
throw lastError;
}
/**
* Validate and sanitize input data
* @param {*} data - Data to validate
* @param {Object} schema - Validation schema
* @param {Object} context - Validation context
* @returns {Object} Validation result
*/
validateInput(data, schema, context = {}) {
const errors = [];
const warnings = [];
try {
// Basic type validation
if (schema.type && typeof data !== schema.type) {
errors.push(`Expected ${schema.type}, got ${typeof data}`);
}
// Required field validation
if (schema.required && (data === null || data === undefined || data === '')) {
errors.push('Field is required');
}
// String validations
if (schema.type === 'string' && typeof data === 'string') {
if (schema.minLength && data.length < schema.minLength) {
errors.push(`Minimum length is ${schema.minLength}, got ${data.length}`);
}
if (schema.maxLength && data.length > schema.maxLength) {
errors.push(`Maximum length is ${schema.maxLength}, got ${data.length}`);
}
if (schema.pattern && !schema.pattern.test(data)) {
errors.push('Value does not match required pattern');
}
}
// Number validations
if (schema.type === 'number' && typeof data === 'number') {
if (schema.min !== undefined && data < schema.min) {
errors.push(`Minimum value is ${schema.min}, got ${data}`);
}
if (schema.max !== undefined && data > schema.max) {
errors.push(`Maximum value is ${schema.max}, got ${data}`);
}
}
// Object validations
if (schema.type === 'object' && typeof data === 'object' && data !== null) {
if (schema.properties) {
for (const [key, propSchema] of Object.entries(schema.properties)) {
const propResult = this.validateInput(data[key], propSchema, {
...context,
field: key
});
errors.push(...propResult.errors.map(err => `${key}: ${err}`));
warnings.push(...propResult.warnings.map(warn => `${key}: ${warn}`));
}
}
}
// Security validations
if (typeof data === 'string') {
// Check for potential injection attacks
const suspiciousPatterns = [
/<script/i,
/javascript:/i,
/on\w+\s*=/i,
/\$\{.*\}/,
/\{\{.*\}\}/
];
for (const pattern of suspiciousPatterns) {
if (pattern.test(data)) {
warnings.push('Potentially unsafe content detected');
break;
}
}
}
} catch (error) {
errors.push(`Validation error: ${error.message}`);
}
const isValid = errors.length === 0;
const result = {
isValid,
errors,
warnings,
sanitizedData: this._sanitizeData(data, schema)
};
// Log validation failures
if (!isValid) {
this.logWarn('Input validation failed', {
...context,
component: 'error-handler',
operation: 'input-validation',
errors,
warnings
});
}
return result;
}
/**
* Log informational messages
* @param {string} message - Log message
* @param {Object} context - Log context
* @returns {Promise<void>}
*/
async logInfo(message, context = {}) {
await this._log(message, context, this.logLevels.INFO);
}
/**
* Log warning messages
* @param {string} message - Log message
* @param {Object} context - Log context
* @returns {Promise<void>}
*/
async logWarn(message, context = {}) {
await this._log(message, context, this.logLevels.WARN);
}
/**
* Log debug messages
* @param {string} message - Log message
* @param {Object} context - Log context
* @returns {Promise<void>}
*/
async logDebug(message, context = {}) {
await this._log(message, context, this.logLevels.DEBUG);
}
/**
* Get error statistics
* @returns {Object} Error statistics
*/
getErrorStatistics() {
const stats = {
totalErrors: 0,
errorsByComponent: {},
errorsByType: {},
circuitBreakers: {},
timestamp: new Date()
};
for (const [key, count] of this.errorCounts.entries()) {
const [component, errorType] = key.split(':');
stats.totalErrors += count;
if (!stats.errorsByComponent[component]) {
stats.errorsByComponent[component] = 0;
}
stats.errorsByComponent[component] += count;
if (!stats.errorsByType[errorType]) {
stats.errorsByType[errorType] = 0;
}
stats.errorsByType[errorType] += count;
}
for (const [component, breaker] of this.circuitBreakers.entries()) {
stats.circuitBreakers[component] = {
isOpen: breaker.isOpen,
errorCount: breaker.errorCount,
lastError: breaker.lastError,
openedAt: breaker.openedAt
};
}
return stats;
}
// Private methods
/**
* Generate unique error ID
* @private
* @returns {string} Error ID
*/
_generateErrorId() {
return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Sanitize context data for logging
* @private
* @param {Object} context - Context to sanitize
* @returns {Object} Sanitized context
*/
_sanitizeContext(context) {
const sanitized = { ...context };
// Remove or mask sensitive data
const sensitiveKeys = ['password', 'secret', 'token', 'key', 'auth'];
for (const key of Object.keys(sanitized)) {
if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
sanitized[key] = '[REDACTED]';
}
}
return sanitized;
}
/**
* Mask license key for logging
* @private
* @param {string} licenseKey - License key to mask
* @returns {string} Masked license key
*/
_maskLicenseKey(licenseKey) {
if (!licenseKey || typeof licenseKey !== 'string') {
return '[INVALID_LICENSE_KEY]';
}
return licenseKey.substring(0, 8) + '...';
}
/**
* Log error information to console
* @private
* @param {Object} errorInfo - Error information
*/
_logToConsole(errorInfo) {
const logMethod = errorInfo.severity === this.logLevels.ERROR ? console.error :
errorInfo.severity === this.logLevels.WARN ? console.warn :
console.log;
logMethod(`[${errorInfo.severity.toUpperCase()}] ${errorInfo.component}:${errorInfo.operation} - ${errorInfo.message}`);
if (errorInfo.severity === this.logLevels.ERROR && errorInfo.stack) {
console.error(errorInfo.stack);
}
}
/**
* Log error to database
* @private
* @param {Object} errorInfo - Error information
* @returns {Promise<void>}
*/
async _logToDatabase(errorInfo) {
if (!this.databaseManager || !this.databaseManager.isMonitoringConnected()) {
return;
}
try {
const db = this.databaseManager.getMonitoringDB();
const collection = db.collection('errorLogs');
await collection.insertOne({
...errorInfo,
createdAt: new Date()
});
} catch (error) {
// Don't throw database logging errors
console.error('Failed to log error to database:', error.message);
}
}
/**
* Track error frequency for circuit breaker
* @private
* @param {Object} errorInfo - Error information
*/
_trackErrorFrequency(errorInfo) {
const key = `${errorInfo.component}:${errorInfo.name}`;
const currentCount = this.errorCounts.get(key) || 0;
this.errorCounts.set(key, currentCount + 1);
}
/**
* Check and update circuit breaker status
* @private
* @param {Object} errorInfo - Error information
*/
_checkCircuitBreaker(errorInfo) {
const component = errorInfo.component;
const threshold = 10; // Open circuit after 10 errors
const timeWindow = 5 * 60 * 1000; // 5 minutes
if (!this.circuitBreakers.has(component)) {
this.circuitBreakers.set(component, {
errorCount: 0,
isOpen: false,
lastError: null,
openedAt: null
});
}
const breaker = this.circuitBreakers.get(component);
breaker.errorCount++;
breaker.lastError = new Date();
if (breaker.errorCount >= threshold && !breaker.isOpen) {
breaker.isOpen = true;
breaker.openedAt = new Date();
console.warn(`Circuit breaker opened for component: ${component}`);
}
// Auto-reset circuit breaker after time window
if (breaker.isOpen && breaker.openedAt &&
(Date.now() - breaker.openedAt.getTime()) > timeWindow) {
breaker.isOpen = false;
breaker.errorCount = 0;
breaker.openedAt = null;
console.info(`Circuit breaker reset for component: ${component}`);
}
}
/**
* Check if circuit breaker is open for component
* @private
* @param {string} component - Component name
* @returns {boolean} True if circuit breaker is open
*/
_isCircuitBreakerOpen(component) {
const breaker = this.circuitBreakers.get(component);
return breaker ? breaker.isOpen : false;
}
/**
* Determine if error should be retried
* @private
* @param {Error} error - Error to check
* @param {Object} context - Error context
* @returns {boolean} True if should retry
*/
_shouldRetry(error, context) {
// Don't retry if circuit breaker is open
if (this._isCircuitBreakerOpen(context.component)) {
return false;
}
// Don't retry validation errors
if (error.name === 'ValidationError' || error.message.includes('validation')) {
return false;
}
// Don't retry authentication errors
if (error.name === 'AuthenticationError' || error.message.includes('authentication')) {
return false;
}
// Retry network and timeout errors
const retryableErrors = [
'NetworkError', 'TimeoutError', 'ConnectionError',
'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED'
];
return retryableErrors.some(retryableError =>
error.name === retryableError ||
error.code === retryableError ||
error.message.includes(retryableError)
);
}
/**
* Calculate retry delay with exponential backoff
* @private
* @param {number} attemptNumber - Current attempt number
* @returns {number} Delay in milliseconds
*/
_calculateRetryDelay(attemptNumber) {
const baseDelay = this.baseRetryDelay;
const maxDelay = 30000; // 30 seconds max
const delay = baseDelay * Math.pow(2, attemptNumber - 1);
return Math.min(delay, maxDelay);
}
/**
* Determine license error type from error
* @private
* @param {Error} error - License error
* @returns {string} Error type
*/
_determineLicenseErrorType(error) {
const message = error.message.toLowerCase();
if (message.includes('not found')) return 'LICENSE_NOT_FOUND';
if (message.includes('expired')) return 'LICENSE_EXPIRED';
if (message.includes('inactive') || message.includes('deactivated')) return 'LICENSE_INACTIVE';
if (message.includes('distribution limit') || message.includes('limit exceeded')) return 'DISTRIBUTION_LIMIT_EXCEEDED';
if (message.includes('connection') || message.includes('network')) return 'CONNECTION_ERROR';
return 'VALIDATION_ERROR';
}
/**
* Sanitize data based on schema
* @private
* @param {*} data - Data to sanitize
* @param {Object} schema - Sanitization schema
* @returns {*} Sanitized data
*/
_sanitizeData(data, schema) {
if (typeof data === 'string') {
// Remove potentially dangerous characters
let sanitized = data.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
sanitized = sanitized.replace(/javascript:/gi, '');
sanitized = sanitized.replace(/on\w+\s*=/gi, '');
// Trim whitespace if specified
if (schema.trim !== false) {
sanitized = sanitized.trim();
}
return sanitized;
}
return data;
}
/**
* Generic logging method
* @private
* @param {string} message - Log message
* @param {Object} context - Log context
* @param {string} level - Log level
* @returns {Promise<void>}
*/
async _log(message, context, level) {
const logEntry = {
message,
level,
timestamp: new Date(),
context: this._sanitizeContext(context),
component: context.component || 'unknown'
};
// Log to console
const logMethod = level === this.logLevels.ERROR ? console.error :
level === this.logLevels.WARN ? console.warn :
console.log;
logMethod(`[${level.toUpperCase()}] ${logEntry.component} - ${message}`);
// Log to database if available
try {
if (this.databaseManager && this.databaseManager.isMonitoringConnected()) {
const db = this.databaseManager.getMonitoringDB();
const collection = db.collection('systemLogs');
await collection.insertOne(logEntry);
}
} catch (error) {
// Don't throw database logging errors
console.error('Failed to log to database:', error.message);
}
}
/**
* Sleep utility function
* @private
* @param {number} ms - Milliseconds to sleep
* @returns {Promise<void>}
*/
_sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Cleanup error handler resources
* @returns {Promise<void>}
*/
async cleanup() {
try {
this.errorCounts.clear();
this.circuitBreakers.clear();
if (this.databaseManager) {
await this.databaseManager.closeConnection();
}
} catch (error) {
console.error('Error during error handler cleanup:', error.message);
}
}
}
export default ErrorHandler;