Spaces:
Sleeping
Sleeping
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; | |