- Created validateEnv utility for comprehensive environment validation - Validates required variables: JWT_SECRET, COUCHDB_URL, COUCHDB_DB_NAME - Validates optional variables with defaults: NODE_ENV, PORT, FRONTEND_URL - Enforces JWT_SECRET minimum length of 32 characters for security - Validates URL formats for COUCHDB_URL and FRONTEND_URL - Validates CouchDB database name format - Warns about missing optional services in production - Integrated validation into server startup - Server exits with clear error messages if configuration is invalid - Logs environment configuration on startup (masks sensitive values) - Updated test setup - Set proper 32+ character JWT_SECRET for tests - Added all required environment variables for validation Security Benefits: - Prevents server from starting with weak or missing credentials - Catches configuration errors early before database connections - Provides clear guidance on required variables - Protects against default/example credentials in production 🤖 Generated with AI Assistant Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
191 lines
6.0 KiB
JavaScript
191 lines
6.0 KiB
JavaScript
/**
|
|
* Environment Variable Validation Utility
|
|
*
|
|
* Validates required environment variables on application startup.
|
|
* Provides clear error messages if configuration is missing or invalid.
|
|
*/
|
|
|
|
const logger = require('./logger');
|
|
|
|
/**
|
|
* Validates that required environment variables are set
|
|
* @param {Object} config - Configuration object with variable definitions
|
|
* @returns {Object} Validated environment variables
|
|
* @throws {Error} If required variables are missing or invalid
|
|
*/
|
|
function validateEnv(config = {}) {
|
|
const errors = [];
|
|
const warnings = [];
|
|
const validated = {};
|
|
|
|
// Define required environment variables with validation rules
|
|
const requiredVars = {
|
|
JWT_SECRET: {
|
|
required: true,
|
|
minLength: 32,
|
|
description: 'JWT secret for token signing',
|
|
validate: (value) => {
|
|
if (value === 'change-this-jwt-secret-key') {
|
|
return 'JWT_SECRET is using the default example value. Please set a secure random string.';
|
|
}
|
|
if (value.length < 32) {
|
|
return `JWT_SECRET must be at least 32 characters (currently ${value.length})`;
|
|
}
|
|
return null;
|
|
}
|
|
},
|
|
COUCHDB_URL: {
|
|
required: true,
|
|
description: 'CouchDB connection URL',
|
|
validate: (value) => {
|
|
if (!value.startsWith('http://') && !value.startsWith('https://')) {
|
|
return 'COUCHDB_URL must start with http:// or https://';
|
|
}
|
|
return null;
|
|
}
|
|
},
|
|
COUCHDB_DB_NAME: {
|
|
required: true,
|
|
description: 'CouchDB database name',
|
|
validate: (value) => {
|
|
if (!/^[a-z][a-z0-9_$()+/-]*$/.test(value)) {
|
|
return 'COUCHDB_DB_NAME must start with a lowercase letter and contain only lowercase letters, digits, and _, $, (, ), +, -, /';
|
|
}
|
|
return null;
|
|
}
|
|
},
|
|
NODE_ENV: {
|
|
required: false,
|
|
default: 'development',
|
|
description: 'Node environment',
|
|
validate: (value) => {
|
|
const valid = ['development', 'production', 'test'];
|
|
if (!valid.includes(value)) {
|
|
return `NODE_ENV must be one of: ${valid.join(', ')}`;
|
|
}
|
|
return null;
|
|
}
|
|
},
|
|
PORT: {
|
|
required: false,
|
|
default: '5000',
|
|
description: 'Server port',
|
|
validate: (value) => {
|
|
const port = parseInt(value, 10);
|
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
return 'PORT must be a valid port number (1-65535)';
|
|
}
|
|
return null;
|
|
}
|
|
},
|
|
FRONTEND_URL: {
|
|
required: false,
|
|
default: 'http://localhost:3000',
|
|
description: 'Frontend application URL for CORS',
|
|
validate: (value) => {
|
|
if (!value.startsWith('http://') && !value.startsWith('https://')) {
|
|
return 'FRONTEND_URL must start with http:// or https://';
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Optional environment variables (warn if missing in production)
|
|
const optionalVars = {
|
|
COUCHDB_USER: 'CouchDB admin username',
|
|
COUCHDB_PASSWORD: 'CouchDB admin password',
|
|
CLOUDINARY_CLOUD_NAME: 'Cloudinary cloud name for image uploads',
|
|
CLOUDINARY_API_KEY: 'Cloudinary API key',
|
|
CLOUDINARY_API_SECRET: 'Cloudinary API secret',
|
|
STRIPE_SECRET_KEY: 'Stripe secret key for payments',
|
|
STRIPE_PUBLISHABLE_KEY: 'Stripe publishable key',
|
|
OPENAI_API_KEY: 'OpenAI API key for AI features'
|
|
};
|
|
|
|
// Validate required variables
|
|
for (const [varName, rules] of Object.entries(requiredVars)) {
|
|
const value = process.env[varName];
|
|
|
|
if (!value || value.trim() === '') {
|
|
if (rules.required) {
|
|
errors.push(`${varName} is required but not set. ${rules.description}`);
|
|
} else if (rules.default) {
|
|
validated[varName] = rules.default;
|
|
logger.debug(`Using default value for ${varName}: ${rules.default}`);
|
|
}
|
|
} else {
|
|
// Run custom validation if provided
|
|
if (rules.validate) {
|
|
const validationError = rules.validate(value);
|
|
if (validationError) {
|
|
errors.push(`${varName}: ${validationError}`);
|
|
}
|
|
}
|
|
validated[varName] = value;
|
|
}
|
|
}
|
|
|
|
// Check optional variables in production
|
|
if (process.env.NODE_ENV === 'production') {
|
|
for (const [varName, description] of Object.entries(optionalVars)) {
|
|
const value = process.env[varName];
|
|
if (!value || value.trim() === '') {
|
|
warnings.push(`${varName} is not set. ${description} may not work.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Log warnings
|
|
if (warnings.length > 0) {
|
|
logger.warn('Environment configuration warnings:');
|
|
warnings.forEach(warning => logger.warn(` - ${warning}`));
|
|
}
|
|
|
|
// Throw error if required variables are missing
|
|
if (errors.length > 0) {
|
|
const errorMessage = [
|
|
'Environment validation failed. Missing or invalid required variables:',
|
|
...errors.map(err => ` - ${err}`),
|
|
'',
|
|
'Please check your .env file or environment configuration.',
|
|
'See .env.example for required variables.'
|
|
].join('\n');
|
|
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
return validated;
|
|
}
|
|
|
|
/**
|
|
* Logs current environment configuration (masks sensitive values)
|
|
*/
|
|
function logEnvConfig() {
|
|
const sensitiveKeys = ['JWT_SECRET', 'COUCHDB_PASSWORD', 'COUCHDB_SECRET',
|
|
'CLOUDINARY_API_SECRET', 'STRIPE_SECRET_KEY', 'OPENAI_API_KEY'];
|
|
|
|
const config = {
|
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
PORT: process.env.PORT || '5000',
|
|
COUCHDB_URL: process.env.COUCHDB_URL || 'not set',
|
|
COUCHDB_DB_NAME: process.env.COUCHDB_DB_NAME || 'not set',
|
|
FRONTEND_URL: process.env.FRONTEND_URL || 'http://localhost:3000',
|
|
COUCHDB_USER: process.env.COUCHDB_USER ? '***' : 'not set',
|
|
JWT_SECRET: process.env.JWT_SECRET ? '***' : 'not set',
|
|
CLOUDINARY_CONFIGURED: process.env.CLOUDINARY_CLOUD_NAME ? 'yes' : 'no',
|
|
STRIPE_CONFIGURED: process.env.STRIPE_SECRET_KEY ? 'yes' : 'no',
|
|
OPENAI_CONFIGURED: process.env.OPENAI_API_KEY ? 'yes' : 'no'
|
|
};
|
|
|
|
logger.info('Environment Configuration:');
|
|
Object.entries(config).forEach(([key, value]) => {
|
|
logger.info(` ${key}: ${value}`);
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
validateEnv,
|
|
logEnvConfig
|
|
};
|