mbland/custom-links

View on GitHub
lib/config.js

Summary

Maintainability
A
0 mins
Test Coverage
'use strict'

var fs = require('fs')

module.exports = Config

function Config(configData) {
  var config = this,
      numericValues = {
        PORT: true,
        SESSION_MAX_AGE: true,
        REDIS_PORT: true,
        REDIS_RANGE_SIZE: true
      }


  applyEnvUpdates(configData)
  validateConfig(configData)
  setItemsToLowerCase(configData, ['users', 'domains'])

  Object.keys(configData).forEach(key => {
    var value = configData[key]
    config[key] = numericValues[key] ? parseInt(value) : value
  })
}

function setItemsToLowerCase(configData, properties) {
  properties.forEach(prop => {
    var items = configData[prop]

    if (items !== undefined) {
      configData[prop] = items.map(item => item.toLowerCase())
    }
  })
}

Config.fromFile = (configPath, logger) => {
  var errorPrefix

  try {
    logger.info('reading configuration from ' + configPath)
    return new Config(JSON.parse(fs.readFileSync(configPath, 'utf8')))

  } catch (err) {
    errorPrefix = 'failed to load configuration: '
    if (err instanceof SyntaxError) {
      errorPrefix += 'invalid JSON: '
    }
    err.message = errorPrefix + err.message
    throw err
  }
}

var schema = {
  requiredTopLevelFields: {
    PORT: 'port on which the server will listen for requests',
    AUTH_PROVIDERS: 'names of the authentication providers to use',
    SESSION_SECRET: 'secret key used to encrypt sessions'
  },
  optionalTopLevelFields: {
    SESSION_MAX_AGE: 'maximum age of a session, in seconds',
    REDIS_RANGE_SIZE: 'redis-server range scan result set size',
    REDIS_HOST: 'redis-server host',
    REDIS_PORT: 'redis-server port',
    users: 'list of authorized usernames (email addresses)',
    domains: 'list of authorized domains'
  },
  requiredProviderFields: {
    'test': {
    }
  }
}

var providers = [
  'google',
  'openidconnect'
]

providers.forEach(provider => {
  schema.requiredProviderFields[provider] = require('./auth/' + provider).config
})

function applyEnvUpdates(configData) {
  var envProperties = [
    'PORT',
    'AUTH_PROVIDERS',
    'SESSION_SECRET',
    'SESSION_MAX_AGE',
    'REDIS_HOST',
    'REDIS_PORT',
    'REDIS_RANGE_SIZE'
  ]

  envProperties.forEach(assignEnvVarToConfigProperty(configData))

  if (configData.AUTH_PROVIDERS === undefined) {
    return
  } else if (typeof configData.AUTH_PROVIDERS === 'string') {
    configData.AUTH_PROVIDERS = configData.AUTH_PROVIDERS.split(',')
  }
  requiredAuthProviderFields(configData.AUTH_PROVIDERS)
    .forEach(assignEnvVarToConfigProperty(configData))
}

function assignEnvVarToConfigProperty(configData) {
  return property => {
    var envVar = process.env['CUSTOM_LINKS_' + property]
    if (envVar !== undefined) {
      configData[property] = envVar
    }
  }
}

function requiredAuthProviderFields(authProviders) {
  var collectFields = (result, provider) => {
    var fields = Object.keys(schema.requiredProviderFields[provider] || {})
    return result.concat(fields)
  }
  return authProviders.reduce(collectFields, [])
}

function allAuthProviderFields() {
  return requiredAuthProviderFields(Object.keys(schema.requiredProviderFields))
}

function validateConfig(config) {
  var errors = [],
      errMsg,
      requiredFields,
      optionalFields,
      authProviders = config.AUTH_PROVIDERS || [],
      users = config.users || [],
      domains = config.domains || []

  requiredFields = Object.keys(schema.requiredTopLevelFields).concat(
    requiredAuthProviderFields(authProviders))
  optionalFields = Object.keys(schema.optionalTopLevelFields).concat(
    allAuthProviderFields())

  findMissingFields(requiredFields, config, errors)
  findUnknownFields(requiredFields, optionalFields, config, errors)

  authProviders.forEach(provider => {
    if (!schema.requiredProviderFields.hasOwnProperty(provider)) {
      errors.push('unknown auth provider ' + provider)
    }
  })

  if (users.length + domains.length === 0) {
    errors.push('at least one of "users" or "domains" must be specified')
  }

  if (errors.length !== 0) {
    errMsg = 'Invalid configuration:\n  ' + errors.join('\n  ')
    throw new Error(errMsg)
  }
}

function findMissingFields(required, target, errors) {
  return required.filter(field => !target.hasOwnProperty(field))
    .forEach(missing => errors.push('missing ' + missing))
}

function findUnknownFields(required, optional, target, errors) {
  var knownFields = {},
      collectFields = (result, field) => {
        result[field] = null
        return result
      }

  required.concat(optional)
    .reduce(collectFields, knownFields)

  return Object.keys(target)
    .filter(field => !knownFields.hasOwnProperty(field))
    .forEach(unknown => errors.push('unknown property ' + unknown))
}