/** * Environment Variable Validation Utility * * Validates required environment variables on application startup. * Provides clear error messages if configuration is missing or invalid. */ const logger = require('./logger'); /** * Validates that required environment variables are set * @param {Object} config - Configuration object with variable definitions * @returns {Object} Validated environment variables * @throws {Error} If required variables are missing or invalid */ function validateEnv(config = {}) { const errors = []; const warnings = []; const validated = {}; // Define required environment variables with validation rules const requiredVars = { JWT_SECRET: { required: true, minLength: 32, description: 'JWT secret for token signing', validate: (value) => { if (value === 'change-this-jwt-secret-key') { return 'JWT_SECRET is using the default example value. Please set a secure random string.'; } if (value.length < 32) { return `JWT_SECRET must be at least 32 characters (currently ${value.length})`; } return null; } }, COUCHDB_URL: { required: true, description: 'CouchDB connection URL', validate: (value) => { if (!value.startsWith('http://') && !value.startsWith('https://')) { return 'COUCHDB_URL must start with http:// or https://'; } return null; } }, COUCHDB_DB_NAME: { required: true, description: 'CouchDB database name', validate: (value) => { if (!/^[a-z][a-z0-9_$()+/-]*$/.test(value)) { return 'COUCHDB_DB_NAME must start with a lowercase letter and contain only lowercase letters, digits, and _, $, (, ), +, -, /'; } return null; } }, NODE_ENV: { required: false, default: 'development', description: 'Node environment', validate: (value) => { const valid = ['development', 'production', 'test']; if (!valid.includes(value)) { return `NODE_ENV must be one of: ${valid.join(', ')}`; } return null; } }, PORT: { required: false, default: '5000', description: 'Server port', validate: (value) => { const port = parseInt(value, 10); if (isNaN(port) || port < 1 || port > 65535) { return 'PORT must be a valid port number (1-65535)'; } return null; } }, FRONTEND_URL: { required: false, default: 'http://localhost:3000', description: 'Frontend application URL for CORS', validate: (value) => { if (!value.startsWith('http://') && !value.startsWith('https://')) { return 'FRONTEND_URL must start with http:// or https://'; } return null; } } }; // Optional environment variables (warn if missing in production) const optionalVars = { COUCHDB_USER: 'CouchDB admin username', COUCHDB_PASSWORD: 'CouchDB admin password', CLOUDINARY_CLOUD_NAME: 'Cloudinary cloud name for image uploads', CLOUDINARY_API_KEY: 'Cloudinary API key', CLOUDINARY_API_SECRET: 'Cloudinary API secret', STRIPE_SECRET_KEY: 'Stripe secret key for payments', STRIPE_PUBLISHABLE_KEY: 'Stripe publishable key', OPENAI_API_KEY: 'OpenAI API key for AI features' }; // Validate required variables for (const [varName, rules] of Object.entries(requiredVars)) { const value = process.env[varName]; if (!value || value.trim() === '') { if (rules.required) { errors.push(`${varName} is required but not set. ${rules.description}`); } else if (rules.default) { validated[varName] = rules.default; logger.debug(`Using default value for ${varName}: ${rules.default}`); } } else { // Run custom validation if provided if (rules.validate) { const validationError = rules.validate(value); if (validationError) { errors.push(`${varName}: ${validationError}`); } } validated[varName] = value; } } // Check optional variables in production if (process.env.NODE_ENV === 'production') { for (const [varName, description] of Object.entries(optionalVars)) { const value = process.env[varName]; if (!value || value.trim() === '') { warnings.push(`${varName} is not set. ${description} may not work.`); } } } // Log warnings if (warnings.length > 0) { logger.warn('Environment configuration warnings:'); warnings.forEach(warning => logger.warn(` - ${warning}`)); } // Throw error if required variables are missing if (errors.length > 0) { const errorMessage = [ 'Environment validation failed. Missing or invalid required variables:', ...errors.map(err => ` - ${err}`), '', 'Please check your .env file or environment configuration.', 'See .env.example for required variables.' ].join('\n'); throw new Error(errorMessage); } return validated; } /** * Logs current environment configuration (masks sensitive values) */ function logEnvConfig() { const sensitiveKeys = ['JWT_SECRET', 'COUCHDB_PASSWORD', 'COUCHDB_SECRET', 'CLOUDINARY_API_SECRET', 'STRIPE_SECRET_KEY', 'OPENAI_API_KEY']; const config = { NODE_ENV: process.env.NODE_ENV || 'development', PORT: process.env.PORT || '5000', COUCHDB_URL: process.env.COUCHDB_URL || 'not set', COUCHDB_DB_NAME: process.env.COUCHDB_DB_NAME || 'not set', FRONTEND_URL: process.env.FRONTEND_URL || 'http://localhost:3000', COUCHDB_USER: process.env.COUCHDB_USER ? '***' : 'not set', JWT_SECRET: process.env.JWT_SECRET ? '***' : 'not set', CLOUDINARY_CONFIGURED: process.env.CLOUDINARY_CLOUD_NAME ? 'yes' : 'no', STRIPE_CONFIGURED: process.env.STRIPE_SECRET_KEY ? 'yes' : 'no', OPENAI_CONFIGURED: process.env.OPENAI_API_KEY ? 'yes' : 'no' }; logger.info('Environment Configuration:'); Object.entries(config).forEach(([key, value]) => { logger.info(` ${key}: ${value}`); }); } module.exports = { validateEnv, logEnvConfig };