From ea3413d8cba0e68172a6c757e4d354515c0f2709 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 23 Feb 2023 21:53:18 +0000 Subject: [PATCH] feat: validate environment variables at startup --- src/env.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 21 ++++++++---------- 2 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 src/env.js diff --git a/src/env.js b/src/env.js new file mode 100644 index 0000000..99824a5 --- /dev/null +++ b/src/env.js @@ -0,0 +1,61 @@ +/* eslint-disable no-console */ + +const dotenv = require('dotenv'); +const { colours } = require('leeks.js'); + +const providers = ['mysql', 'postgresql', 'sqlite']; + +// ideally the defaults would be set here too, but the pre-install script may run when `src/` is not available +const env = { + DB_CONNECTION_URL: v => + !!v || + (process.env.DB_PROVIDER === 'sqlite') || + new Error('must be set when "DB_PROVIDER" is not "sqlite"'), + DB_PROVIDER: v => + (!!v && providers.includes(v)) || + new Error(`must be one of: ${providers.map(v => `"${v}"`).join(', ')}`), + DISCORD_SECRET: v => + !!v || + new Error('is required'), + DISCORD_TOKEN: v => + !!v || + new Error('is required'), + ENCRYPTION_KEY: v => + (!!v && v.length >= 48) || + new Error('is required and must be at least 48 characters long; run "npm run keygen" to generate a key'), + HTTP_EXTERNAL: v => + (!!v && v.startsWith('http') && !v.endsWith('/')) || + new Error('must be a valid URL without a trailing slash'), + HTTP_HOST: v => + (!!v && !v.startsWith('http')) || + new Error('is required and must be an address, not a URL'), + HTTP_PORT: v => + !!v || + new Error('is required'), + HTTP_TRUST_PROXY: () => true, // optional + OVERRIDE_ARCHIVE: () => true, // optional + PUBLIC_BOT: () => true, // optional + SETTINGS_HOST: v => + (!!v && !v.startsWith('http')) || + new Error('is required and must be an address, not a URL'), + SETTINGS_PORT: v => + !!v || + new Error('is required'), + SUPER: () => true, // optional +}; + +const load = options => { + dotenv.config(options); + Object.entries(env).forEach(([name, validate]) => { + const result = validate(process.env[name]); // `true` for pass, or `Error` for fail + if (result instanceof Error) { + console.log('\x07' + colours.redBright(`Error: The "${name}" environment variable ${result.message}.`)); + process.exit(1); + } + }); +}; + +module.exports = { + env, + load, +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 2181c22..79a8a45 100644 --- a/src/index.js +++ b/src/index.js @@ -23,20 +23,12 @@ /* eslint-disable no-console */ -process.env.NODE_ENV ??= 'development'; // make sure NODE_ENV is set -require('dotenv').config(); // load env file - const pkg = require('../package.json'); const banner = require('./lib/banner'); console.log(banner(pkg.version)); // print big title -const fs = require('fs'); const semver = require('semver'); const { colours } = require('leeks.js'); -const logger = require('./lib/logger'); -const YAML = require('yaml'); -const Client = require('./client'); -const http = require('./http'); // check node version if (!semver.satisfies(process.versions.node, pkg.engines.node)) { @@ -44,10 +36,15 @@ if (!semver.satisfies(process.versions.node, pkg.engines.node)) { process.exit(1); } -if (process.env.ENCRYPTION_KEY === undefined) { - console.log('\x07' + colours.redBright('Error: The "ENCRYPTION_KEY" environment variable is not set.\nRun "npm run keygen" to generate a key.')); - process.exit(1); -} +// this could be done first, but then there would be no banner :( +process.env.NODE_ENV ??= 'development'; // make sure NODE_ENV is set +require('./env').load(); // load and check environment variables + +const fs = require('fs'); +const YAML = require('yaml'); +const logger = require('./lib/logger'); +const Client = require('./client'); +const http = require('./http'); if (!fs.existsSync('./user/config.yml')) { const examplePath = './user/example.config.yml';