Files
adopt-a-street/backend/utils/validateEnv.js
William Valentin 928d9a65fe 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>
2025-11-03 13:07:26 -08:00

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
};