const dotenv = require('dotenv'); const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); /** * This class is responsible for loading the environment variables * * Inspired by: https://thekenyandev.com/blog/environment-variables-strategy-for-node/ */ class Env { constructor() { this.envMap = { default: '.env', development: '.env.development', test: '.env.test', production: '.env.production', }; this.init(); this.isProduction = process.env.NODE_ENV === 'production'; this.domains = { client: process.env.DOMAIN_CLIENT, server: process.env.DOMAIN_SERVER, }; } /** * Initialize the environment variables */ init() { let hasDefault = false; // Load the default env file if it exists if (fs.existsSync(this.envMap.default)) { hasDefault = true; dotenv.config({ path: this.resolve(this.envMap.default), }); } else { console.warn('The default .env file was not found'); } const environment = this.currentEnvironment(); // Load the environment specific env file const envFile = this.envMap[environment]; // check if the file exists if (fs.existsSync(envFile)) { dotenv.config({ path: this.resolve(envFile), }); } else if (!hasDefault) { console.warn('No env files found, have you completed the install process?'); } } /** * Validate Config */ validate() { const requiredKeys = [ 'NODE_ENV', 'JWT_SECRET', 'DOMAIN_CLIENT', 'DOMAIN_SERVER', 'CREDS_KEY', 'CREDS_IV', ]; const missingKeys = requiredKeys .map((key) => { const variable = process.env[key]; if (variable === undefined || variable === null) { return key; } }) .filter((value) => value !== undefined); // Throw an error if any required keys are missing if (missingKeys.length) { const message = ` The following required env variables are missing: ${missingKeys.toString()}. Please add them to your env file or run 'npm run install' `; throw new Error(message); } // Check JWT secret for default if (process.env.JWT_SECRET === 'secret') { console.warn('Warning: JWT_SECRET is set to default value'); } } /** * Resolve the location of the env file * * @param {String} envFile * @returns */ resolve(envFile) { return path.resolve(process.cwd(), envFile); } /** * Add secure keys to the env * * @param {String} filePath The path of the .env you are updating * @param {String} key The env you are adding * @param {Number} length The length of the secure key */ addSecureEnvVar(filePath, key, length) { const env = {}; env[key] = this.generateSecureRandomString(length); this.writeEnvFile(filePath, env); } /** * Write the change to the env file */ writeEnvFile(filePath, env) { const content = fs.readFileSync(filePath, 'utf-8'); const lines = content.split('\n'); const updatedLines = lines .map((line) => { if (line.trim().startsWith('#')) { // Allow comment removal if (env[line] === 'remove') { return null; // Mark the line for removal } // Preserve comments return line; } const [key, value] = line.split('='); if (key && value && Object.prototype.hasOwnProperty.call(env, key.trim())) { if (env[key.trim()] === 'remove') { return null; // Mark the line for removal } return `${key.trim()}=${env[key.trim()]}`; } return line; }) .filter((line) => line !== null); // Remove lines marked for removal // Add any new environment variables that are not in the file yet Object.entries(env).forEach(([key, value]) => { if (value !== 'remove' && !updatedLines.some((line) => line.startsWith(`${key}=`))) { updatedLines.push(`${key}=${value}`); } }); // Loop through updatedLines and wrap values with spaces in double quotes const fixedLines = updatedLines.map((line) => { // lets only split the first = sign const [key, value] = line.split(/=(.+)/); if (typeof value === 'undefined' || line.trim().startsWith('#')) { return line; } // Skip lines with quotes and numbers already // Todo: this could be one regex const wrappedValue = value.includes(' ') && !value.includes('"') && !value.includes('\'') && !/\d/.test(value) ? `"${value}"` : value; return `${key}=${wrappedValue}`; }); const updatedContent = fixedLines.join('\n'); fs.writeFileSync(filePath, updatedContent); } /** * Generate Secure Random Strings * * @param {Number} length The length of the random string * @returns */ generateSecureRandomString(length = 32) { return crypto.randomBytes(length).toString('hex'); } /** * Get all the environment variables */ all() { return process.env; } /** * Get an environment variable * * @param {String} variable * @returns */ get(variable) { return process.env[variable]; } /** * Get the current environment name * * @returns {String} */ currentEnvironment() { return this.get('NODE_ENV'); } /** * Are we running in development? * * @returns {Boolean} */ isDevelopment() { return this.currentEnvironment() === 'development'; } /** * Are we running tests? * * @returns {Boolean} */ isTest() { return this.currentEnvironment() === 'test'; } /** * Are we running in production? * * @returns {Boolean} */ isProduction() { return this.currentEnvironment() === 'production'; } /** * Are we running in CI? * * @returns {Boolean} */ isCI() { return this.currentEnvironment() === 'ci'; } } const env = new Env(); module.exports = env;