UNPKG

expresser

Version:

A ready to use Node.js web app wrapper, built on top of Express.

302 lines (246 loc) 11.8 kB
# EXPRESSER SETTINGS # ----------------------------------------------------------------------------- crypto = require "crypto" errors = require "./errors.coffee" events = require "./events.coffee" fs = require "fs" lodash = require "lodash" path = require "path" utils = require "./utils.coffee" # Current environment env = null ### # All main settings for an Expresser apps are set and described on the # settings.default.json file. Do not edit it!!! To change settings please # create a settings.json on the root of your application and put your # own 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 loaded # after the main `settings.json` file. # # @example Sample settings.json file # { # "general": { # "debug": true # }, # "app" { # "title": "My Super App" # } # } ### class Settings newInstance: -> obj = new Settings() obj.load() return obj ## # List of loaded settings files. # @property # @type Array files: [] # MAIN METHODS # -------------------------------------------------------------------------- ### # Load settings from `settings.default.json`, then `settings.json`, then # environment specific settings. ### load: => @loadFromJson "settings.default.json" @loadFromJson "settings.json" @loadFromJson "settings.#{env.toString().toLowerCase()}.json" events.emit "Settings.on.load" ### # Load settings from the specified JSON file. # @param {String} filename The filename or full path to the settings file, mandatory. # @param {Boolean} extend If true it won't update settings that are already existing, default is false. # @return {Object} Returns the JSON representation of the loaded file, or null if error / empty. ### loadFromJson: (filename, extend = false) => env = process.env.NODE_ENV or "development" filename = utils.io.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.data.minifyJson settingsJson catch ex settingsJson = fs.readFileSync filename, encAscii settingsJson = utils.data.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 # Add file to the `files` list. @files.push {filename: filename, watching: false} if settingsJson? if @general.debug and @logger.console and env isnt "test" console.log "Settings.loadFromJson", filename events.emit "Settings.on.loadFromJson", filename # Return the JSON representation of the file (or null if not found / empty). return settingsJson ### # Reset to default settings by clearing values and listeners, and re-calling `load`. ### reset: => @unwatch() @files = [] @instance = new Settings() @instance.load() # ENCRYPTION # -------------------------------------------------------------------------- # Helper to encrypt or decrypt settings files. The default encryption key # defined on the `Settings.coffee` file is "ExpresserSettings", which # you should change to your desired value. You can also set the key # via the EXPRESSER_SETTINGS_CRYPTOKEY environment variable. # 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 encryption key. cryptoHelper: (encrypt, filename, options) -> env = process.env options = {} if not options? options = lodash.defaults options, {cipher: "aes256", key: env["EXPRESSER_SETTINGS_CRYPTOKEY"]} options.key = "ExpresserSettings" if not options.key? or options.key is "" 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.key 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.key 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 # -------------------------------------------------------------------------- ### # Watch loaded settings files for changes by using a file watcher. # The `callback` is optional in case you want to notify another module about settings updates. # @param {Function} callback Optional function (event, filename) triggered when a settings file gets updated. ### watch: (callback) => env = process.env.NODE_ENV or "development" if lodash.isBoolean callback console.warn "Settings.watch(boolean)", "DEPRECATED! Please use watch(callback) and unwatch(callback)." if callback? and not lodash.isFunction callback return errors.throw "callbackMustBeFunction", "Pass null if you don't need a callback for the file watchers." logToConsole = @general.debug and @logger.console # Iterate loaded files to create the file system watchers. for f in @files do (f) => filename = utils.io.getFilePath f.filename if filename? and not f.watching fs.watchFile filename, {persistent: true}, (evt, filename) => @loadFromJson filename console.log "Settings.watch", f, "Reloaded!" if env isnt "test" callback? evt, filename f.watching = true if @general.debug and env isnt "test" console.log "Settings.watch", (if callback? then "With callback" else "No callback") ### # Unwatch changes on loaded settings files. # The `callback` is optional in case you want to stop notifying only for that function. # @param {Function} callback Optional function to be removed from the file watchers. ### unwatch: (callback) => env = process.env.NODE_ENV or "development" try for f in @files filename = utils.io.getFilePath f.filename if filename? if callback? fs.unwatchFile filename, callback else fs.unwatchFile filename f.watching = false catch ex console.error "Settings.unwatch", ex if @general.debug and env isnt "test" console.log "Settings.unwatch", (if callback? then "With callback" else "No callback") # Singleton implementation # ----------------------------------------------------------------------------- Settings.getInstance = -> if not @instance? @instance = new Settings() @instance.load() return @instance module.exports = Settings.getInstance()