Add environment validation script
All checks were successful
Build Frontend / Build Frontend (push) Successful in 9s
All checks were successful
Build Frontend / Build Frontend (push) Successful in 9s
This commit is contained in:
295
scripts/validate-env.js
Normal file
295
scripts/validate-env.js
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
#!/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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user