expresser
Version:
A ready to use Node.js web app wrapper, built on top of Express.
340 lines (279 loc) • 14.8 kB
text/coffeescript
# EXPRESSER SETTINGS
# -----------------------------------------------------------------------------
# All main settings for the Expresser platform are set and described on the
# settings.default.json file. Do not edit it!!! To change settings please
# create a settings.json file and put your values there.
#
# You can also create specific settings for different runtime environments.
# For example to set settings on development, create `settings.development.json`
# and for production a `settings.production.json` file. These will be parsed
# AFTER the main `settings.json` file.
#
# Please note that the `settings.json` must ne located on the root of your app!
# <!--
# @example Sample settings.json file
# {
# "general": {
# "debug": true,
# "appTitle": "A Super Cool App"
# },
# "firewall" {
# "enabled": false
# }
# }
# -->
class Settings
crypto = require "crypto"
fs = require "fs"
lodash = require "lodash"
path = require "path"
utils = require "./utils.coffee"
currentEnv: process.env.NODE_ENV
# MAIN METHODS
# --------------------------------------------------------------------------
# Load settings from settings.default.json, then settings.json, then environment specific settings.
load: =>
@currentEnv = process.env.NODE_ENV or "development"
@loadFromJson "settings.default.json"
@loadFromJson "settings.json"
@loadFromJson "settings.#{@currentEnv.toString().toLowerCase()}.json"
# Helper to load values from the specified settings file.
# @param [String] filename The filename or path to the settings file.
# @param [Boolean] extend If true it won't update settings that are already existing.
# @return [Object] Returns the JSON representation of the loaded file.
loadFromJson: (filename, extend) =>
extend = false if not extend?
filename = utils.getFilePath filename
settingsJson = null
# Has json? Load it. Try using UTF8 first, if failed, use ASCII.
if filename?
encUtf8 = {encoding: "utf8"}
encAscii = {encoding: "ascii"}
# Try parsing the file with UTF8 first, if fails, try ASCII.
try
settingsJson = fs.readFileSync filename, encUtf8
settingsJson = utils.minifyJson settingsJson
catch ex
settingsJson = fs.readFileSync filename, encAscii
settingsJson = utils.minifyJson settingsJson
# Helper function to overwrite properties.
xtend = (source, target) ->
for prop, value of source
if value?.constructor is Object
target[prop] = {} if not target[prop]?
xtend source[prop], target[prop]
else if not extend or not target[prop]?
target[prop] = source[prop]
xtend settingsJson, this
if @general.debug and @logger.console
console.log "Settings.loadFromJson", filename
# Return the JSON representation of the file (or null if not found / empty).
return settingsJson
# Reset to default settings.
reset: =>
@instance = new Settings()
@instance.load()
# ENCRYPTION
# --------------------------------------------------------------------------
# Helper to encrypt or decrypt settings files. The default encryption password
# defined on the `Settings.coffee` file is "ExpresserSettings", which ideally you
# should change. The default cipher algorithm is AES 256.
# @param [Boolean] encrypt Pass true to encrypt, false to decrypt.
# @param [String] filename The file to be encrypted or decrypted.
# @param [Object] options Options to be passed to the cipher.
# @option options [String] cipher The cipher to be used, default is aes256.
# @option options [String] password The default encryption password.
cryptoHelper: (encrypt, filename, options) =>
options = {} if not options?
options = lodash.defaults options, {cipher: "aes256", password: "ExpresserSettings"}
settingsJson = @loadFromJson filename, false
# Settings file not found or invalid? Stop here.
if not settingsJson? and @logger.console
console.warn "Settings.cryptoHelper", encrypt, filename, "File not found or invalid, abort!"
return false
# If trying to encrypt and settings property `encrypted` is true,
# abort encryption and log to the console.
if settingsJson.encrypted is true and encrypt
if @logger.console
console.warn "Settings.cryptoHelper", encrypt, filename, "Property 'encrypted' is true, abort!"
return false
# Helper to parse and encrypt / decrypt settings data.
parser = (obj) =>
currentValue = null
for prop, value of obj
if value?.constructor is Object
parser obj[prop]
else
try
currentValue = obj[prop]
if encrypt
# Check the property data type and prefix the new value.
if lodash.isBoolean currentValue
newValue = "bool:"
else if lodash.isNumber currentValue
newValue = "number:"
else
newValue = "string:"
# Create cipher amd encrypt data.
c = crypto.createCipher options.cipher, options.password
newValue += c.update currentValue.toString(), @general.encoding, "hex"
newValue += c.final "hex"
else
# Split the data as "datatype:encryptedValue".
arrValue = currentValue.split ":"
newValue = ""
# Create cipher and decrypt.
c = crypto.createDecipher options.cipher, options.password
newValue += c.update arrValue[1], "hex", @general.encoding
newValue += c.final @general.encoding
# Cast data type (boolean, number or string).
if arrValue[0] is "bool"
if newValue is "true" or newValue is "1"
newValue = true
else
newValue = false
else if arrValue[0] is "number"
newValue = parseFloat newValue
catch ex
if @logger.console
console.error "Settings.cryptoHelper", encrypt, filename, ex, currentValue
# Update settings property value.
obj[prop] = newValue
# Remove `encrypted` property prior to decrypting.
if not encrypt
delete settingsJson["encrypted"]
# Process settings data.
parser settingsJson
# Add `encrypted` property after file is encrypted.
if encrypt
settingsJson.encrypted = true
# Stringify and save the new settings file.
newSettingsJson = JSON.stringify settingsJson, null, 4
fs.writeFileSync filename, newSettingsJson, {encoding: @general.encoding}
return true
# Helper to encrypt the specified settings file. Please see `cryptoHelper` above.
# @param [String] filename The file to be encrypted.
# @param [Object] options Options to be passed to the cipher.
# @return [Boolean] Returns true if encryption OK, false if something went wrong.
encrypt: (filename, options) =>
@cryptoHelper true, filename, options
# Helper to decrypt the specified settings file. Please see `cryptoHelper` above.
# @param [String] filename The file to be decrypted.
# @param [Object] options Options to be passed to the cipher.
# @return [Boolean] Returns true if decryption OK, false if something went wrong.
decrypt: (filename, options) =>
@cryptoHelper false, filename, options
# FILE WATCHER
# --------------------------------------------------------------------------
# Enable or disable the settings files watcher to auto reload settings when file changes.
# The `callback` is optional in case you want to notify another module about settings updates.
# @param [Boolean] enable If enabled is true activate the fs watcher, otherwise deactivate.
# @param [Method] callback A function (event, filename) triggered when a settings file changes.
watch: (enable, callback) =>
if callback? and not lodash.isFunction callback
throw new TypeError "The callback must be a valid function, or null/undefined."
logToConsole = @general.debug and @logger.console
# Add / remove watcher for the @json file if it exists.
filename = utils.getFilePath "settings.json"
if filename?
if enable
fs.watchFile filename, {persistent: true}, (evt, filename) =>
@loadFromJson filename
console.log "Settings.watch", filename, "Reloaded!" if logToConsole
callback(evt, filename) if callback?
else
fs.unwatchFile filename, callback
# Add / remove watcher for the settings.NODE_ENV.json file if it exists.
filename = utils.getFilePath "settings.#{@currentEnv.toString().toLowerCase()}.json"
if filename?
if enable
fs.watchFile filename, {persistent: true}, (evt, filename) =>
@loadFromJson filename
console.log "Settings.watch", filename, "Reloaded!" if logToConsole
callback(evt, filename) if callback?
else
fs.unwatchFile filename, callback
if logToConsole
console.log "Settings.watch", enable, (if callback? then "With callback" else "No callback")
# PAAS
# --------------------------------------------------------------------------
# Update settings based on Cloud Environmental variables. If a `filter` is specified,
# update only settings that match it, otherwise update everything.
# @param [String] filter Filter settings to be updated, for example "mailer" or "database".
updateFromPaaS: (filter) =>
env = process.env
filter = false if not filter? or filter is ""
# Update app IP and port (OpenShift, AppFog).
if not filter or filter.indexOf("app") >= 0
ip = env.OPENSHIFT_NODEJS_IP or env.IP
port = env.OPENSHIFT_NODEJS_PORT or env.VCAP_APP_PORT or env.PORT
@app.ip = ip if ip? and ip isnt ""
@app.port = port if port? and port isnt ""
# Update database settings (AppFog, MongoLab, MongoHQ).
if not filter or filter.indexOf("database") >= 0
vcap = env.VCAP_SERVICES
vcap = JSON.parse vcap if vcap?
# Check for AppFog MongoDB variables.
if vcap? and vcap isnt ""
mongo = vcap["mongodb-1.8"]
mongo = mongo[0]["credentials"] if mongo?
if mongo?
@database.connString = "mongodb://#{mongo.hostname}:#{mongo.port}/#{mongo.db}"
# Check for MongoLab variables.
mongoLab = env.MONGOLAB_URI
@database.connString = mongoLab if mongoLab? and mongoLab isnt ""
# Check for MongoHQ variables.
mongoHq = env.MONGOHQ_URL
@database.connString = mongoHq if mongoHq? and mongoHq isnt ""
# Update logger settings (Logentries and Loggly).
if not filter or filter.indexOf("logger") >= 0
logentriesToken = env.LOGENTRIES_TOKEN
logglyToken = env.LOGGLY_TOKEN
logglySubdomain = env.LOGGLY_SUBDOMAIN
@logger.logentries.token = logentriesToken if logentriesToken? and logentriesToken isnt ""
@logger.loggly.token = logglyToken if logglyToken? and logglyToken isnt ""
@logger.loggly.subdomain = logglySubdomain if logglySubdomain? and logglySubdomain isnt ""
# Update mailer settings (Mailgun, Mandrill, SendGrid).
if not filter or filter.indexOf("mail") >= 0
currentSmtpHost = @mailer.smtp.host?.toLowerCase()
currentSmtpHost = "" if not currentSmtpHost?
# Get and set Mailgun.
if currentSmtpHost.indexOf("mailgun") >= 0 or smtpHost?.indexOf("mailgun") >= 0
@mailer.smtp.service = "mailgun"
smtpUser = env.MAILGUN_SMTP_LOGIN
smtpPassword = env.MAILGUN_SMTP_PASSWORD
if smtpUser? and smtpUser isnt "" and smtpPassword? and smtpPassword isnt ""
@mailer.smtp.user = smtpUser
@mailer.smtp.password = smtpPassword
# Get and set Mandrill.
if currentSmtpHost.indexOf("mandrill") >= 0 or smtpHost?.indexOf("mandrill") >= 0
@mailer.smtp.service = "mandrill"
smtpUser = env.MANDRILL_USERNAME
smtpPassword = env.MANDRILL_APIKEY
if smtpUser? and smtpUser isnt "" and smtpPassword? and smtpPassword isnt ""
@mailer.smtp.user = smtpUser
@mailer.smtp.password = smtpPassword
# Get and set SendGrid.
if currentSmtpHost.indexOf("sendgrid") >= 0 or smtpHost?.indexOf("sendgrid") >= 0
@mailer.smtp.service = "sendgrid"
smtpUser = env.SENDGRID_USERNAME
smtpPassword = env.SENDGRID_PASSWORD
if smtpUser? and smtpUser isnt "" and smtpPassword? and smtpPassword isnt ""
@mailer.smtp.user = smtpUser
@mailer.smtp.password = smtpPassword
# Log to console.
if @general.debug and @logger.console
console.log "Settings.updateFromPaaS", "Updated!", filter
# Singleton implementation
# -----------------------------------------------------------------------------
Settings.getInstance = ->
if process.env is "test"
obj = new Settings()
obj.load()
obj.logger.console = false
return obj
if not @instance?
@instance = new Settings()
@instance.load()
return @instance
module.exports = exports = Settings.getInstance()