#!/usr/bin/env node /** * Environment Variable Validation Script * Checks your .env file for production deployment without exposing secrets */ import dotenv from "dotenv"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import { existsSync } from "fs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Colors for terminal output const colors = { reset: "\x1b[0m", red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m", magenta: "\x1b[35m", cyan: "\x1b[36m", }; const log = { error: (msg) => console.log(`${colors.red}❌ ${msg}${colors.reset}`), success: (msg) => console.log(`${colors.green}✅ ${msg}${colors.reset}`), warning: (msg) => console.log(`${colors.yellow}⚠️ ${msg}${colors.reset}`), info: (msg) => console.log(`${colors.blue}ℹ️ ${msg}${colors.reset}`), header: (msg) => console.log(`\n${colors.cyan}${"=".repeat(60)}${colors.reset}`), title: (msg) => console.log(`${colors.magenta}${msg}${colors.reset}`), }; // Load environment variables const envPath = join(__dirname, "..", ".env"); const envLocalPath = join(__dirname, "..", ".env.local"); console.clear(); log.header(); log.title("🔍 Environment Configuration Validator"); log.header(); // Check if .env files exist console.log("\n📁 Checking .env files...\n"); if (!existsSync(envPath)) { log.error(".env file not found!"); process.exit(1); } log.success(".env file exists"); if (existsSync(envLocalPath)) { log.info(".env.local file exists (will override .env values)"); dotenv.config({ path: envLocalPath }); } dotenv.config({ path: envPath }); // Validation rules const issues = []; const warnings = []; let passedChecks = 0; const totalChecks = 15; console.log("\n🔍 Validating configuration...\n"); // Helper functions const checkRequired = (key, name) => { if (!process.env[key]) { issues.push(`Missing required variable: ${key} (${name})`); return false; } passedChecks++; return true; }; const checkUrl = (key, expectedProtocol, expectedDomain) => { const value = process.env[key]; if (!value) { issues.push(`Missing ${key}`); return false; } if (value.includes("localhost") || value.includes("127.0.0.1")) { issues.push( `${key} contains localhost/127.0.0.1 - should be production domain` ); log.error(`${key}: Uses localhost (should be ${expectedDomain})`); return false; } if (!value.startsWith(expectedProtocol)) { warnings.push(`${key} should start with ${expectedProtocol}`); log.warning(`${key}: Should start with ${expectedProtocol}`); } if (expectedDomain && !value.includes(expectedDomain)) { issues.push(`${key} should contain ${expectedDomain}`); log.error(`${key}: Should contain ${expectedDomain}`); return false; } log.success(`${key}: Correctly configured`); passedChecks++; return true; }; const checkValue = (key, expectedValue, description) => { const value = process.env[key]; if (!value) { warnings.push(`${key} not set (${description})`); return false; } if (value !== expectedValue) { issues.push(`${key} should be "${expectedValue}" but is "${value}"`); log.error(`${key}: Should be "${expectedValue}"`); return false; } log.success(`${key}: Correctly set to "${expectedValue}"`); passedChecks++; return true; }; const checkBoolean = (key, expectedValue) => { const value = process.env[key]; if (!value) { warnings.push(`${key} not set`); return false; } if (value !== expectedValue) { issues.push(`${key} should be "${expectedValue}" but is "${value}"`); log.error(`${key}: Should be "${expectedValue}" for production`); return false; } log.success(`${key}: Correctly set to "${expectedValue}"`); passedChecks++; return true; }; const maskSecret = (value) => { if (!value || value.length < 8) return "***"; return value.substring(0, 4) + "..." + value.substring(value.length - 4); }; // Run validations console.log("🌐 Network Configuration:"); checkUrl("STEAM_REALM", "https://", "api.turbotrades.dev"); checkUrl("STEAM_RETURN_URL", "https://", "api.turbotrades.dev"); checkUrl("CORS_ORIGIN", "https://", "turbotrades.dev"); // Check WebSocket URL (optional but recommended) if (process.env.WS_URL) { checkUrl("WS_URL", "wss://", "api.turbotrades.dev"); } else { log.info("WS_URL: Not set (will use default)"); } // Check CORS_ORIGIN is NOT the API domain if ( process.env.CORS_ORIGIN && process.env.CORS_ORIGIN.includes("api.turbotrades.dev") ) { issues.push( "CORS_ORIGIN should be the FRONTEND domain (turbotrades.dev), not the API domain" ); log.error("CORS_ORIGIN: Should be https://turbotrades.dev (frontend domain)"); log.info(" CORS_ORIGIN = where requests COME FROM (frontend)"); log.info(" STEAM_REALM = where requests GO TO (backend)"); } else if ( process.env.CORS_ORIGIN && !process.env.CORS_ORIGIN.includes("localhost") ) { passedChecks++; } console.log("\n🔒 Security Configuration:"); checkBoolean("COOKIE_SECURE", "true"); checkValue("COOKIE_SAME_SITE", "none", "for cross-domain cookies"); checkValue("NODE_ENV", "production", "production environment"); // Check cookie domain if (process.env.COOKIE_DOMAIN) { if (process.env.COOKIE_DOMAIN.includes("localhost")) { issues.push( "COOKIE_DOMAIN contains localhost - should be .turbotrades.dev" ); log.error("COOKIE_DOMAIN: Contains localhost"); } else if ( process.env.COOKIE_DOMAIN === ".turbotrades.dev" || process.env.COOKIE_DOMAIN === "turbotrades.dev" ) { log.success("COOKIE_DOMAIN: Correctly set"); passedChecks++; } else { warnings.push("COOKIE_DOMAIN might be incorrect"); log.warning( `COOKIE_DOMAIN: Set to "${process.env.COOKIE_DOMAIN}" (should be .turbotrades.dev)` ); } } console.log("\n🔑 Required Secrets:"); // Check Steam API Key if (checkRequired("STEAM_API_KEY", "Steam API Key")) { const key = process.env.STEAM_API_KEY; if (key.length === 32 && /^[A-F0-9]{32}$/i.test(key)) { log.success(`STEAM_API_KEY: Valid format (${maskSecret(key)})`); } else { warnings.push( "STEAM_API_KEY format looks unusual (should be 32 hex characters)" ); log.warning(`STEAM_API_KEY: Unusual format (${maskSecret(key)})`); } } // Check MongoDB URI if (checkRequired("MONGODB_URI", "MongoDB connection string")) { const uri = process.env.MONGODB_URI; if (uri.includes("localhost") || uri.includes("127.0.0.1")) { issues.push( "MONGODB_URI contains localhost - should be production database" ); log.error("MONGODB_URI: Uses localhost (should be production database)"); } else { log.success("MONGODB_URI: Points to remote database"); } } // Check JWT secrets console.log("\n🎫 JWT Configuration:"); ["JWT_ACCESS_SECRET", "JWT_REFRESH_SECRET", "SESSION_SECRET"].forEach((key) => { if (process.env[key]) { const value = process.env[key]; if ( value.includes("your-") || value.includes("change-this") || value.length < 16 ) { warnings.push( `${key} appears to be a default/weak value - generate a secure one!` ); log.warning(`${key}: Using default/weak value (${maskSecret(value)})`); } else { log.success(`${key}: Set (${maskSecret(value)})`); passedChecks++; } } else { warnings.push(`${key} not set - will use default`); log.warning(`${key}: Not set (will use default)`); } }); // Summary log.header(); console.log("\n📊 Validation Summary:\n"); const score = Math.round((passedChecks / totalChecks) * 100); if (issues.length === 0 && warnings.length === 0) { log.success(`Perfect! All checks passed (${passedChecks}/${totalChecks})`); log.success("Your .env file is correctly configured for production! 🎉"); process.exit(0); } console.log(`Checks passed: ${passedChecks}/${totalChecks} (${score}%)\n`); if (issues.length > 0) { log.error(`Found ${issues.length} critical issue(s):\n`); issues.forEach((issue, i) => { console.log(` ${i + 1}. ${issue}`); }); console.log(); } if (warnings.length > 0) { log.warning(`Found ${warnings.length} warning(s):\n`); warnings.forEach((warning, i) => { console.log(` ${i + 1}. ${warning}`); }); console.log(); } // Recommendations if (issues.length > 0) { log.header(); log.title("🔧 Recommended Fixes:"); log.header(); console.log("\n1. Edit your .env file:"); console.log(" nano .env # or vim .env\n"); console.log("2. Update the following values:\n"); if (issues.some((i) => i.includes("CORS_ORIGIN"))) { console.log(" CORS_ORIGIN=https://turbotrades.dev"); } if (issues.some((i) => i.includes("STEAM_REALM"))) { console.log(" STEAM_REALM=https://api.turbotrades.dev"); } if (issues.some((i) => i.includes("STEAM_RETURN_URL"))) { console.log( " STEAM_RETURN_URL=https://api.turbotrades.dev/auth/steam/return" ); } if (issues.some((i) => i.includes("COOKIE_DOMAIN"))) { console.log(" COOKIE_DOMAIN=.turbotrades.dev"); } if (issues.some((i) => i.includes("COOKIE_SECURE"))) { console.log(" COOKIE_SECURE=true"); } if (issues.some((i) => i.includes("MONGODB_URI"))) { console.log(" MONGODB_URI=mongodb+srv://...(your production database)"); } if (issues.some((i) => i.includes("WS_URL"))) { console.log(" WS_URL=wss://api.turbotrades.dev/ws"); } console.log("\n3. Restart PM2:"); console.log(" pm2 restart turbotrades-backend --update-env\n"); log.header(); process.exit(1); } if (warnings.length > 0) { log.info("Configuration is functional but has some warnings."); log.info("Consider addressing the warnings above for better security."); process.exit(0); }