Files
TurboTrades/scripts/validate-env.js
iDefineHD 6c489092ed
All checks were successful
Build Frontend / Build Frontend (push) Successful in 9s
Add environment validation script
2026-01-11 01:49:46 +00:00

296 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 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)');
}
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);
}