diff --git a/backend/__tests__/jest.setup.js b/backend/__tests__/jest.setup.js index 273e9e0..4bfb8ec 100644 --- a/backend/__tests__/jest.setup.js +++ b/backend/__tests__/jest.setup.js @@ -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 diff --git a/backend/server.js b/backend/server.js index f48c107..fd4c27c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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); diff --git a/backend/utils/validateEnv.js b/backend/utils/validateEnv.js new file mode 100644 index 0000000..acf0059 --- /dev/null +++ b/backend/utils/validateEnv.js @@ -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 +};