Repository: rickbergfalk/sqlpad
Pull request #539 (2020-02-19 17:39:22)

Inject config into express app
Refactors express app related things to favor the `config` object being injected into the app or utility as opposed to referencing `lib/config` directly. 

This is a portion of the work related to addressing #537 

Another PR will follow tackling refactoring db and related things, though that will likely see a larger restructuring to make things convenient to work with


Patch:
diff --git a/server/app.js b/server/app.js
index 8a3301a2..1e206c7f 100644
--- a/server/app.js
+++ b/server/app.js
@@ -4,150 +4,167 @@ const express = require('express');
 const helmet = require('helmet');
 const session = require('express-session');
 const FileStore = require('session-file-store')(session);
-const config = require('./lib/config');
 const logger = require('./lib/logger');
 
-const baseUrl = config.get('baseUrl');
-const dbPath = config.get('dbPath');
-const debug = config.get('debug');
-const webLogLevel = config.get('webLogLevel');
-const cookieName = config.get('cookieName');
-const cookieSecret = config.get('cookieSecret');
-const sessionMinutes = config.get('sessionMinutes');
-
-const expressPino = require('express-pino-logger')({
-  level: webLogLevel,
-  name: 'sqlpad-web',
-  // express-pino-logger logs all the headers by default
-  // Removing these for now but open to adding them back in based on feedback
-  redact: {
-    paths: [
-      'req.headers',
-      'res.headers',
-      'req.remoteAddress',
-      'req.remotePort'
-    ],
-    remove: true
+/**
+ * Create an express app using config
+ * @param {object} config
+ */
+function makeApp(config) {
+  if (typeof config.get !== 'function') {
+    throw new Error('config is required to create app');
   }
-});
 
-/*  Express setup
+  const baseUrl = config.get('baseUrl');
+  const dbPath = config.get('dbPath');
+  const debug = config.get('debug');
+  const webLogLevel = config.get('webLogLevel');
+  const cookieName = config.get('cookieName');
+  const cookieSecret = config.get('cookieSecret');
+  const sessionMinutes = config.get('sessionMinutes');
+
+  const expressPino = require('express-pino-logger')({
+    level: webLogLevel,
+    name: 'sqlpad-web',
+    // express-pino-logger logs all the headers by default
+    // Removing these for now but open to adding them back in based on feedback
+    redact: {
+      paths: [
+        'req.headers',
+        'res.headers',
+        'req.remoteAddress',
+        'req.remotePort'
+      ],
+      remove: true
+    }
+  });
+
+  /*  Express setup
 ============================================================================= */
-const bodyParser = require('body-parser');
-const favicon = require('serve-favicon');
-const passport = require('passport');
-const passportBasic = require('./middleware/passport-basic');
-
-const app = express();
-
-// Default helmet protections, minus frameguard (becaue of sqlpad iframe embed), adding referrerPolicy
-app.use(helmet.dnsPrefetchControl());
-app.use(helmet.hidePoweredBy());
-app.use(helmet.hsts({}));
-app.use(helmet.ieNoOpen());
-app.use(helmet.noSniff());
-app.use(helmet.xssFilter());
-app.use(helmet.referrerPolicy({ policy: 'same-origin' }));
-
-app.set('env', debug ? 'development' : 'production');
-
-app.use(expressPino);
-app.use(favicon(path.join(__dirname, '/public/favicon.ico')));
-app.use(bodyParser.json());
-app.use(
-  bodyParser.urlencoded({
-    extended: true
-  })
-);
-
-app.use(
-  session({
-    store: new FileStore({
-      path: path.join(dbPath, '/sessions')
-    }),
-    saveUninitialized: false,
-    resave: true,
-    rolling: true,
-    cookie: { maxAge: 1000 * 60 * sessionMinutes },
-    secret: cookieSecret,
-    name: cookieName
-  })
-);
-
-app.use(passport.initialize());
-app.use(passport.session());
-app.use(baseUrl, express.static(path.join(__dirname, 'public')));
-
-/*  Passport setup
+  const bodyParser = require('body-parser');
+  const favicon = require('serve-favicon');
+  const passport = require('passport');
+  const passportBasic = require('./middleware/passport-basic');
+
+  const app = express();
+
+  // Default helmet protections, minus frameguard (becaue of sqlpad iframe embed), adding referrerPolicy
+  app.use(helmet.dnsPrefetchControl());
+  app.use(helmet.hidePoweredBy());
+  app.use(helmet.hsts({}));
+  app.use(helmet.ieNoOpen());
+  app.use(helmet.noSniff());
+  app.use(helmet.xssFilter());
+  app.use(helmet.referrerPolicy({ policy: 'same-origin' }));
+
+  // Add config to req.config
+  app.use(function(req, res, next) {
+    req.config = config;
+    next();
+  });
+
+  app.set('env', debug ? 'development' : 'production');
+
+  app.use(expressPino);
+  app.use(favicon(path.join(__dirname, '/public/favicon.ico')));
+  app.use(bodyParser.json());
+  app.use(
+    bodyParser.urlencoded({
+      extended: true
+    })
+  );
+
+  app.use(
+    session({
+      store: new FileStore({
+        path: path.join(dbPath, '/sessions')
+      }),
+      saveUninitialized: false,
+      resave: true,
+      rolling: true,
+      cookie: { maxAge: 1000 * 60 * sessionMinutes },
+      secret: cookieSecret,
+      name: cookieName
+    })
+  );
+
+  app.use(passport.initialize());
+  app.use(passport.session());
+  app.use(baseUrl, express.static(path.join(__dirname, 'public')));
+
+  /*  Passport setup
 ============================================================================= */
-require('./middleware/passport.js');
+  require('./middleware/passport.js');
 
-// If local auth is not disabled, support basic auth using a user's email and password
-// This is currently used for running integration tests and serves as a convenient alternative to API keys
-if (!config.get('disableUserpassAuth')) {
-  app.use(passportBasic);
-}
+  // If local auth is not disabled, support basic auth using a user's email and password
+  // This is currently used for running integration tests and serves as a convenient alternative to API keys
+  if (!config.get('disableUserpassAuth')) {
+    app.use(passportBasic);
+  }
 
-/*  Routes
+  /*  Routes
 ============================================================================= */
-const routers = [
-  require('./routes/drivers.js'),
-  require('./routes/users.js'),
-  require('./routes/forgot-password.js'),
-  require('./routes/password-reset.js'),
-  require('./routes/connections.js'),
-  require('./routes/connection-accesses.js'),
-  require('./routes/test-connection.js'),
-  require('./routes/queries.js'),
-  require('./routes/query-history.js'),
-  require('./routes/query-result.js'),
-  require('./routes/download-results.js'), // streams result download to browser
-  require('./routes/schema-info.js'),
-  require('./routes/tags.js'),
-  require('./routes/format-sql.js'),
-  require('./routes/signup-signin-signout.js'),
-  require('./routes/oauth.js'),
-  require('./routes/saml.js')
-];
-
-// Add all core routes to the baseUrl except for the */api/app route
-routers.forEach(function(router) {
-  app.use(baseUrl, router);
-});
-
-// Add '*/api/app' route last and without baseUrl
-app.use(require('./routes/app.js'));
-
-// For any missing api route, return a 404
-// NOTE - this cannot be a general catch-all because it might be a valid non-api route from a front-end perspective
-app.use(baseUrl + '/api/', function(req, res) {
-  req.log.debug('reached catch all api route');
-  res.sendStatus(404);
-});
-
-// Anything else should render the client-side app
-// Client-side routing will take care of things from here
-// Because index.html will be served via static plugin,
-// we need to rename it to something else and switch out the URLs to consider the baseUrl
-const indexPath = path.join(__dirname, 'public/index.html');
-const indexTemplatePath = path.join(__dirname, 'public/index-template.html');
-
-if (fs.existsSync(indexPath)) {
-  fs.renameSync(indexPath, indexTemplatePath);
-}
+  const routers = [
+    require('./routes/drivers.js'),
+    require('./routes/users.js'),
+    require('./routes/forgot-password.js'),
+    require('./routes/password-reset.js'),
+    require('./routes/connections.js'),
+    require('./routes/connection-accesses.js'),
+    require('./routes/test-connection.js'),
+    require('./routes/queries.js'),
+    require('./routes/query-history.js'),
+    require('./routes/query-result.js'),
+    require('./routes/download-results.js'), // streams result download to browser
+    require('./routes/schema-info.js'),
+    require('./routes/tags.js'),
+    require('./routes/format-sql.js'),
+    require('./routes/signup-signin-signout.js')(config),
+    require('./routes/oauth.js')(config),
+    require('./routes/saml.js')(config)
+  ];
+
+  // Add all core routes to the baseUrl except for the */api/app route
+  routers.forEach(function(router) {
+    app.use(baseUrl, router);
+  });
+
+  // Add '*/api/app' route last and without baseUrl
+  app.use(require('./routes/app.js'));
+
+  // For any missing api route, return a 404
+  // NOTE - this cannot be a general catch-all because it might be a valid non-api route from a front-end perspective
+  app.use(baseUrl + '/api/', function(req, res) {
+    req.log.debug('reached catch all api route');
+    res.sendStatus(404);
+  });
+
+  // Anything else should render the client-side app
+  // Client-side routing will take care of things from here
+  // Because index.html will be served via static plugin,
+  // we need to rename it to something else and switch out the URLs to consider the baseUrl
+  const indexPath = path.join(__dirname, 'public/index.html');
+  const indexTemplatePath = path.join(__dirname, 'public/index-template.html');
+
+  if (fs.existsSync(indexPath)) {
+    fs.renameSync(indexPath, indexTemplatePath);
+  }
+
+  if (fs.existsSync(indexTemplatePath)) {
+    const html = fs.readFileSync(indexTemplatePath, 'utf8');
+    const baseUrlHtml = html
+      .replace(/="\/stylesheets/g, `="${baseUrl}/stylesheets`)
+      .replace(/="\/javascripts/g, `="${baseUrl}/javascripts`)
+      .replace(/="\/images/g, `="${baseUrl}/images`)
+      .replace(/="\/fonts/g, `="${baseUrl}/fonts`)
+      .replace(/="\/static/g, `="${baseUrl}/static`);
+    app.use((req, res) => res.send(baseUrlHtml));
+  } else {
+    logger.warn('NO FRONT END TEMPLATE DETECTED');
+    logger.warn('If not running in dev mode please report this issue.');
+  }
 
-if (fs.existsSync(indexTemplatePath)) {
-  const html = fs.readFileSync(indexTemplatePath, 'utf8');
-  const baseUrlHtml = html
-    .replace(/="\/stylesheets/g, `="${baseUrl}/stylesheets`)
-    .replace(/="\/javascripts/g, `="${baseUrl}/javascripts`)
-    .replace(/="\/images/g, `="${baseUrl}/images`)
-    .replace(/="\/fonts/g, `="${baseUrl}/fonts`)
-    .replace(/="\/static/g, `="${baseUrl}/static`);
-  app.use((req, res) => res.send(baseUrlHtml));
-} else {
-  logger.warn('NO FRONT END TEMPLATE DETECTED');
-  logger.warn('If not running in dev mode please report this issue.');
+  return app;
 }
 
-module.exports = app;
+module.exports = makeApp;
diff --git a/server/lib/cipher.js b/server/lib/cipher.js
deleted file mode 100644
index af03dc07..00000000
--- a/server/lib/cipher.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const crypto = require('crypto');
-const algorithm = 'aes256';
-const config = require('../lib/config');
-
-const passphrase = config.get('passphrase');
-
-module.exports = function(text) {
-  const myCipher = crypto.createCipher(algorithm, passphrase);
-  return myCipher.update(text, 'utf8', 'hex') + myCipher.final('hex');
-};
diff --git a/server/lib/decipher.js b/server/lib/decipher.js
deleted file mode 100644
index 6bf544fe..00000000
--- a/server/lib/decipher.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const crypto = require('crypto');
-const algorithm = 'aes256';
-const config = require('../lib/config');
-const passphrase = config.get('passphrase');
-
-/**
- * @param {string} gibberish ciphered value that needs deciphering
- * @returns {string} deciphered value
- */
-module.exports = function decipher(gibberish) {
-  const myDecipher = crypto.createDecipher(algorithm, passphrase);
-  return myDecipher.update(gibberish, 'hex', 'utf8') + myDecipher.final('utf8');
-};
diff --git a/server/lib/email.js b/server/lib/email.js
index 2dafc475..66371acb 100644
--- a/server/lib/email.js
+++ b/server/lib/email.js
@@ -1,83 +1,86 @@
 const nodemailer = require('nodemailer');
 const logger = require('./logger');
-const config = require('./config');
 
-/**
- * Get full sqlpad url
- * @param {string} path - path (leading slash)
- */
-function fullUrl(path) {
-  const port = config.get('port');
-  const urlPort = port === 80 ? '' : ':' + port;
-  const urlPublicUrl = config.get('publicUrl');
-  const urlBaseUrl = config.get('baseUrl');
-  return `${urlPublicUrl}${urlPort}${urlBaseUrl}${path}`;
-}
+function makeEmail(config) {
+  /**
+   * Get full sqlpad url
+   * @param {string} path - path (leading slash)
+   */
+  function fullUrl(path) {
+    const port = config.get('port');
+    const urlPort = port === 80 ? '' : ':' + port;
+    const urlPublicUrl = config.get('publicUrl');
+    const urlBaseUrl = config.get('baseUrl');
+    return `${urlPublicUrl}${urlPort}${urlBaseUrl}${path}`;
+  }
 
-function sendForgotPassword(to, passwordResetPath) {
-  const url = fullUrl(passwordResetPath);
-  const text = `Hello! \n\nYou recently requested a password reset for your SQLPad account. \n\nTo reset your password, visit ${url}.`;
-  const html = `
+  function sendForgotPassword(to, passwordResetPath) {
+    const url = fullUrl(passwordResetPath);
+    const text = `Hello! \n\nYou recently requested a password reset for your SQLPad account. \n\nTo reset your password, visit ${url}.`;
+    const html = `
     <p>Hello!</p>
     <p>You recently requested a password reset for your SQLPad account.</p>
     <p>To reset your password, visit <a href="${url}">${url}</a>.</p>
   `;
-  return send(to, 'SQLPad Password Reset', text, html);
-}
+    return send(to, 'SQLPad Password Reset', text, html);
+  }
 
-function sendInvite(to) {
-  const url = fullUrl('/signup');
-  const text = `Hello! \n\nA colleague has invited you to SQLPad. \n\nTo sign up, visit ${url}.`;
-  const html = `
+  function sendInvite(to) {
+    const url = fullUrl('/signup');
+    const text = `Hello! \n\nA colleague has invited you to SQLPad. \n\nTo sign up, visit ${url}.`;
+    const html = `
     <p>Hello!</p> 
     <p>A colleague has invited you to SQLPad.</p> 
     <p>To sign up, visit <a href="${url}">'${url}</a>.</p>
   `;
-  return send(to, "You've been invited to SQLPad", text, html);
-}
-
-async function send(to, subject, text, html) {
-  if (!config.smtpConfigured()) {
-    logger.error('email.send() called without being configured');
-    return;
+    return send(to, "You've been invited to SQLPad", text, html);
   }
 
-  const smtpConfig = {
-    host: config.get('smtpHost'),
-    port: config.get('smtpPort'),
-    secure: config.get('smtpSecure'),
-    auth: {
-      user: config.get('smtpUser'),
-      pass: config.get('smtpPassword')
-    },
-    tls: {
-      ciphers: 'SSLv3'
+  async function send(to, subject, text, html) {
+    if (!config.smtpConfigured()) {
+      logger.error('email.send() called without being configured');
+      return;
     }
-  };
 
-  return new Promise((resolve, reject) => {
-    const transporter = nodemailer.createTransport(smtpConfig);
-    const mailOptions = {
-      from: config.get('smtpFrom'),
-      to,
-      subject,
-      text,
-      html
-    };
-    transporter.sendMail(mailOptions, function(err, info) {
-      logger.info('sent email: %s', info);
-      if (err) {
-        logger.error(err);
-        return reject(err);
+    const smtpConfig = {
+      host: config.get('smtpHost'),
+      port: config.get('smtpPort'),
+      secure: config.get('smtpSecure'),
+      auth: {
+        user: config.get('smtpUser'),
+        pass: config.get('smtpPassword')
+      },
+      tls: {
+        ciphers: 'SSLv3'
       }
-      resolve(info);
+    };
+
+    return new Promise((resolve, reject) => {
+      const transporter = nodemailer.createTransport(smtpConfig);
+      const mailOptions = {
+        from: config.get('smtpFrom'),
+        to,
+        subject,
+        text,
+        html
+      };
+      transporter.sendMail(mailOptions, function(err, info) {
+        logger.info('sent email: %s', info);
+        if (err) {
+          logger.error(err);
+          return reject(err);
+        }
+        resolve(info);
+      });
     });
-  });
+  }
+
+  return {
+    fullUrl,
+    send,
+    sendForgotPassword,
+    sendInvite
+  };
 }
 
-module.exports = {
-  fullUrl,
-  send,
-  sendForgotPassword,
-  sendInvite
-};
+module.exports = makeEmail;
diff --git a/server/lib/makeCipher.js b/server/lib/makeCipher.js
new file mode 100644
index 00000000..0a7a6afb
--- /dev/null
+++ b/server/lib/makeCipher.js
@@ -0,0 +1,18 @@
+const crypto = require('crypto');
+const algorithm = 'aes256';
+
+module.exports = function makeCipher(passphrase) {
+  return {
+    cipher: text => {
+      // TODO createCipher is deprecated
+      const myCipher = crypto.createCipher(algorithm, passphrase);
+      return myCipher.update(text, 'utf8', 'hex') + myCipher.final('hex');
+    },
+    decipher: gibberish => {
+      const myDecipher = crypto.createDecipher(algorithm, passphrase);
+      return (
+        myDecipher.update(gibberish, 'hex', 'utf8') + myDecipher.final('utf8')
+      );
+    }
+  };
+};
diff --git a/server/lib/pushQueryToSlack.js b/server/lib/pushQueryToSlack.js
index caa2619d..fd75662c 100644
--- a/server/lib/pushQueryToSlack.js
+++ b/server/lib/pushQueryToSlack.js
@@ -1,8 +1,7 @@
 const logger = require('./logger');
-const config = require('./config');
 const request = require('request');
 
-function pushQueryToSlack(query) {
+function pushQueryToSlack(config, query) {
   const SLACK_WEBHOOK = config.get('slackWebhook');
   if (SLACK_WEBHOOK) {
     const PUBLIC_URL = config.get('publicUrl');
@@ -24,8 +23,7 @@ ${'```'}`
     };
     request(options, function(err) {
       if (err) {
-        logger.error('Something went wrong while sending to Slack.');
-        logger.error(err);
+        logger.error(err, 'Problem sending query to slack');
       }
     });
   }
diff --git a/server/middleware/must-be-authenticated-or-chart-link-noauth.js b/server/middleware/must-be-authenticated-or-chart-link-noauth.js
index 43e51b29..67b47fae 100644
--- a/server/middleware/must-be-authenticated-or-chart-link-noauth.js
+++ b/server/middleware/must-be-authenticated-or-chart-link-noauth.js
@@ -1,9 +1,7 @@
-const config = require('../lib/config');
-
 // If authenticated or setting allows it continue. Otherwise redirect user to signin
 module.exports = function mustBeAuthenticatedOrChartLinkNoAuth(req, res, next) {
-  if (req.isAuthenticated() || !config.get('tableChartLinksRequireAuth')) {
+  if (req.isAuthenticated() || !req.config.get('tableChartLinksRequireAuth')) {
     return next();
   }
-  res.redirect(config.get('baseUrl') + '/signin');
+  res.redirect(req.config.get('baseUrl') + '/signin');
 };
diff --git a/server/middleware/must-be-authenticated.js b/server/middleware/must-be-authenticated.js
index 62415d19..ce052176 100644
--- a/server/middleware/must-be-authenticated.js
+++ b/server/middleware/must-be-authenticated.js
@@ -1,9 +1,7 @@
-const config = require('../lib/config');
-
 // If authenticated continue. otherwise redirect user to signin
 module.exports = function mustBeAuthenticated(req, res, next) {
   if (req.isAuthenticated()) {
     return next();
   }
-  res.redirect(config.get('baseUrl') + '/signin');
+  res.redirect(req.config.get('baseUrl') + '/signin');
 };
diff --git a/server/middleware/must-have-connection-access-or-chart-link-noauth.js b/server/middleware/must-have-connection-access-or-chart-link-noauth.js
index 58852560..b8ce3b58 100644
--- a/server/middleware/must-have-connection-access-or-chart-link-noauth.js
+++ b/server/middleware/must-have-connection-access-or-chart-link-noauth.js
@@ -1,4 +1,3 @@
-const config = require('../lib/config');
 const mustBeAuthenticated = require('./must-be-authenticated');
 const connectionAccessesUtil = require('../models/connectionAccesses.js');
 
@@ -9,7 +8,7 @@ module.exports = [
   async function mustHaveConnectionAccessOrChartLinkNoAuth(req, res, next) {
     if (
       req.user.role === 'admin' ||
-      !config.get('tableChartLinksRequireAuth')
+      !req.config.get('tableChartLinksRequireAuth')
     ) {
       return next();
     }
diff --git a/server/models/connections.js b/server/models/connections.js
index e497de7c..7ebb9085 100644
--- a/server/models/connections.js
+++ b/server/models/connections.js
@@ -2,12 +2,14 @@ const db = require('../lib/db.js');
 const logger = require('../lib/logger');
 const _ = require('lodash');
 const drivers = require('../drivers');
-const cipher = require('../lib/cipher.js');
-const decipher = require('../lib/decipher');
+const makeCipher = require('../lib/makeCipher');
+const config = require('../lib/config');
 const getConfigFromFile = require('../lib/config/fromFile.js');
 
 const [configFromFile] = getConfigFromFile() || {};
 
+const { cipher, decipher } = makeCipher(config.get('passphrase'));
+
 /**
  * Get connections from config.
  *
diff --git a/server/routes/app.js b/server/routes/app.js
index 6ae5a1a8..a63a4e9e 100644
--- a/server/routes/app.js
+++ b/server/routes/app.js
@@ -2,12 +2,12 @@ const router = require('express').Router();
 const packageJson = require('../package.json');
 const usersUtil = require('../models/users.js');
 const sendError = require('../lib/sendError');
-const config = require('../lib/config');
 
 // NOTE: this route needs a wildcard because it is fetched as a relative url
 // from the front-end. The static SPA does not know if sqlpad is mounted at
 // the root of a domain or if there is a base-url provided in the config
 router.get('*/api/app', async (req, res) => {
+  const { config } = req;
   try {
     const adminRegistrationOpen = await usersUtil.adminRegistrationOpen();
     const currentUser =
diff --git a/server/routes/download-results.js b/server/routes/download-results.js
index 2d72730c..30c8dc7d 100644
--- a/server/routes/download-results.js
+++ b/server/routes/download-results.js
@@ -1,13 +1,12 @@
 const fs = require('fs');
 const router = require('express').Router();
 const resultCache = require('../models/resultCache.js');
-const config = require('../lib/config');
 const logger = require('../lib/logger');
 
 router.get('/download-results/:cacheKey.csv', async function(req, res, next) {
   const { cacheKey } = req.params;
   try {
-    if (config.get('allowCsvDownload')) {
+    if (req.config.get('allowCsvDownload')) {
       const cache = await resultCache.findOneByCacheKey(cacheKey);
       if (!cache) {
         return next(new Error('Result cache not found'));
@@ -32,7 +31,7 @@ router.get('/download-results/:cacheKey.csv', async function(req, res, next) {
 router.get('/download-results/:cacheKey.xlsx', async function(req, res, next) {
   const { cacheKey } = req.params;
   try {
-    if (config.get('allowCsvDownload')) {
+    if (req.config.get('allowCsvDownload')) {
       const cache = await resultCache.findOneByCacheKey(cacheKey);
       if (!cache) {
         return next(new Error('Result cache not found'));
@@ -60,7 +59,7 @@ router.get('/download-results/:cacheKey.xlsx', async function(req, res, next) {
 router.get('/download-results/:cacheKey.json', async function(req, res, next) {
   const { cacheKey } = req.params;
   try {
-    if (config.get('allowCsvDownload')) {
+    if (req.config.get('allowCsvDownload')) {
       const cache = await resultCache.findOneByCacheKey(cacheKey);
       if (!cache) {
         return next(new Error('Result cache not found'));
diff --git a/server/routes/forgot-password.js b/server/routes/forgot-password.js
index 44e751e0..26d34513 100644
--- a/server/routes/forgot-password.js
+++ b/server/routes/forgot-password.js
@@ -1,19 +1,20 @@
 const router = require('express').Router();
 const uuid = require('uuid');
 const usersUtil = require('../models/users.js');
-const email = require('../lib/email');
+const makeEmail = require('../lib/email');
 const sendError = require('../lib/sendError');
-const config = require('../lib/config');
 const logger = require('../lib/logger');
 
 router.post('/api/forgot-password', async function(req, res) {
   if (!req.body.email) {
     return sendError(res, null, 'Email address must be provided');
   }
-  if (!config.smtpConfigured()) {
+  if (!req.config.smtpConfigured()) {
     return sendError(res, null, 'Email must be configured');
   }
 
+  const email = makeEmail(req.config);
+
   try {
     const user = await usersUtil.findOneByEmail(req.body.email);
 
diff --git a/server/routes/oauth.js b/server/routes/oauth.js
index ae5653f7..6d194fa6 100644
--- a/server/routes/oauth.js
+++ b/server/routes/oauth.js
@@ -1,93 +1,101 @@
 const passport = require('passport');
 const PassportGoogleStrategy = require('passport-google-oauth20').Strategy;
 const router = require('express').Router();
-const config = require('../lib/config');
 const logger = require('../lib/logger');
 const checkWhitelist = require('../lib/check-whitelist.js');
 const usersUtil = require('../models/users.js');
-const baseUrl = config.get('baseUrl');
 
-const googleClientId = config.get('googleClientId');
-const googleClientSecret = config.get('googleClientSecret');
-const publicUrl = config.get('publicUrl');
+/**
+ * Adds Google auth strategy and adds Google auth routes if Google auth is configured
+ * @param {object} config
+ */
+function makeGoogleAuth(config) {
+  const baseUrl = config.get('baseUrl');
+  const googleClientId = config.get('googleClientId');
+  const googleClientSecret = config.get('googleClientSecret');
+  const publicUrl = config.get('publicUrl');
 
-async function passportGoogleStrategyHandler(
-  accessToken,
-  refreshToken,
-  profile,
-  done
-) {
-  const email = profile && profile._json && profile._json.email;
+  async function passportGoogleStrategyHandler(
+    accessToken,
+    refreshToken,
+    profile,
+    done
+  ) {
+    const email = profile && profile._json && profile._json.email;
 
-  if (!email) {
-    return done(null, false, {
-      message: 'email not provided from Google'
-    });
-  }
+    if (!email) {
+      return done(null, false, {
+        message: 'email not provided from Google'
+      });
+    }
 
-  try {
-    let [openAdminRegistration, user] = await Promise.all([
-      usersUtil.adminRegistrationOpen(),
-      usersUtil.findOneByEmail(email)
-    ]);
+    try {
+      let [openAdminRegistration, user] = await Promise.all([
+        usersUtil.adminRegistrationOpen(),
+        usersUtil.findOneByEmail(email)
+      ]);
 
-    if (user) {
-      user.signupDate = new Date();
-      const newUser = await usersUtil.save(user);
-      newUser.id = newUser._id;
-      return done(null, newUser);
-    }
-    const whitelistedDomains = config.get('whitelistedDomains');
-    if (openAdminRegistration || checkWhitelist(whitelistedDomains, email)) {
-      const newUser = await usersUtil.save({
-        email,
-        role: openAdminRegistration ? 'admin' : 'editor',
-        signupDate: new Date()
+      if (user) {
+        user.signupDate = new Date();
+        const newUser = await usersUtil.save(user);
+        newUser.id = newUser._id;
+        return done(null, newUser);
+      }
+      const whitelistedDomains = config.get('whitelistedDomains');
+      if (openAdminRegistration || checkWhitelist(whitelistedDomains, email)) {
+        const newUser = await usersUtil.save({
+          email,
+          role: openAdminRegistration ? 'admin' : 'editor',
+          signupDate: new Date()
+        });
+        newUser.id = newUser._id;
+        return done(null, newUser);
+      }
+      // at this point we don't have an error, but authentication is invalid
+      // per passport docs, we call done() here without an error
+      // instead passing false for user and a message why
+      return done(null, false, {
+        message: "You haven't been invited by an admin yet."
       });
-      newUser.id = newUser._id;
-      return done(null, newUser);
+    } catch (error) {
+      done(error, null);
     }
-    // at this point we don't have an error, but authentication is invalid
-    // per passport docs, we call done() here without an error
-    // instead passing false for user and a message why
-    return done(null, false, {
-      message: "You haven't been invited by an admin yet."
-    });
-  } catch (error) {
-    done(error, null);
   }
-}
 
-if (config.googleAuthConfigured()) {
-  logger.info('Enabling Google authentication strategy.');
+  if (config.googleAuthConfigured()) {
+    logger.info('Enabling Google authentication strategy.');
+
+    // Register the passport strategy
+    passport.use(
+      new PassportGoogleStrategy(
+        {
+          clientID: googleClientId,
+          clientSecret: googleClientSecret,
+          callbackURL: publicUrl + baseUrl + '/auth/google/callback',
+          // This option tells the strategy to use the userinfo endpoint instead
+          userProfileURL:
+            'https://www.googleapis.com/oauth2/v3/userinfo?alt=json'
+        },
+        passportGoogleStrategyHandler
+      )
+    );
 
-  // Register the passport strategy
-  passport.use(
-    new PassportGoogleStrategy(
-      {
-        clientID: googleClientId,
-        clientSecret: googleClientSecret,
-        callbackURL: publicUrl + baseUrl + '/auth/google/callback',
-        // This option tells the strategy to use the userinfo endpoint instead
-        userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo?alt=json'
-      },
-      passportGoogleStrategyHandler
-    )
-  );
+    // Add routes for oauth flow
+    router.get(
+      '/auth/google',
+      passport.authenticate('google', { scope: ['profile email'] })
+    );
 
-  // Add routes for oauth flow
-  router.get(
-    '/auth/google',
-    passport.authenticate('google', { scope: ['profile email'] })
-  );
+    router.get(
+      '/auth/google/callback',
+      passport.authenticate('google', {
+        successRedirect: baseUrl + '/',
+        failureRedirect: baseUrl + '/signin'
+      })
+    );
+  }
 
-  router.get(
-    '/auth/google/callback',
-    passport.authenticate('google', {
-      successRedirect: baseUrl + '/',
-      failureRedirect: baseUrl + '/signin'
-    })
-  );
+  return router;
 }
 
-module.exports = router;
+module.exports = makeGoogleAuth;
diff --git a/server/routes/queries.js b/server/routes/queries.js
index 3e3f0a69..e2af1cba 100644
--- a/server/routes/queries.js
+++ b/server/routes/queries.js
@@ -3,7 +3,6 @@ const queriesUtil = require('../models/queries.js');
 const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
 const mustBeAuthenticatedOrChartLink = require('../middleware/must-be-authenticated-or-chart-link-noauth.js');
 const sendError = require('../lib/sendError');
-const config = require('../lib/config');
 const pushQueryToSlack = require('../lib/pushQueryToSlack');
 
 // NOTE: this non-api route is special since it redirects legacy urls
@@ -12,12 +11,13 @@ router.get('/queries/:_id', mustBeAuthenticatedOrChartLink, function(
   res,
   next
 ) {
-  const { query, params } = req;
+  const { query, params, config } = req;
   const { format } = query;
+  const baseUrl = config.get('baseUrl');
   if (format === 'table') {
-    return res.redirect(config.get('baseUrl') + '/query-table/' + params._id);
+    return res.redirect(`${baseUrl}/query-table/${params._id}`);
   } else if (format === 'chart') {
-    return res.redirect(config.get('baseUrl') + '/query-chart/' + params._id);
+    return res.redirect(`${baseUrl}/query-chart/${params._id}`);
   }
   next();
 });
@@ -77,7 +77,7 @@ router.post('/api/queries', mustBeAuthenticated, async function(req, res) {
   try {
     const newQuery = await queriesUtil.save(query);
     // This is async, but save operation doesn't care about when/if finished
-    pushQueryToSlack(newQuery);
+    pushQueryToSlack(req.config, newQuery);
     return res.json({
       query: newQuery
     });
diff --git a/server/routes/query-history.js b/server/routes/query-history.js
index bb8cef86..7c3f5dd2 100644
--- a/server/routes/query-history.js
+++ b/server/routes/query-history.js
@@ -5,7 +5,6 @@ const queryHistoryUtil = require('../models/queryHistory.js');
 const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
 const urlFilterToNeDbFilter = require('../lib/urlFilterToNeDbFilter');
 const sendError = require('../lib/sendError');
-const config = require('../lib/config');
 
 router.get('/api/query-history', mustBeAuthenticated, async function(req, res) {
   try {
@@ -33,7 +32,7 @@ router.get('/api/query-history', mustBeAuthenticated, async function(req, res) {
     });
 
     queryHistory.incomplete =
-      dbQueryHistory.length >= config.get('queryHistoryResultMaxRows');
+      dbQueryHistory.length >= req.config.get('queryHistoryResultMaxRows');
     queryHistory.rows = dbQueryHistory;
     queryHistory.stopTime = new Date();
     queryHistory.queryRunTime =
diff --git a/server/routes/query-result.js b/server/routes/query-result.js
index 0362befb..3a17a51b 100644
--- a/server/routes/query-result.js
+++ b/server/routes/query-result.js
@@ -7,7 +7,6 @@ const queryHistory = require('../models/queryHistory');
 const mustHaveConnectionAccess = require('../middleware/must-have-connection-access.js');
 const mustHaveConnectionAccessOrChartLink = require('../middleware/must-have-connection-access-or-chart-link-noauth');
 const sendError = require('../lib/sendError');
-const config = require('../lib/config');
 
 // This allows executing a query relying on the saved query text
 // Instead of relying on an open endpoint that executes arbitrary sql
@@ -30,7 +29,7 @@ router.get(
       };
       // IMPORTANT: Send actual error here since it might have info on why the query is bad
       try {
-        const queryResult = await getQueryResult(data);
+        const queryResult = await getQueryResult(req, data);
         return res.send({ queryResult });
       } catch (error) {
         sendError(res, error);
@@ -57,26 +56,26 @@ router.post('/api/query-result', mustHaveConnectionAccess, async function(
   };
 
   try {
-    const queryResult = await getQueryResult(data);
+    const queryResult = await getQueryResult(req, data);
     return res.send({ queryResult });
   } catch (error) {
     sendError(res, error);
   }
 });
 
-async function getQueryResult(data) {
+async function getQueryResult(req, data) {
   const { connectionId, cacheKey, queryId, queryName, queryText, user } = data;
   const connection = await connections.findOneById(connectionId);
 
   if (!connection) {
     throw new Error('Please choose a connection');
   }
-  connection.maxRows = Number(config.get('queryResultMaxRows'));
+  connection.maxRows = Number(req.config.get('queryResultMaxRows'));
 
   const queryResult = await runQuery(queryText, connection, user);
   queryResult.cacheKey = cacheKey;
 
-  if (config.get('queryHistoryRetentionTimeInDays') > 0) {
+  if (req.config.get('queryHistoryRetentionTimeInDays') > 0) {
     await queryHistory.removeOldEntries();
     await queryHistory.save({
       userId: user._id,
@@ -94,7 +93,7 @@ async function getQueryResult(data) {
     });
   }
 
-  if (config.get('allowCsvDownload')) {
+  if (req.config.get('allowCsvDownload')) {
     resultCache.saveResultCache(cacheKey, queryName);
     await resultCache.writeXlsx(cacheKey, queryResult);
     await resultCache.writeCsv(cacheKey, queryResult);
diff --git a/server/routes/saml.js b/server/routes/saml.js
index b1e9208c..c23b60ec 100644
--- a/server/routes/saml.js
+++ b/server/routes/saml.js
@@ -1,73 +1,86 @@
 const passport = require('passport');
 const SamlStrategy = require('passport-saml').Strategy;
 const router = require('express').Router();
-const config = require('../lib/config');
 const logger = require('../lib/logger');
 const usersUtil = require('../models/users.js');
 
-const samlEntryPoint = config.get('samlEntryPoint');
-const samlIssuer = config.get('samlIssuer');
-const samlCallbackUrl = config.get('samlCallbackUrl');
-const samlCert = config.get('samlCert');
-const samlAuthContext = config.get('samlAuthContext');
+/**
+ * Adds passport SAML strategy and SAML auth routes if SAML is configured
+ * @param {object} config
+ */
+function makeSamlAuth(config) {
+  const samlEntryPoint = config.get('samlEntryPoint');
+  const samlIssuer = config.get('samlIssuer');
+  const samlCallbackUrl = config.get('samlCallbackUrl');
+  const samlCert = config.get('samlCert');
+  const samlAuthContext = config.get('samlAuthContext');
 
-if (
-  samlEntryPoint &&
-  samlIssuer &&
-  samlCallbackUrl &&
-  samlCert &&
-  samlAuthContext
-) {
-  logger.info('Enabling SAML authentication strategy.');
+  if (
+    samlEntryPoint &&
+    samlIssuer &&
+    samlCallbackUrl &&
+    samlCert &&
+    samlAuthContext
+  ) {
+    logger.info('Enabling SAML authentication strategy.');
 
-  // Register SAML strategy
-  passport.use(
-    new SamlStrategy(
-      {
-        path: '/login/callback',
-        entryPoint: samlEntryPoint,
-        issuer: samlIssuer,
-        callbackUrl: samlCallbackUrl,
-        cert: samlCert,
-        authnContext: samlAuthContext,
-        identifierFormat: null
-      },
-      async function(p, done) {
-        const email =
-          p[
-            'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
-          ];
-        const user = await usersUtil.findOneByEmail(email);
-        logger.info('User logged in via SAML %s', email);
-        if (!user) {
-          return done(null, false);
+    // Register SAML strategy
+    passport.use(
+      new SamlStrategy(
+        {
+          path: '/login/callback',
+          entryPoint: samlEntryPoint,
+          issuer: samlIssuer,
+          callbackUrl: samlCallbackUrl,
+          cert: samlCert,
+          authnContext: samlAuthContext,
+          identifierFormat: null
+        },
+        async function(p, done) {
+          const email =
+            p[
+              'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'
+            ];
+          const user = await usersUtil.findOneByEmail(email);
+          logger.info('User logged in via SAML %s', email);
+          if (!user) {
+            return done(null, false);
+          }
+          return done(null, {
+            id: user._id,
+            _id: user._id,
+            role: user.role,
+            email: user.email
+          });
         }
-        return done(null, {
-          id: user._id,
-          _id: user._id,
-          role: user.role,
-          email: user.email
-        });
+      )
+    );
+
+    // Add SAML auth routes
+    router.post(
+      '/login/callback',
+      passport.authenticate('saml', {
+        failureRedirect: '/',
+        failureFlash: true
+      }),
+      function(req, res) {
+        res.redirect('/');
       }
-    )
-  );
+    );
 
-  // Add SAML auth routes
-  router.post(
-    '/login/callback',
-    passport.authenticate('saml', { failureRedirect: '/', failureFlash: true }),
-    function(req, res) {
-      res.redirect('/');
-    }
-  );
+    router.get(
+      '/auth/saml',
+      passport.authenticate('saml', {
+        failureRedirect: '/',
+        failureFlash: true
+      }),
+      function(req, res) {
+        res.redirect('/');
+      }
+    );
+  }
 
-  router.get(
-    '/auth/saml',
-    passport.authenticate('saml', { failureRedirect: '/', failureFlash: true }),
-    function(req, res) {
-      res.redirect('/');
-    }
-  );
+  return router;
 }
 
-module.exports = router;
+module.exports = makeSamlAuth;
diff --git a/server/routes/signup-signin-signout.js b/server/routes/signup-signin-signout.js
index 5bc8df3b..9329a2f7 100644
--- a/server/routes/signup-signin-signout.js
+++ b/server/routes/signup-signin-signout.js
@@ -5,13 +5,12 @@ const router = require('express').Router();
 const checkWhitelist = require('../lib/check-whitelist');
 const usersUtil = require('../models/users.js');
 const sendError = require('../lib/sendError');
-const config = require('../lib/config');
 const logger = require('../lib/logger');
 const passhash = require('../lib/passhash.js');
 
 async function handleSignup(req, res, next) {
   try {
-    const whitelistedDomains = config.get('whitelistedDomains');
+    const whitelistedDomains = req.config.get('whitelistedDomains');
 
     if (req.body.password !== req.body.passwordConfirmation) {
       return sendError(res, null, 'Passwords do not match');
@@ -58,87 +57,95 @@ function sendSuccess(req, res) {
   res.json({});
 }
 
-/*  Some routes should only exist if userpath auth is enabled
-============================================================================= */
-if (!config.get('disableUserpassAuth')) {
-  // Register local auth strategy
-  // TODO: Should all authentication strategies be disabled by default, requiring SQLPad implementer to pick one?
-  // This might be more secure as it'd prevent SQLPad's from launching with local auth enabled and open admin registration by default
-  logger.info('Enabling local authentication strategy.');
-  passport.use(
-    new PassportLocalStrategy(
-      {
-        usernameField: 'email'
-      },
-      async function passportLocalStrategyHandler(email, password, done) {
+/**
+ * Adds local and basic auth strategy and routes if not disabled
+ * @param {object} config
+ */
+function makeLocalAuth(config) {
+  if (!config.get('disableUserpassAuth')) {
+    // Register local auth strategy
+    // TODO: Should all authentication strategies be disabled by default, requiring SQLPad implementer to pick one?
+    // This might be more secure as it'd prevent SQLPad's from launching with local auth enabled and open admin registration by default
+    logger.info('Enabling local authentication strategy.');
+    passport.use(
+      new PassportLocalStrategy(
+        {
+          usernameField: 'email'
+        },
+        async function passportLocalStrategyHandler(email, password, done) {
+          try {
+            const user = await usersUtil.findOneByEmail(email);
+            if (!user) {
+              return done(null, false, { message: 'wrong email or password' });
+            }
+            const isMatch = await passhash.comparePassword(
+              password,
+              user.passhash
+            );
+            if (isMatch) {
+              return done(null, {
+                id: user._id,
+                _id: user._id,
+                role: user.role,
+                email: user.email
+              });
+            }
+            return done(null, false, { message: 'wrong email or password' });
+          } catch (error) {
+            done(error);
+          }
+        }
+      )
+    );
+
+    // TODO - basic auth should perhaps be something opted into as a separate config
+    logger.info('Enabling basic authentication strategy.');
+    passport.use(
+      new BasicStrategy(async function(username, password, callback) {
         try {
-          const user = await usersUtil.findOneByEmail(email);
+          const user = await usersUtil.findOneByEmail(username);
           if (!user) {
-            return done(null, false, { message: 'wrong email or password' });
+            return callback(null, false);
           }
           const isMatch = await passhash.comparePassword(
             password,
             user.passhash
           );
-          if (isMatch) {
-            return done(null, {
-              id: user._id,
-              _id: user._id,
-              role: user.role,
-              email: user.email
-            });
+          if (!isMatch) {
+            return callback(null, false);
           }
-          return done(null, false, { message: 'wrong email or password' });
+          return callback(null, user);
         } catch (error) {
-          done(error);
-        }
-      }
-    )
-  );
-
-  // TODO - basic auth should perhaps be something opted into as a separate config
-  logger.info('Enabling basic authentication strategy.');
-  passport.use(
-    new BasicStrategy(async function(username, password, callback) {
-      try {
-        const user = await usersUtil.findOneByEmail(username);
-        if (!user) {
-          return callback(null, false);
+          callback(error);
         }
-        const isMatch = await passhash.comparePassword(password, user.passhash);
-        if (!isMatch) {
-          return callback(null, false);
-        }
-        return callback(null, user);
-      } catch (error) {
-        callback(error);
-      }
-    })
-  );
+      })
+    );
 
-  // Add routes for local auth signup & signin
-  router.post(
-    '/api/signup',
-    handleSignup,
-    passport.authenticate('local'),
-    sendSuccess
-  );
-
-  router.post('/api/signin', passport.authenticate('local'), sendSuccess);
-}
+    // Add routes for local auth signup & signin
+    router.post(
+      '/api/signup',
+      handleSignup,
+      passport.authenticate('local'),
+      sendSuccess
+    );
 
-/*  These auth routes should always exist regardless of strategy
-============================================================================= */
-router.get('/api/signout', function(req, res) {
-  if (!req.session) {
-    return res.json({});
+    router.post('/api/signin', passport.authenticate('local'), sendSuccess);
   }
-  req.session.destroy(function(err) {
-    if (err) {
-      logger.error(err);
+
+  // Sign out route should always exist
+  router.get('/api/signout', function(req, res) {
+    if (!req.session) {
+      return res.json({});
     }
-    res.json({});
+    req.session.destroy(function(err) {
+      if (err) {
+        logger.error(err);
+      }
+      res.json({});
+    });
   });
-});
 
-module.exports = router;
+  return router;
+}
+
+module.exports = makeLocalAuth;
diff --git a/server/routes/users.js b/server/routes/users.js
index d117604b..7fbd2b8a 100644
--- a/server/routes/users.js
+++ b/server/routes/users.js
@@ -1,10 +1,9 @@
 const router = require('express').Router();
 const usersUtil = require('../models/users.js');
-const email = require('../lib/email');
+const makeEmail = require('../lib/email');
 const mustBeAdmin = require('../middleware/must-be-admin.js');
 const mustBeAuthenticated = require('../middleware/must-be-authenticated.js');
 const sendError = require('../lib/sendError');
-const config = require('../lib/config');
 const logger = require('../lib/logger');
 
 router.get('/api/users', mustBeAuthenticated, async function(req, res) {
@@ -28,7 +27,9 @@ router.post('/api/users', mustBeAdmin, async function(req, res) {
       role: req.body.role
     });
 
-    if (config.smtpConfigured()) {
+    const email = makeEmail(req.config);
+
+    if (req.config.smtpConfigured()) {
       email.sendInvite(req.body.email).catch(error => logger.error(error));
     }
     return res.json({ user });
diff --git a/server/server.js b/server/server.js
index 1a01825c..f4296d7f 100755
--- a/server/server.js
+++ b/server/server.js
@@ -18,7 +18,7 @@ if (configValidations.errors.length > 0) {
   process.exit(1);
 }
 
-const app = require('./app');
+const app = require('./app')(config);
 
 const baseUrl = config.get('baseUrl');
 const ip = config.get('ip');


Test patch:
diff --git a/server/test/lib/email.js b/server/test/lib/email.js
index b82b1871..504feb09 100644
--- a/server/test/lib/email.js
+++ b/server/test/lib/email.js
@@ -1,8 +1,10 @@
-const email = require('../../lib/email.js');
+const makeEmail = require('../../lib/email.js');
 const config = require('../../lib/config');
 
 describe('lib/email.js', async function() {
   if (config.smtpConfigured() && process.env.SQLPAD_TEST_EMAIL) {
+    const email = makeEmail(config);
+
     it('should send invites', function() {
       return email.sendInvite(process.env.SQLPAD_TEST_EMAIL);
     });
diff --git a/server/test/utils.js b/server/test/utils.js
index ef48c7c9..b45b3c63 100644
--- a/server/test/utils.js
+++ b/server/test/utils.js
@@ -3,7 +3,8 @@ const request = require('supertest');
 const usersUtil = require('../models/users');
 const consts = require('../lib/consts');
 const db = require('../lib/db');
-const app = require('../app');
+const config = require('../lib/config');
+const app = require('../app')(config);
 
 const users = {
   admin: {