feat: add environment variable validation on startup
- 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>
This commit is contained in:
@@ -4,10 +4,12 @@ const couchdbService = require('../services/couchdbService');
|
||||
// Make mock available for tests to reference
|
||||
global.mockCouchdbService = couchdbService;
|
||||
|
||||
// Set test environment variables
|
||||
process.env.JWT_SECRET = 'test-jwt-secret';
|
||||
// Set test environment variables (must be at least 32 chars for validation)
|
||||
process.env.JWT_SECRET = 'test-jwt-secret-for-testing-purposes-that-is-long-enough';
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.COUCHDB_URL = 'http://localhost:5984';
|
||||
process.env.COUCHDB_DB_NAME = 'adopt-a-street-test';
|
||||
process.env.COUCHDB_URL = 'http://localhost:5984';
|
||||
process.env.COUCHDB_DB_NAME = 'test-adopt-a-street';
|
||||
|
||||
// Suppress console logs during tests unless there's an error
|
||||
|
||||
@@ -12,6 +12,17 @@ const { errorHandler } = require("./middleware/errorHandler");
|
||||
const socketAuth = require("./middleware/socketAuth");
|
||||
const requestLogger = require("./middleware/requestLogger");
|
||||
const logger = require("./utils/logger");
|
||||
const { validateEnv, logEnvConfig } = require("./utils/validateEnv");
|
||||
|
||||
// Validate environment variables before starting server
|
||||
try {
|
||||
validateEnv();
|
||||
logEnvConfig();
|
||||
} catch (error) {
|
||||
logger.error('Environment validation failed:');
|
||||
logger.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
||||
190
backend/utils/validateEnv.js
Normal file
190
backend/utils/validateEnv.js
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
Reference in New Issue
Block a user