/** * Environment Variable Validation * * Validates required environment variables at startup and provides * helpful error messages for production deployments. */ /** * Required environment variables for production */ const REQUIRED_ENV_VARS = { // Deployment Configuration DEPLOYMENT_TARGET: { required: true, description: 'Deployment environment (dev, docker-local, production)', validate: val => ['dev', 'docker-local', 'production'].includes(val) }, // Server Configuration NODE_ENV: { required: true, description: 'Environment mode (development, production)', validate: val => ['development', 'production', 'test'].includes(val) }, PORT: { required: true, description: 'Server port number', validate: val => !isNaN(parseInt(val)) && parseInt(val) > 0 && parseInt(val) < 65536 }, // Frontend URL for CORS FRONTEND_URL: { required: true, description: 'Frontend URL for CORS', validate: (val, env) => { if (!val) return false; // Validate that FRONTEND_URL matches DEPLOYMENT_TARGET (if set) const target = env?.DEPLOYMENT_TARGET; if (!target) return true; // Skip validation if target not set yet if (target === 'dev' && !val.includes('localhost:5173')) { console.error('⚠️ FRONTEND_URL should be http://localhost:5173 for dev target'); return false; } if (target === 'docker-local' && !val.includes('localhost:8099')) { console.error('⚠️ FRONTEND_URL should be http://localhost:8099 for docker-local target'); return false; } if (target === 'production' && !val.includes('app.pokedex.online')) { console.error('⚠️ FRONTEND_URL should be https://app.pokedex.online for production target'); return false; } return true; } }, // Optional but recommended for production SESSION_SECRET: { required: false, description: 'Secret key for session encryption', warn: val => !val || val.length < 32 ? 'SESSION_SECRET should be at least 32 characters for security' : null }, // Challonge OAuth (optional) CHALLONGE_CLIENT_ID: { required: false, description: 'Challonge OAuth client ID' }, CHALLONGE_CLIENT_SECRET: { required: false, description: 'Challonge OAuth client secret' }, CHALLONGE_REDIRECT_URI: { required: false, description: 'OAuth redirect URI' } }; /** * Validate environment variables * @returns {Object} Validation result with errors and warnings */ export function validateEnvironment() { const errors = []; const warnings = []; const missing = []; // Check required variables for (const [key, config] of Object.entries(REQUIRED_ENV_VARS)) { const value = process.env[key]; // Check if required variable is missing if (config.required && !value) { errors.push( `Missing required environment variable: ${key} - ${config.description}` ); missing.push(key); continue; } // Validate value if present if (value && config.validate && !config.validate(value)) { errors.push( `Invalid value for ${key}: "${value}" - ${config.description}` ); } // Check for warnings if (config.warn) { const warning = config.warn(value, process.env); if (warning) { warnings.push(`${key}: ${warning}`); } } } return { valid: errors.length === 0, errors, warnings, missing }; } /** * Validate environment and exit if critical errors found * @param {boolean} exitOnError - Whether to exit process on validation errors (default: true) */ export function validateOrExit(exitOnError = true) { const result = validateEnvironment(); // Print validation results console.log('\n🔍 Environment Validation:'); console.log(` DEPLOYMENT_TARGET: ${process.env.DEPLOYMENT_TARGET || 'not set'}`); console.log(` NODE_ENV: ${process.env.NODE_ENV || 'not set'}`); console.log(` PORT: ${process.env.PORT || 'not set'}`); console.log(` FRONTEND_URL: ${process.env.FRONTEND_URL || 'not set'}`); // Show errors if (result.errors.length > 0) { console.error('\n❌ Environment Validation Errors:'); result.errors.forEach(error => console.error(` - ${error}`)); if (result.missing.length > 0) { console.error('\n💡 Tip: Create a .env file with these variables:'); result.missing.forEach(key => { console.error(` ${key}=your_value_here`); }); console.error('\n See .env.example for reference'); } if (exitOnError) { console.error('\n❌ Server cannot start due to environment errors\n'); process.exit(1); } } else { console.log(' ✅ All required variables present'); } // Show warnings if (result.warnings.length > 0) { console.warn('\n⚠️ Environment Warnings:'); result.warnings.forEach(warning => console.warn(` - ${warning}`)); } console.log(''); return result; } /** * Get configuration object with validated environment variables * @returns {Object} Configuration object */ export function getConfig() { const deploymentTarget = process.env.DEPLOYMENT_TARGET || 'dev'; const frontendUrl = process.env.FRONTEND_URL; return { deploymentTarget, nodeEnv: process.env.NODE_ENV || 'development', port: parseInt(process.env.PORT || '3001'), isProduction: process.env.NODE_ENV === 'production', isDevelopment: process.env.NODE_ENV === 'development', // Challonge OAuth challonge: { clientId: process.env.CHALLONGE_CLIENT_ID, clientSecret: process.env.CHALLONGE_CLIENT_SECRET, redirectUri: process.env.CHALLONGE_REDIRECT_URI, configured: !!( process.env.CHALLONGE_CLIENT_ID && process.env.CHALLONGE_CLIENT_SECRET ) }, // CORS - Single origin based on deployment target cors: { origin: frontendUrl, credentials: true }, // Security session: { secret: process.env.SESSION_SECRET || 'dev-secret-change-in-production' }, // Discord User Permissions discord: { adminUsers: process.env.DISCORD_ADMIN_USERS ? process.env.DISCORD_ADMIN_USERS.split(',').map(u => u.trim().toLowerCase() ) : [] } }; } // Run validation when executed directly if (import.meta.url === `file://${process.argv[1]}`) { validateOrExit(); }