#!/usr/bin/env ts-node /** * Environment Variables Validation Script * * Validates that all required environment variables are set * before deployment to production. * * Usage: * ts-node scripts/validate-env.ts * or * npm run validate:env */ // Load environment variables from .env file const dotenv = require('dotenv'); const path = require('path'); const fs = require('fs'); // Try to load .env file const envPath = path.resolve(process.cwd(), '.env'); if (fs.existsSync(envPath)) { dotenv.config({ path: envPath }); console.log(`✅ Loaded environment variables from ${envPath}\n`); } else { console.warn(`⚠️ Warning: .env file not found at ${envPath}`); console.warn(' Trying to load from process.env only...\n'); // Still try to load from current directory dotenv.config(); } const requiredVars = [ // Core 'DATABASE_URL', 'NEXTAUTH_URL', 'NEXTAUTH_SECRET', // Keycloak 'KEYCLOAK_BASE_URL', 'KEYCLOAK_REALM', 'KEYCLOAK_CLIENT_ID', 'KEYCLOAK_CLIENT_SECRET', 'KEYCLOAK_ISSUER', 'NEXT_PUBLIC_KEYCLOAK_ISSUER', ]; const optionalButRecommended = [ // Redis 'REDIS_HOST', 'REDIS_PORT', 'REDIS_PASSWORD', // External Services 'N8N_API_KEY', 'N8N_WEBHOOK_URL', 'LEANTIME_API_URL', 'LEANTIME_TOKEN', 'ROCKET_CHAT_TOKEN', 'ROCKET_CHAT_USER_ID', ]; interface ValidationResult { valid: boolean; missing: string[]; warnings: string[]; errors: string[]; } function validateEnvironment(): ValidationResult { const result: ValidationResult = { valid: true, missing: [], warnings: [], errors: [], }; // Check required variables for (const varName of requiredVars) { const value = process.env[varName]; if (!value || value.trim() === '') { result.missing.push(varName); result.valid = false; } } // Check optional but recommended for (const varName of optionalButRecommended) { const value = process.env[varName]; if (!value || value.trim() === '') { result.warnings.push(varName); } } // Validate DATABASE_URL format const dbUrl = process.env.DATABASE_URL; if (dbUrl) { if (!dbUrl.startsWith('postgresql://') && !dbUrl.startsWith('postgres://')) { result.errors.push('DATABASE_URL must start with postgresql:// or postgres://'); result.valid = false; } // Check for connection pool parameters if (!dbUrl.includes('connection_limit')) { result.warnings.push( 'DATABASE_URL should include connection_limit parameter (e.g., ?connection_limit=10&pool_timeout=20)' ); } } // Validate NEXTAUTH_SECRET strength const nextAuthSecret = process.env.NEXTAUTH_SECRET; if (nextAuthSecret && nextAuthSecret.length < 32) { result.warnings.push('NEXTAUTH_SECRET should be at least 32 characters long'); } // Validate URLs const urlVars = ['NEXTAUTH_URL', 'KEYCLOAK_BASE_URL', 'NEXT_PUBLIC_KEYCLOAK_ISSUER']; for (const varName of urlVars) { const value = process.env[varName]; if (value) { try { new URL(value); } catch { result.errors.push(`${varName} is not a valid URL: ${value}`); result.valid = false; } } } return result; } function main() { console.log('🔍 Validating environment variables...\n'); const result = validateEnvironment(); if (result.missing.length > 0) { console.error('❌ Missing required environment variables:'); result.missing.forEach((varName) => { console.error(` - ${varName}`); }); console.error(''); } if (result.errors.length > 0) { console.error('❌ Validation errors:'); result.errors.forEach((error) => { console.error(` - ${error}`); }); console.error(''); } if (result.warnings.length > 0) { console.warn('⚠️ Warnings:'); result.warnings.forEach((warning) => { console.warn(` - ${warning}`); }); console.warn(''); } if (result.valid) { console.log('✅ All required environment variables are set!\n'); if (result.warnings.length > 0) { console.log('⚠️ Some optional variables are missing, but deployment can proceed.\n'); } process.exit(0); } else { console.error('❌ Environment validation failed!\n'); console.error('Please set all required variables before deploying.\n'); process.exit(1); } } if (require.main === module) { main(); } export { validateEnvironment };