All checks were successful
Build Frontend / Build Frontend (push) Successful in 23s
336 lines
9.6 KiB
JavaScript
336 lines
9.6 KiB
JavaScript
#!/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);
|
||
}
|