diff --git a/bin/cli-serve b/bin/cli-serve index eca23303b..7978c6c85 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -9,13 +9,15 @@ const kue = require('../services/kue'); const mongoose = require('../services/mongoose'); const util = require('./util'); const {createSubscriptionManager} = require('../graph/subscriptions'); +const { + PORT +} = require('../config'); /** * Get port from environment and store in Express. */ -const port = normalizePort(process.env.TALK_PORT || '3000'); - +const port = normalizePort(PORT); app.set('port', port); /** diff --git a/bin/commander.js b/bin/commander.js index ad346a911..328a25b29 100644 --- a/bin/commander.js +++ b/bin/commander.js @@ -3,11 +3,6 @@ const dotenv = require('dotenv'); const fs = require('fs'); const program = require('commander'); -// Perform rewrites to the runtime environment variables based on the contents -// of the process.env.REWRITE_ENV if it exists. This is done here as it is the -// entrypoint for the entire application. -require('env-rewrite').rewrite(); - //============================================================================== // Setting up the program command line arguments. //============================================================================== diff --git a/config.js b/config.js new file mode 100644 index 000000000..271f13420 --- /dev/null +++ b/config.js @@ -0,0 +1,147 @@ +// This file serves as the entrypoint to all configuration loaded by the +// application. All defaults are assumed here, validation should also be +// completed here. + +// Perform rewrites to the runtime environment variables based on the contents +// of the process.env.REWRITE_ENV if it exists. This is done here as it is the +// entrypoint for the entire applications configuration. +require('env-rewrite').rewrite(); + +//============================================================================== +// CONFIG INITIALIZATION +//============================================================================== + +const CONFIG = { + + //------------------------------------------------------------------------------ + // JWT based configuration + //------------------------------------------------------------------------------ + + // JWT_SECRET is the secret used to sign and verify tokens issued by this + // application. + JWT_SECRET: process.env.TALK_JWT_SECRET || null, + + // JWT_AUDIENCE is the value for the audience claim for the tokens that will be + // verified when decoding. If `JWT_AUDIENCE` is not in the environment, then it + // will default to `talk`. + JWT_AUDIENCE: process.env.TALK_JWT_AUDIENCE || 'talk', + + // JWT_ISSUER is the value for the issuer for the tokens that will be verified + // when decoding. If `JWT_ISSUER` is not in the environment, then it will try + // `TALK_ROOT_URL`, otherwise, it will be undefined. + JWT_ISSUER: process.env.TALK_JWT_ISSUER || process.env.TALK_ROOT_URL || undefined, + + // JWT_EXPIRY is the time for which a given token is valid for. + JWT_EXPIRY: process.env.TALK_JWT_EXPIRY || '1 day', + + //------------------------------------------------------------------------------ + // Installation locks + //------------------------------------------------------------------------------ + + INSTALL_LOCK: process.env.TALK_INSTALL_LOCK === 'TRUE', + + //------------------------------------------------------------------------------ + // External database url's + //------------------------------------------------------------------------------ + + MONGO_URL: process.env.TALK_MONGO_URL, + REDIS_URL: process.env.TALK_REDIS_URL, + + //------------------------------------------------------------------------------ + // Plugins + //------------------------------------------------------------------------------ + + PLUGINS_JSON: process.env.TALK_PLUGINS_JSON, + + //------------------------------------------------------------------------------ + // Server Config + //------------------------------------------------------------------------------ + + // Port to bind to. + PORT: process.env.TALK_PORT || '3000', + + // The URL for this Talk Instance as viewable from the outside. + ROOT_URL: process.env.TALK_ROOT_URL, + + //------------------------------------------------------------------------------ + // Recaptcha configuration + //------------------------------------------------------------------------------ + + RECAPTCHA_ENABLED: false, // updated below + RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC, + RECAPTCHA_SECRET: process.env.TALK_RECAPTCHA_SECRET, + + //------------------------------------------------------------------------------ + // SMTP Server configuration + //------------------------------------------------------------------------------ + + SMTP_FROM_ADDRESS: process.env.TALK_SMTP_FROM_ADDRESS, + SMTP_HOST: process.env.TALK_SMTP_HOST, + SMTP_PASSWORD: process.env.TALK_SMTP_PASSWORD, + SMTP_PORT: process.env.TALK_SMTP_PORT, + SMTP_USERNAME: process.env.TALK_SMTP_USERNAME +}; + +//============================================================================== +// CONFIG VALIDATION +//============================================================================== + +//------------------------------------------------------------------------------ +// JWT based configuration +//------------------------------------------------------------------------------ + +if (process.env.NODE_ENV === 'test' && !CONFIG.JWT_SECRET) { + CONFIG.JWT_SECRET = 'keyboard cat'; +} else if (!CONFIG.JWT_SECRET) { + throw new Error('TALK_JWT_SECRET must be provided in the environment to sign/verify tokens'); +} + +//------------------------------------------------------------------------------ +// External database url's +//------------------------------------------------------------------------------ + +// Reset the mongo url in the event it hasn't been overrided and we are in a +// testing environment. Every new mongo instance comes with a test database by +// default, this is consistent with common testing and use case practices. +if (process.env.NODE_ENV === 'test' && !CONFIG.MONGO_URL) { + CONFIG.MONGO_URL = 'mongodb://localhost/test'; +} + +// Reset the redis url in the event it hasn't been overrided and we are in a +// testing environment. +if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) { + CONFIG.REDIS_URL = 'redis://localhost'; +} + +//------------------------------------------------------------------------------ +// Recaptcha configuration +//------------------------------------------------------------------------------ + +/** + * This is true when the recaptcha secret is provided and the Recaptcha feature + * is to be enabled. + */ +CONFIG.RECAPTCHA_ENABLED = CONFIG.RECAPTCHA_SECRET && CONFIG.RECAPTCHA_SECRET.length > 0 && + CONFIG.RECAPTCHA_PUBLIC && CONFIG.RECAPTCHA_PUBLIC.length > 0; +if (!CONFIG.RECAPTCHA_ENABLED) { + console.warn('Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.'); +} + +//------------------------------------------------------------------------------ +// SMTP Server configuration +//------------------------------------------------------------------------------ + +{ + const requiredProps = [ + 'SMTP_FROM_ADDRESS', + 'SMTP_USERNAME', + 'SMTP_PASSWORD', + 'SMTP_HOST' + ]; + + if (requiredProps.some((prop) => !CONFIG[prop])) { + console.warn(`${requiredProps.map((v) => `TALK_${v}`).join(', ')} should be defined in the environment if you would like to send password reset emails from Talk`); + } +} + +module.exports = CONFIG; diff --git a/plugins.js b/plugins.js index 22293c975..f353cd392 100644 --- a/plugins.js +++ b/plugins.js @@ -4,6 +4,9 @@ const resolve = require('resolve'); const debug = require('debug')('talk:plugins'); const Joi = require('joi'); const amp = require('app-module-path'); +const { + PLUGINS_JSON +} = require('./config'); // Add the current path to the module root. amp.addPath(__dirname); @@ -18,7 +21,7 @@ try { let customPlugins = path.join(__dirname, 'plugins.json'); let defaultPlugins = path.join(__dirname, 'plugins.default.json'); - if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) { + if (PLUGINS_JSON && PLUGINS_JSON.length > 0) { debug('Now using TALK_PLUGINS_JSON environment variable for plugins'); plugins = require(envPlugins); } else if (fs.existsSync(customPlugins)) { diff --git a/routes/admin/index.js b/routes/admin/index.js index 1a9769591..44ce83222 100644 --- a/routes/admin/index.js +++ b/routes/admin/index.js @@ -1,5 +1,8 @@ const express = require('express'); const router = express.Router(); +const { + RECAPTCHA_PUBLIC +} = require('../../config'); // Get /email-confirmation expects a signed JWT in the hash router.get('/confirm-email', (req, res) => { @@ -17,7 +20,7 @@ router.get('/password-reset', (req, res) => { router.get('*', (req, res) => { const data = { - TALK_RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC + TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC }; res.render('admin', {basePath: '/client/coral-admin', data}); diff --git a/routes/api/account/index.js b/routes/api/account/index.js index e1c73d638..4c49c00a5 100644 --- a/routes/api/account/index.js +++ b/routes/api/account/index.js @@ -4,6 +4,9 @@ const UsersService = require('../../../services/users'); const mailer = require('../../../services/mailer'); const authorization = require('../../../middleware/authorization'); const errors = require('../../../errors'); +const { + ROOT_URL +} = require('../../../config'); //============================================================================== // ROUTES @@ -62,7 +65,7 @@ router.post('/password/reset', (req, res, next) => { template: 'password-reset', // needed to know which template to render! locals: { // specifies the template locals. token, - rootURL: process.env.TALK_ROOT_URL + rootURL: ROOT_URL }, subject: 'Password Reset', to: email diff --git a/routes/api/users/index.js b/routes/api/users/index.js index a01fed2c9..9d81b87b7 100644 --- a/routes/api/users/index.js +++ b/routes/api/users/index.js @@ -5,6 +5,9 @@ const CommentsService = require('../../../services/comments'); const mailer = require('../../../services/mailer'); const errors = require('../../../errors'); const authorization = require('../../../middleware/authorization'); +const { + ROOT_URL +} = require('../../../config'); // get a list of users. router.get('/', authorization.needed('ADMIN'), (req, res, next) => { @@ -108,7 +111,7 @@ const SendEmailConfirmation = (app, userID, email, referer) => UsersService template: 'email-confirm', // needed to know which template to render! locals: { // specifies the template locals. token, - rootURL: process.env.TALK_ROOT_URL, + rootURL: ROOT_URL, email }, subject: 'Email Confirmation', diff --git a/routes/embed/index.js b/routes/embed/index.js index efa563422..0aaa9ff9c 100644 --- a/routes/embed/index.js +++ b/routes/embed/index.js @@ -1,6 +1,9 @@ const express = require('express'); const router = express.Router(); const SettingsService = require('../../services/settings'); +const { + RECAPTCHA_PUBLIC +} = require('../../config'); router.use('/:embed', (req, res, next) => { switch (req.params.embed) { @@ -8,7 +11,7 @@ router.use('/:embed', (req, res, next) => { return SettingsService.retrieve() .then(({customCssUrl}) => { const data = { - TALK_RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC + TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC }; return res.render('embed/stream', {customCssUrl, data}); diff --git a/services/mailer.js b/services/mailer.js index 35741ee9f..f3d9235ba 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -5,16 +5,13 @@ const path = require('path'); const fs = require('fs'); const _ = require('lodash'); -const smtpRequiredProps = [ - 'TALK_SMTP_FROM_ADDRESS', - 'TALK_SMTP_USERNAME', - 'TALK_SMTP_PASSWORD', - 'TALK_SMTP_HOST' -]; - -if (smtpRequiredProps.some(prop => !process.env[prop])) { - console.error(`${smtpRequiredProps.join(', ')} should be defined in the environment if you would like to send password reset emails from Talk`); -} +const { + SMTP_HOST, + SMTP_USERNAME, + SMTP_PORT, + SMTP_PASSWORD, + SMTP_FROM_ADDRESS +} = require('../config'); // load all the templates as strings const templates = { @@ -56,15 +53,15 @@ templates.render = (name, format = 'txt', context) => new Promise((resolve, reje }); const options = { - host: process.env.TALK_SMTP_HOST, + host: SMTP_HOST, auth: { - user: process.env.TALK_SMTP_USERNAME, - pass: process.env.TALK_SMTP_PASSWORD + user: SMTP_USERNAME, + pass: SMTP_PASSWORD } }; -if (process.env.TALK_SMTP_PORT) { - options.port = process.env.TALK_SMTP_PORT; +if (SMTP_PORT) { + options.port = SMTP_PORT; } else { options.port = 25; } @@ -126,7 +123,7 @@ const mailer = module.exports = { debug(`Starting to send mail for Job[${id}]`); // Set the `from` field. - data.message.from = process.env.TALK_SMTP_FROM_ADDRESS; + data.message.from = SMTP_FROM_ADDRESS; // Actually send the email. defaultTransporter.sendMail(data.message, (err) => { diff --git a/services/mongoose.js b/services/mongoose.js index c7092326f..acda2f564 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -1,7 +1,12 @@ const mongoose = require('mongoose'); const debug = require('debug')('talk:db'); +const enabled = require('debug').enabled; const queryDebuger = require('debug')('talk:db:query'); +const { + MONGO_URL +} = require('../config'); + // Loading the formatter from Mongoose: // // https://github.com/Automattic/mongoose/blob/1a93d1f4d12e441e17ddf451e96fbc5f6e8f54b8/lib/drivers/node-mongodb-native/collection.js#L182 @@ -24,18 +29,6 @@ function debugQuery(name, i, ...args) { queryDebuger(functionCall + params); } -const enabled = require('debug').enabled; - -// Pull the mongo url out of the environment. -let url = process.env.TALK_MONGO_URL; - -// Reset the mongo url in the event it hasn't been overrided and we are in a -// testing environment. Every new mongo instance comes with a test database by -// default, this is consistent with common testing and use case practices. -if (process.env.NODE_ENV === 'test' && !url) { - url = 'mongodb://localhost/test'; -} - // Use native promises mongoose.Promise = global.Promise; @@ -48,7 +41,7 @@ if (enabled('talk:db')) { } // Connect to the Mongo instance. -mongoose.connect(url, (err) => { +mongoose.connect(MONGO_URL, (err) => { if (err) { throw err; } diff --git a/services/passport.js b/services/passport.js index 41d57a539..fd56a8c86 100644 --- a/services/passport.js +++ b/services/passport.js @@ -9,25 +9,14 @@ const errors = require('../errors'); const uuid = require('uuid'); const debug = require('debug')('talk:passport'); -// JWT_SECRET is the secret used to sign and verify tokens issued by this -// application. -const JWT_SECRET = process.env.JWT_SECRET || null; -if (JWT_SECRET === null) { - throw new Error('JWT_SECRET must be provided in the environment to sign/verify tokens'); -} - -// JWT_EXPIRY is the time for which a given token is valid for. -const JWT_EXPIRY = process.env.JWT_EXPIRY || '1 day'; - -// JWT_ISSUER is the value for the issuer for the tokens that will be verified -// when decoding. If `JWT_ISSUER` is not in the environment, then it will try -// `TALK_ROOT_URL`, otherwise, it will be undefined. -const JWT_ISSUER = process.env.JWT_ISSUER || process.env.TALK_ROOT_URL || undefined; - -// JWT_AUDIENCE is the value for the audience claim for the tokens that will be -// verified when decoding. If `JWT_AUDIENCE` is not in the environment, then it -// will default to `talk`. -const JWT_AUDIENCE = process.env.JWT_AUDIENCE || 'talk'; +const { + JWT_SECRET, + JWT_ISSUER, + JWT_EXPIRY, + JWT_AUDIENCE, + RECAPTCHA_SECRET, + RECAPTCHA_ENABLED +} = require('../config'); // GenerateToken will sign a token to include all the authorization information // needed for the front end. @@ -200,21 +189,6 @@ const CheckIfNeedsRecaptcha = (user, email) => { return false; }; -/** - * This stores the Recaptcha secret. - */ -const RECAPTCHA_SECRET = process.env.TALK_RECAPTCHA_SECRET; -const RECAPTCHA_PUBLIC = process.env.TALK_RECAPTCHA_PUBLIC; - -/** - * This is true when the recaptcha secret is provided and the Recaptcha feature - * is to be enabled. - */ -const RECAPTCHA_ENABLED = RECAPTCHA_SECRET && RECAPTCHA_SECRET.length > 0 && RECAPTCHA_PUBLIC && RECAPTCHA_PUBLIC.length > 0; -if (!RECAPTCHA_ENABLED) { - console.log('Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.'); -} - /** * This sends the request details down Google to check to see if the response is * genuine or not. diff --git a/services/redis.js b/services/redis.js index d27303fca..c049b2d00 100644 --- a/services/redis.js +++ b/services/redis.js @@ -1,9 +1,11 @@ const redis = require('redis'); const debug = require('debug')('talk:redis'); -const url = process.env.TALK_REDIS_URL || 'redis://localhost'; +const { + REDIS_URL +} = require('../config'); const connectionOptions = { - url, + url: REDIS_URL, retry_strategy: function(options) { if (options.error && options.error.code === 'ECONNREFUSED') { diff --git a/services/setup.js b/services/setup.js index 1f1542d5e..233021c17 100644 --- a/services/setup.js +++ b/services/setup.js @@ -2,6 +2,9 @@ const UsersService = require('./users'); const SettingsService = require('./settings'); const SettingsModel = require('../models/setting'); const errors = require('../errors'); +const { + INSTALL_LOCK +} = require('../config'); /** * This service is used when we want to setup the application. It is consumed by @@ -15,7 +18,7 @@ module.exports = class SetupService { static isAvailable() { // Check if we have an install lock present. - if (process.env.TALK_INSTALL_LOCK === 'TRUE') { + if (INSTALL_LOCK) { return Promise.reject(errors.ErrInstallLock); } diff --git a/services/users.js b/services/users.js index 59cc6ba7d..03c1671d5 100644 --- a/services/users.js +++ b/services/users.js @@ -1,12 +1,14 @@ const assert = require('assert'); +const uuid = require('uuid'); const bcrypt = require('bcrypt'); const url = require('url'); const jwt = require('jsonwebtoken'); const Wordlist = require('./wordlist'); - const errors = require('../errors'); - -const uuid = require('uuid'); +const { + JWT_SECRET, + ROOT_URL +} = require('../config'); const redis = require('./redis'); const redisClient = redis.createClient(); @@ -22,14 +24,6 @@ const SettingsService = require('./settings'); const ActionsService = require('./actions'); const MailerService = require('./mailer'); -// In the event that the TALK_JWT_SECRET is missing but we are testing, then -// set the process.env.TALK_JWT_SECRET. -if (process.env.NODE_ENV === 'test' && !process.env.TALK_JWT_SECRET) { - process.env.TALK_JWT_SECRET = 'keyboard cat'; -} else if (!process.env.TALK_JWT_SECRET) { - throw new Error('TALK_JWT_SECRET must be defined to encode JSON Web Tokens and other auth functionality'); -} - const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm'; const PASSWORD_RESET_JWT_SUBJECT = 'password_reset'; @@ -564,7 +558,7 @@ module.exports = class UsersService { version: user.__v }; - return jwt.sign(payload, process.env.TALK_JWT_SECRET, { + return jwt.sign(payload, JWT_SECRET, { algorithm: 'HS256', expiresIn: '1d', subject: PASSWORD_RESET_JWT_SUBJECT @@ -583,7 +577,7 @@ module.exports = class UsersService { // Set the allowed algorithms. options.algorithms = ['HS256']; - jwt.verify(token, process.env.TALK_JWT_SECRET, options, (err, decoded) => { + jwt.verify(token, JWT_SECRET, options, (err, decoded) => { if (err) { return reject(err); } @@ -697,7 +691,7 @@ module.exports = class UsersService { * @param {String} email The email that we are needing to get confirmed. * @return {Promise} */ - static createEmailConfirmToken(userID = null, email, referer = process.env.TALK_ROOT_URL) { + static createEmailConfirmToken(userID = null, email, referer = ROOT_URL) { if (!email || typeof email !== 'string') { return Promise.reject('email is required when creating a JWT for resetting passord'); } @@ -740,7 +734,7 @@ module.exports = class UsersService { email, referer, userID: user.id - }, process.env.TALK_JWT_SECRET, tokenOptions); + }, JWT_SECRET, tokenOptions); }); } diff --git a/webpack.config.js b/webpack.config.js index 3a54e85c0..5aade15a1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,13 +9,18 @@ const webpack = require('webpack'); // Possibly load the config from the .env file (if there is one). require('dotenv').config(); +// Load the config after dotenv has does it's thing. +const { + PLUGINS_JSON +} = require('./config'); + let pluginsConfigPath; let envPlugins = path.join(__dirname, 'plugins.env.js'); let customPlugins = path.join(__dirname, 'plugins.json'); let defaultPlugins = path.join(__dirname, 'plugins.default.json'); -if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) { +if (PLUGINS_JSON && PLUGINS_JSON.length > 0) { pluginsConfigPath = envPlugins; } else if (fs.existsSync(customPlugins)) { pluginsConfigPath = customPlugins;