From 766981ebbae9ef9e22713c9076ac528bb73fbcb1 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 28 Jan 2014 17:52:46 -0800 Subject: [PATCH] Add basic browser build --- dist/loopback.js | 27324 +++++++++++++++++++++++++++++++++++++++ lib/browser-express.js | 7 + lib/loopback.js | 32 +- package.json | 20 +- 4 files changed, 27370 insertions(+), 13 deletions(-) create mode 100644 dist/loopback.js create mode 100644 lib/browser-express.js diff --git a/dist/loopback.js b/dist/loopback.js new file mode 100644 index 00000000..8162f5f6 --- /dev/null +++ b/dist/loopback.js @@ -0,0 +1,27324 @@ +!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.loopback=e():"undefined"!=typeof global?global.loopback=e():"undefined"!=typeof self&&(self.loopback=e())}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o {name: 'pencil'} + * }); + * ``` + * + * @param {String} modelName The name of the model to define + * @options {Object} config The model's configuration + * @property {String} dataSource The `DataSource` to attach the model to + * @property {Object} [options] an object containing `Model` options + * @property {Object} [properties] object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language) + * @end + * @returns {ModelConstructor} the model class + */ + +app.model = function (Model, config) { + if(arguments.length === 1) { + assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor'); + assert(Model.pluralModelName, 'Model must have a "pluralModelName" property'); + this.remotes().exports[Model.pluralModelName] = Model; + this.models().push(Model); + Model.shared = true; + Model.app = this; + Model.emit('attached', this); + return; + } + var modelName = Model; + config = config || {}; + assert(typeof modelName === 'string', 'app.model(name, config) => "name" name must be a string'); + + Model = + this.models[modelName] = + this.models[classify(modelName)] = + this.models[camelize(modelName)] = modelFromConfig(modelName, config, this); + + if(config.public !== false) { + this.model(Model); + } + + return Model; +} + +/** + * Get the models exported by the app. Only models defined using `app.model()` + * will show up in this list. + * + * There are two ways how to access models. + * + * **1. A list of all models** + * + * Call `app.models()` to get a list of all models. + * + * ```js + * var models = app.models(); + * + * models.forEach(function (Model) { + * console.log(Model.modelName); // color + * }); + * ``` + * + * **2. By model name** + * + * `app.model` has properties for all defined models. + * + * In the following example the `Product` and `CustomerReceipt` models are + * accessed using the `models` object. + * + * ```js + * var loopback = require('loopback'); + * var app = loopback(); + * app.boot({ + * dataSources: { + * db: {connector: 'memory'} + * } + * }); + * + * app.model('product', {dataSource: 'db'}); + * app.model('customer-receipt', {dataSource: 'db'}); + * + * // available based on the given name + * var Product = app.models.Product; + * + * // also available as camelCase + * var product = app.models.product; + * + * // multi-word models are avaiable as pascal cased + * var CustomerReceipt = app.models.CustomerReceipt; + * + * // also available as camelCase + * var customerReceipt = app.models.customerReceipt; + * ``` + * + * @returns {Array} a list of model classes + */ + +app.models = function () { + return this._models || (this._models = []); +} + +/** + * Define a DataSource. + * + * @param {String} name The data source name + * @param {Object} config The data source config + */ + +app.dataSource = function (name, config) { + this.dataSources[name] = + this.dataSources[classify(name)] = + this.dataSources[camelize(name)] = dataSourcesFromConfig(config); +} + +/** + * Get all remote objects. + */ + +app.remoteObjects = function () { + var result = {}; + var models = this.models(); + + // add in models + models.forEach(function (ModelCtor) { + // only add shared models + if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') { + result[ModelCtor.pluralModelName] = ModelCtor; + } + }); + + return result; +} + +/** + * Get the apps set of remote objects. + */ + +app.remotes = function () { + return this._remotes || (this._remotes = RemoteObjects.create()); +} + +/** + * Enable swagger REST API documentation. + * + * > Note: This method is deprecated, use the extension + * [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead. + * + * **Options** + * + * - `basePath` The basepath for your API - eg. 'http://localhost:3000'. + * + * **Example** + * + * ```js + * // enable docs + * app.docs({basePath: 'http://localhost:3000'}); + * ``` + * + * Run your app then navigate to + * [the API explorer](http://petstore.swagger.wordnik.com/). + * Enter your API basepath to view your generated docs. + * + * @deprecated + */ + +app.docs = function (options) { + var remotes = this.remotes(); + swagger(remotes, options); +} + +/*! + * Get a handler of the specified type from the handler cache. + */ + +app.handler = function (type) { + var handlers = this._handlers || (this._handlers = {}); + if(handlers[type]) { + return handlers[type]; + } + + var remotes = this.remotes(); + var handler = this._handlers[type] = remotes.handler(type); + return handler; +} + +/** + * An object to store dataSource instances. + */ + +app.dataSources = app.datasources = {}; + +/** + * Enable app wide authentication. + */ + +app.enableAuth = function() { + var remotes = this.remotes(); + + remotes.before('**', function(ctx, next, method) { + var req = ctx.req; + var Model = method.ctor; + var modelInstance = ctx.instance; + var modelId = modelInstance && modelInstance.id || req.param('id'); + + if(Model.checkAccess) { + Model.checkAccess( + req.accessToken, + modelId, + method.name, + function(err, allowed) { + if(err) { + console.log(err); + next(err); + } else if(allowed) { + next(); + } else { + var e = new Error('Access Denied'); + e.statusCode = 401; + next(e); + } + } + ); + } else { + next(); + } + }); +} + +/** + * Initialize an application from an options object or a set of JSON and JavaScript files. + * + * **What happens during an app _boot_?** + * + * 1. **DataSources** are created from an `options.dataSources` object or `datasources.json` in the current directory + * 2. **Models** are created from an `options.models` object or `models.json` in the current directory + * 3. Any JavaScript files in the `./models` directory are loaded with `require()`. + * 4. Any JavaScript files in the `./boot` directory are loaded with `require()`. + * + * **Options** + * + * - `cwd` - _optional_ - the directory to use when loading JSON and JavaScript files + * - `models` - _optional_ - an object containing `Model` definitions + * - `dataSources` - _optional_ - an object containing `DataSource` definitions + * + * > **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple + * > files may result + * > in models being **undefined** due to race conditions. To avoid this when + * > using `app.boot()` + * > make sure all models are passed as part of the `models` definition. + * + * + * **Model Definitions** + * + * The following is an example of an object containing two `Model` definitions: "location" and "inventory". + * + * ```js + * { + * "dealership": { + * // a reference, by name, to a dataSource definition + * "dataSource": "my-db", + * // the options passed to Model.extend(name, properties, options) + * "options": { + * "relations": { + * "cars": { + * "type": "hasMany", + * "model": "Car", + * "foreignKey": "dealerId" + * } + * } + * }, + * // the properties passed to Model.extend(name, properties, options) + * "properties": { + * "id": {"id": true}, + * "name": "String", + * "zip": "Number", + * "address": "String" + * } + * }, + * "car": { + * "dataSource": "my-db" + * "properties": { + * "id": { + * "type": "String", + * "required": true, + * "id": true + * }, + * "make": { + * "type": "String", + * "required": true + * }, + * "model": { + * "type": "String", + * "required": true + * } + * } + * } + * } + * ``` + * + * **Model definition properties** + * + * - `dataSource` - **required** - a string containing the name of the data source definition to attach the `Model` to + * - `options` - _optional_ - an object containing `Model` options + * - `properties` _optional_ - an object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language) + * + * **DataSource definition properties** + * + * - `connector` - **required** - the name of the [connector](#working-with-data-sources-and-connectors) + * + * @header app.boot([options]) + * @throws {Error} If config is not valid + * @throws {Error} If boot fails + */ + +app.boot = function(options) { + options = options || {}; + + if(typeof options === 'string') { + options = {appRootDir: options}; + } + var app = this; + var appRootDir = options.appRootDir = options.appRootDir || process.cwd(); + var ctx = {}; + var appConfig = options.app; + var modelConfig = options.models; + var dataSourceConfig = options.dataSources; + + if(!appConfig) { + appConfig = tryReadConfig(appRootDir, 'app') || {}; + } + if(!modelConfig) { + modelConfig = tryReadConfig(appRootDir, 'models') || {}; + } + if(!dataSourceConfig) { + dataSourceConfig = tryReadConfig(appRootDir, 'datasources') || {}; + } + + assertIsValidConfig('app', appConfig); + assertIsValidConfig('model', modelConfig); + assertIsValidConfig('data source', dataSourceConfig); + + appConfig.host = + process.env.npm_config_host || + process.env.OPENSHIFT_SLS_IP || + process.env.OPENSHIFT_NODEJS_IP || + process.env.HOST || + appConfig.host || + process.env.npm_package_config_host || + app.get('host'); + + appConfig.port = + process.env.npm_config_port || + process.env.OPENSHIFT_SLS_PORT || + process.env.OPENSHIFT_NODEJS_PORT || + process.env.PORT || + appConfig.port || + process.env.npm_package_config_port || + app.get('port') || + 3000; + + appConfig.restApiRoot = + appConfig.restApiRoot || + app.get('restApiRoot') || + '/api'; + + if(appConfig.host !== undefined) { + assert(typeof appConfig.host === 'string', 'app.host must be a string'); + app.set('host', appConfig.host); + } + + if(appConfig.port !== undefined) { + var portType = typeof appConfig.port; + assert(portType === 'string' || portType === 'number', 'app.port must be a string or number'); + app.set('port', appConfig.port); + } + + assert(appConfig.restApiRoot !== undefined, 'app.restBasePath is required'); + assert(typeof appConfig.restApiRoot === 'string', 'app.restBasePath must be a string'); + assert(/^\//.test(appConfig.restApiRoot), 'app.restBasePath must start with "/"'); + app.set('restApiRoot', appConfig.restBasePath); + + for(var configKey in appConfig) { + var cur = app.get(configKey); + if(cur === undefined || cur === null) { + app.set(configKey, appConfig[configKey]); + } + } + + // instantiate data sources + forEachKeyedObject(dataSourceConfig, function(key, obj) { + app.dataSource(key, obj); + }); + + // instantiate models + forEachKeyedObject(modelConfig, function(key, obj) { + app.model(key, obj); + }); + + // try to attach models to dataSources by type + try { + require('./loopback').autoAttach(); + } catch(e) { + if(e.name === 'AssertionError') { + console.warn(e); + } else { + throw e; + } + } + + // disable token requirement for swagger, if available + var swagger = app.remotes().exports.swagger; + var requireTokenForSwagger = appConfig.swagger + && appConfig.swagger.requireToken; + if(swagger) { + swagger.requireToken = requireTokenForSwagger || false; + } + + // require directories + var requiredModels = requireDir(path.join(appRootDir, 'models')); + var requiredBootScripts = requireDir(path.join(appRootDir, 'boot')); +} + +function assertIsValidConfig(name, config) { + if(config) { + assert(typeof config === 'object', name + ' config must be a valid JSON object'); + } +} + +function forEachKeyedObject(obj, fn) { + if(typeof obj !== 'object') return; + + Object.keys(obj).forEach(function(key) { + fn(key, obj[key]); + }); +} + +function classify(str) { + return stringUtils.classify(str); +} + +function camelize(str) { + return stringUtils.camelize(str); +} + +function dataSourcesFromConfig(config) { + var connectorPath; + + assert(typeof config === 'object', + 'cannont create data source without config object'); + + if(typeof config.connector === 'string') { + connectorPath = path.join(__dirname, 'connectors', config.connector+'.js'); + + if(fs.existsSync(connectorPath)) { + config.connector = require(connectorPath); + } + } + + return require('./loopback').createDataSource(config); +} + +function modelFromConfig(name, config, app) { + var ModelCtor = require('./loopback').createModel(name, config.properties, config.options); + var dataSource = app.dataSources[config.dataSource]; + + assert(isDataSource(dataSource), name + ' is referencing a dataSource that does not exist: "'+ config.dataSource +'"'); + + ModelCtor.attachTo(dataSource); + return ModelCtor; +} + +function requireDir(dir, basenames) { + assert(dir, 'cannot require directory contents without directory name'); + + var requires = {}; + + if (arguments.length === 2) { + // if basenames argument is passed, explicitly include those files + basenames.forEach(function (basename) { + var filepath = Path.resolve(Path.join(dir, basename)); + requires[basename] = tryRequire(filepath); + }); + } else if (arguments.length === 1) { + // if basenames arguments isn't passed, require all javascript + // files (except for those prefixed with _) and all directories + + var files = tryReadDir(dir); + + // sort files in lowercase alpha for linux + files.sort(function (a,b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + + if (a < b) { + return -1; + } else if (b < a) { + return 1; + } else { + return 0; + } + }); + + files.forEach(function (filename) { + // ignore index.js and files prefixed with underscore + if ((filename === 'index.js') || (filename[0] === '_')) { return; } + + var filepath = path.resolve(path.join(dir, filename)); + var ext = path.extname(filename); + var stats = fs.statSync(filepath); + + // only require files supported by require.extensions (.txt .md etc.) + if (stats.isFile() && !(ext in require.extensions)) { return; } + + var basename = path.basename(filename, ext); + + requires[basename] = tryRequire(filepath); + }); + + } + + return requires; +}; + +function tryRequire(modulePath) { + try { + return require.apply(this, arguments); + } catch(e) { + console.error('failed to require "%s"', modulePath); + throw e; + } +} + +function tryReadDir() { + try { + return fs.readdirSync.apply(fs, arguments); + } catch(e) { + return []; + } +} + +function isModelCtor(obj) { + return typeof obj === 'function' && obj.modelName && obj.name === 'ModelCtor'; +} + +function isDataSource(obj) { + return obj instanceof DataSource; +} + +function tryReadConfig(cwd, fileName) { + try { + return require(path.join(cwd, fileName + '.json')); + } catch(e) { + if(e.code !== "MODULE_NOT_FOUND") { + throw e; + } + } +} + +/** + * Install all express middleware required by LoopBack. + * + * It is possible to inject your own middleware by listening on one of the + * following events: + * + * - `middleware:preprocessors` is emitted after all other + * request-preprocessing middleware was installed, but before any + * request-handling middleware is configured. + * + * Usage: + * ```js + * app.once('middleware:preprocessors', function() { + * app.use(loopback.limit('5.5mb')) + * }); + * ``` + * - `middleware:handlers` is emitted when it's time to add your custom + * request-handling middleware. Note that you should not install any + * express routes at this point (express routes are discussed later). + * + * Usage: + * ```js + * app.once('middleware:handlers', function() { + * app.use('/admin', adminExpressApp); + * app.use('/custom', function(req, res, next) { + * res.send(200, { url: req.url }); + * }); + * }); + * ``` + * - `middleware:error-loggers` is emitted at the end, before the loopback + * error handling middleware is installed. This is the point where you + * can install your own middleware to log errors. + * + * Notes: + * - The middleware function must take four parameters, otherwise it won't + * be called by express. + * + * - It should also call `next(err)` to let the loopback error handler convert + * the error to an HTTP error response. + * + * Usage: + * ```js + * var bunyan = require('bunyan'); + * var log = bunyan.createLogger({name: "myapp"}); + * app.once('middleware:error-loggers', function() { + * app.use(function(err, req, res, next) { + * log.error(err); + * next(err); + * }); + * }); + * ``` + * + * Express routes should be added after `installMiddleware` was called. + * This way the express router middleware is injected at the right place in the + * middleware chain. If you add an express route before calling this function, + * bad things will happen: Express will automatically add the router + * middleware and since we haven't added request-preprocessing middleware like + * cookie & body parser yet, your route handlers will receive raw unprocessed + * requests. + * + * This is the correct order in which to call `app` methods: + * ```js + * app.boot(__dirname); // optional + * + * app.installMiddleware(); + * + * // [register your express routes here] + * + * app.listen(); + * ``` + */ +app.installMiddleware = function() { + var loopback = require('../'); + + /* + * Request pre-processing + */ + this.use(loopback.favicon()); + // TODO(bajtos) refactor to app.get('loggerFormat') + var loggerFormat = this.get('env') === 'development' ? 'dev' : 'default'; + this.use(loopback.logger(loggerFormat)); + this.use(loopback.cookieParser(this.get('cookieSecret'))); + this.use(loopback.token({ model: this.models.accessToken })); + this.use(loopback.bodyParser()); + this.use(loopback.methodOverride()); + + // Allow the app to install custom preprocessing middleware + this.emit('middleware:preprocessors'); + + /* + * Request handling + */ + + // LoopBack REST transport + this.use(this.get('restApiRoot') || '/api', loopback.rest()); + + // Allow the app to install custom request handling middleware + this.emit('middleware:handlers'); + + // Let express routes handle requests that were not handled + // by any of the middleware registered above. + // This way LoopBack REST and API Explorer take precedence over + // express routes. + this.use(this.router); + + // The static file server should come after all other routes + // Every request that goes through the static middleware hits + // the file system to check if a file exists. + this.use(loopback.static(path.join(__dirname, 'public'))); + + // Requests that get this far won't be handled + // by any middleware. Convert them into a 404 error + // that will be handled later down the chain. + this.use(loopback.urlNotFound()); + + /* + * Error handling + */ + + // Allow the app to install custom error logging middleware + this.emit('middleware:error-handlers'); + + // The ultimate error handler. + this.use(loopback.errorHandler()); +}; + +/** + * Listen for connections and update the configured port. + * + * When there are no parameters or there is only one callback parameter, + * the server will listen on `app.get('host')` and `app.get('port')`. + * + * ```js + * // listen on host/port configured in app config + * app.listen(); + * ``` + * + * Otherwise all arguments are forwarded to `http.Server.listen`. + * + * ```js + * // listen on the specified port and all hosts, ignore app config + * app.listen(80); + * ``` + * + * The function also installs a `listening` callback that calls + * `app.set('port')` with the value returned by `server.address().port`. + * This way the port param contains always the real port number, even when + * listen was called with port number 0. + * + * @param {Function=} cb If specified, the callback will be added as a listener + * for the server's "listening" event. + * @returns {http.Server} A node `http.Server` with this application configured + * as the request handler. + */ +app.listen = function(cb) { + var self = this; + + var server = require('http').createServer(this); + + server.on('listening', function() { + self.set('port', this.address().port); + }); + + var useAppConfig = + arguments.length == 0 || + (arguments.length == 1 && typeof arguments[0] == 'function'); + + if (useAppConfig) { + server.listen(this.get('port'), this.get('host'), cb); + } else { + server.listen.apply(server, arguments); + } + + return server; +} + +},{"../":1,"./loopback":7,"__browserify_process":36,"assert":21,"fs":20,"http":30,"loopback-datasource-juggler":62,"path":40,"strong-remoting":86,"strong-remoting/ext/swagger":85,"underscore.string":95}],3:[function(require,module,exports){ +module.exports = browserExpress; + +function browserExpress() { + return {}; +} + +browserExpress.errorHandler = {}; + +},{}],4:[function(require,module,exports){ +/** + * Expose `Connector`. + */ + +module.exports = Connector; + +/** + * Module dependencies. + */ + +var EventEmitter = require('events').EventEmitter + , debug = require('debug')('connector') + , util = require('util') + , inherits = util.inherits + , assert = require('assert'); + +/** + * Create a new `Connector` with the given `options`. + * + * @param {Object} options + * @return {Connector} + */ + +function Connector(options) { + EventEmitter.apply(this, arguments); + this.options = options; + + debug('created with options', options); +} + +/** + * Inherit from `EventEmitter`. + */ + +inherits(Connector, EventEmitter); + +/*! + * Create an connector instance from a JugglingDB adapter. + */ + +Connector._createJDBAdapter = function (jdbModule) { + var fauxSchema = {}; + jdbModule.initialize(fauxSchema, function () { + // connected + }); +} + +/*! + * Add default crud operations from a JugglingDB adapter. + */ + +Connector.prototype._addCrudOperationsFromJDBAdapter = function (connector) { + +} +},{"assert":21,"debug":57,"events":29,"util":55}],5:[function(require,module,exports){ +var process=require("__browserify_process");/** + * Dependencies. + */ + +var mailer = require('nodemailer') + , assert = require('assert') + , debug = require('debug') + , STUB = 'STUB'; + +/** + * Export the MailConnector class. + */ + +module.exports = MailConnector; + +/** + * Create an instance of the connector with the given `settings`. + */ + +function MailConnector(settings) { + assert(typeof settings === 'object', 'cannot initiaze MailConnector without a settings object'); + var transports = settings.transports || []; + this.transportsIndex = {}; + this.transports = []; + + transports.forEach(this.setupTransport.bind(this)); +} + +MailConnector.initialize = function(dataSource, callback) { + dataSource.connector = new MailConnector(dataSource.settings); + callback(); +} + +MailConnector.prototype.DataAccessObject = Mailer; + + +/** + * Add a transport to the available transports. See https://github.com/andris9/Nodemailer#setting-up-a-transport-method. + * + * Example: + * + * Email.setupTransport({ + * type: 'SMTP', + * host: "smtp.gmail.com", // hostname + * secureConnection: true, // use SSL + * port: 465, // port for secure SMTP + * auth: { + * user: "gmail.user@gmail.com", + * pass: "userpass" + * } + * }); + * + */ + +MailConnector.prototype.setupTransport = function(setting) { + var connector = this; + connector.transports = connector.transports || []; + connector.transportsIndex = connector.transportsIndex || {}; + var transport = mailer.createTransport(setting.type, setting); + connector.transportsIndex[setting.type] = transport; + connector.transports.push(transport); +} + +function Mailer() { + +} + +/** + * Get a transport by name. + * + * @param {String} name + * @return {Transport} transport + */ + +MailConnector.prototype.transportForName = function(name) { + return this.transportsIndex[name]; +} + +/** + * Get the default transport. + * + * @return {Transport} transport + */ + +MailConnector.prototype.defaultTransport = function() { + return this.transports[0] || this.stubTransport; +} + +/** + * Send an email with the given `options`. + * + * Example Options: + * + * { + * from: "Fred Foo ✔ ", // sender address + * to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers + * subject: "Hello ✔", // Subject line + * text: "Hello world ✔", // plaintext body + * html: "Hello world ✔" // html body + * } + * + * See https://github.com/andris9/Nodemailer for other supported options. + * + * @param {Object} options + * @param {Function} callback Called after the e-mail is sent or the sending failed + */ + +Mailer.send = function (options, fn) { + var dataSource = this.dataSource; + var settings = dataSource && dataSource.settings; + var connector = dataSource.connector; + assert(connector, 'Cannot send mail without a connector!'); + + var transport = connector.transportForName(options.transport); + + if(!transport) { + transport = connector.defaultTransport(); + } + + if(debug.enabled || settings && settings.debug) { + console.log('Sending Mail:'); + if(options.transport) { + console.log('\t TRANSPORT:', options.transport); + } + console.log('\t TO:', options.to); + console.log('\t FROM:', options.from); + console.log('\t SUBJECT:', options.subject); + console.log('\t TEXT:', options.text); + console.log('\t HTML:', options.html); + } + + if(transport) { + assert(transport.sendMail, 'You must supply an Email.settings.transports containing a valid transport'); + transport.sendMail(options, fn); + } else { + console.warn('Warning: No email transport specified for sending email.' + + ' Setup a transport to send mail messages.'); + process.nextTick(function() { + fn(null, options); + }); + } +} + +/** + * Send an email instance using `modelInstance.send()`. + */ + +Mailer.prototype.send = function (fn) { + this.constructor.send(this, fn); +} + +/** + * Access the node mailer object. + */ + +MailConnector.mailer = +MailConnector.prototype.mailer = +Mailer.mailer = +Mailer.prototype.mailer = mailer; + +},{"__browserify_process":36,"assert":21,"debug":57,"nodemailer":20}],6:[function(require,module,exports){ +/** + * Expose `Memory`. + */ + +module.exports = Memory; + +/** + * Module dependencies. + */ + +var Connector = require('./base-connector') + , debug = require('debug')('memory') + , util = require('util') + , inherits = util.inherits + , assert = require('assert') + , JdbMemory = require('loopback-datasource-juggler/lib/connectors/memory'); + +/** + * Create a new `Memory` connector with the given `options`. + * + * @param {Object} options + * @return {Memory} + */ + +function Memory() { + // TODO implement entire memory connector +} + +/** + * Inherit from `DBConnector`. + */ + +inherits(Memory, Connector); + +/** + * JugglingDB Compatibility + */ + +Memory.initialize = JdbMemory.initialize; +},{"./base-connector":4,"assert":21,"debug":57,"loopback-datasource-juggler/lib/connectors/memory":64,"util":55}],7:[function(require,module,exports){ +var __dirname="/lib";/*! + * Module dependencies. + */ + +var express = require('express') + , fs = require('fs') + , ejs = require('ejs') + , EventEmitter = require('events').EventEmitter + , path = require('path') + , proto = require('./application') + , DataSource = require('loopback-datasource-juggler').DataSource + , ModelBuilder = require('loopback-datasource-juggler').ModelBuilder + , assert = require('assert') + , i8n = require('inflection'); + +/** + * `loopback` is the main entry for LoopBack core module. It provides static + * methods to create models and data sources. The module itself is a function + * that creates loopback `app`. For example, + * + * + * ```js + * var loopback = require('loopback'); + * var app = loopback(); + * ``` + */ + +var loopback = exports = module.exports = createApplication; + +/** + * Framework version. + */ + +loopback.version = require('../package.json').version; + +/** + * Expose mime. + */ + +loopback.mime = express.mime; + +/** + * Create an loopback application. + * + * @return {Function} + * @api public + */ + +function createApplication() { + var app = express(); + + merge(app, proto); + + return app; +} + +/*! + * Expose express.middleware as loopback.* + * for example `loopback.errorHandler` etc. + */ + +for (var key in express) { + Object.defineProperty( + loopback + , key + , Object.getOwnPropertyDescriptor(express, key)); +} + +/*! + * Expose additional loopback middleware + * for example `loopback.configure` etc. + * + * ***only in node*** + */ + +if (typeof window === 'undefined') { + fs + .readdirSync(path.join(__dirname, 'middleware')) + .filter(function (file) { + return file.match(/\.js$/); + }) + .forEach(function (m) { + loopback[m.replace(/\.js$/, '')] = require('./middleware/' + m); + }); +} + +/*! + * Error handler title + */ + +loopback.errorHandler.title = 'Loopback'; + +/** + * Create a data source with passing the provided options to the connector. + * + * @param {String} name (optional) + * @param {Object} options + * + * - connector - an loopback connector + * - other values - see the specified `connector` docs + */ + +loopback.createDataSource = function (name, options) { + var ds = new DataSource(name, options, loopback.Model.modelBuilder); + ds.createModel = function (name, properties, settings) { + var ModelCtor = loopback.createModel(name, properties, settings); + ModelCtor.attachTo(ds); + return ModelCtor; + }; + + if(ds.settings && ds.settings.defaultForType) { + loopback.setDefaultDataSourceForType(ds.settings.defaultForType, ds); + } + + return ds; +}; + +/** + * Create a named vanilla JavaScript class constructor with an attached set of properties and options. + * + * @param {String} name - must be unique + * @param {Object} properties + * @param {Object} options (optional) + */ + +loopback.createModel = function (name, properties, options) { + options = options || {}; + var BaseModel = options.base || options.super; + + if(typeof BaseModel === 'string') { + BaseModel = loopback.getModel(BaseModel); + } + + BaseModel = BaseModel || loopback.Model; + + var model = BaseModel.extend(name, properties, options); + + // try to attach + try { + loopback.autoAttachModel(model); + } catch(e) {} + + return model; +} + +/** + * Add a remote method to a model. + * @param {Function} fn + * @param {Object} options (optional) + */ + +loopback.remoteMethod = function (fn, options) { + fn.shared = true; + if(typeof options === 'object') { + Object.keys(options).forEach(function (key) { + fn[key] = options[key]; + }); + } + fn.http = fn.http || {verb: 'get'}; +} + +/** + * Create a template helper. + * + * var render = loopback.template('foo.ejs'); + * var html = render({foo: 'bar'}); + * + * @param {String} path Path to the template file. + * @returns {Function} + */ + +loopback.template = function (file) { + var templates = this._templates || (this._templates = {}); + var str = templates[file] || (templates[file] = fs.readFileSync(file, 'utf8')); + return ejs.compile(str); +} + +/** + * Get an in-memory data source. Use one if it already exists. + * + * @param {String} [name] The name of the data source. If not provided, the `'default'` is used. + */ + +loopback.memory = function (name) { + name = name || 'default'; + var memory = ( + this._memoryDataSources + || (this._memoryDataSources = {}) + )[name]; + + if(!memory) { + memory = this._memoryDataSources[name] = loopback.createDataSource({ + connector: loopback.Memory + }); + } + + return memory; +} + +/** + * Look up a model class by name from all models created by loopback.createModel() + * @param {String} modelName The model name + * @return {Model} The model class + */ +loopback.getModel = function(modelName) { + return loopback.Model.modelBuilder.models[modelName]; +}; + +/** + * Look up a model class by the base model class. The method can be used by LoopBack + * to find configured models in models.json over the base model. + * @param {Model} The base model class + * @return {Model} The subclass if found or the base class + */ +loopback.getModelByType = function(modelType) { + assert(typeof modelType === 'function', 'The model type must be a constructor'); + var models = loopback.Model.modelBuilder.models; + for(var m in models) { + if(models[m].prototype instanceof modelType) { + return models[m]; + } + } + return modelType; +}; + +/** + * Set the default `dataSource` for a given `type`. + * @param {String} type The datasource type + * @param {Object|DataSource} dataSource The data source settings or instance + * @return {DataSource} The data source instance + */ + +loopback.setDefaultDataSourceForType = function(type, dataSource) { + var defaultDataSources = this.defaultDataSources || (this.defaultDataSources = {}); + + if(!(dataSource instanceof DataSource)) { + dataSource = this.createDataSource(dataSource); + } + + defaultDataSources[type] = dataSource; + return dataSource; +} + +/** + * Get the default `dataSource` for a given `type`. + * @param {String} type The datasource type + * @return {DataSource} The data source instance + */ + +loopback.getDefaultDataSourceForType = function(type) { + return this.defaultDataSources && this.defaultDataSources[type]; +} + +/** + * Attach any model that does not have a dataSource to + * the default dataSource for the type the Model requests + */ + +loopback.autoAttach = function() { + var models = this.Model.modelBuilder.models; + assert.equal(typeof models, 'object', 'Cannot autoAttach without a models object'); + + Object.keys(models).forEach(function(modelName) { + var ModelCtor = models[modelName]; + + // Only auto attach if the model doesn't have an explicit data source + if(ModelCtor && (!(ModelCtor.dataSource instanceof DataSource))) { + loopback.autoAttachModel(ModelCtor); + } + }); +} + +loopback.autoAttachModel = function(ModelCtor) { + if(ModelCtor.autoAttach) { + var ds = loopback.getDefaultDataSourceForType(ModelCtor.autoAttach); + + assert(ds instanceof DataSource, 'cannot autoAttach model "' + + ModelCtor.modelName + + '". No dataSource found of type ' + ModelCtor.autoAttach); + + ModelCtor.attachTo(ds); + } +} + +function merge(a, b){ + if (a && b) { + for (var key in b) { + a[key] = b[key]; + } + } + return a; +} + +/* + * Built in models / services + */ + +loopback.Model = require('./models/model'); +loopback.Email = require('./models/email'); +loopback.User = require('./models/user'); +loopback.Application = require('./models/application'); +loopback.AccessToken = require('./models/access-token'); +loopback.Role = require('./models/role').Role; +loopback.RoleMapping = require('./models/role').RoleMapping; +loopback.ACL = require('./models/acl').ACL; +loopback.Scope = require('./models/acl').Scope; +loopback.Change = require('./models/change'); + +/*! + * Automatically attach these models to dataSources + */ + +var dataSourceTypes = { + DB: 'db', + MAIL: 'mail' +}; + +loopback.Email.autoAttach = dataSourceTypes.MAIL; +loopback.User.autoAttach = dataSourceTypes.DB; +loopback.AccessToken.autoAttach = dataSourceTypes.DB; +loopback.Role.autoAttach = dataSourceTypes.DB; +loopback.RoleMapping.autoAttach = dataSourceTypes.DB; +loopback.ACL.autoAttach = dataSourceTypes.DB; +loopback.Scope.autoAttach = dataSourceTypes.DB; +loopback.Application.autoAttach = dataSourceTypes.DB; + +},{"../package.json":96,"./application":2,"./models/access-token":9,"./models/acl":10,"./models/application":11,"./models/change":12,"./models/email":14,"./models/model":15,"./models/role":16,"./models/user":17,"assert":21,"ejs":58,"events":29,"express":3,"fs":20,"inflection":61,"loopback-datasource-juggler":62,"path":40}],8:[function(require,module,exports){ +var loopback = require('../loopback'); +var AccessToken = require('./access-token'); +var debug = require('debug')('loopback:security:access-context'); + +/** + * Access context represents the context for a request to access protected + * resources + * + * @class + * @property {Principal[]} principals An array of principals + * @property {Function} model The model class + * @property {String} modelName The model name + * @property {String} modelId The model id + * @property {String} property The model property/method/relation name + * @property {String} method The model method to be invoked + * @property {String} accessType The access type + * @property {AccessToken} accessToken The access token + * + * @param {Object} context The context object + * @returns {AccessContext} + * @constructor + */ +function AccessContext(context) { + if (!(this instanceof AccessContext)) { + return new AccessContext(context); + } + context = context || {}; + + this.principals = context.principals || []; + var model = context.model; + model = ('string' === typeof model) ? loopback.getModel(model) : model; + this.model = model; + this.modelName = model && model.modelName; + + this.modelId = context.id || context.modelId; + this.property = context.property || AccessContext.ALL; + + this.method = context.method; + + this.accessType = context.accessType || AccessContext.ALL; + this.accessToken = context.accessToken || AccessToken.ANONYMOUS; + + var principalType = context.principalType || Principal.USER; + var principalId = context.principalId || undefined; + var principalName = context.principalName || undefined; + if (principalId) { + this.addPrincipal(principalType, principalId, principalName); + } + + var token = this.accessToken || {}; + + if (token.userId) { + this.addPrincipal(Principal.USER, token.userId); + } + if (token.appId) { + this.addPrincipal(Principal.APPLICATION, token.appId); + } +} + +// Define constant for the wildcard +AccessContext.ALL = '*'; + +// Define constants for access types +AccessContext.READ = 'READ'; // Read operation +AccessContext.WRITE = 'WRITE'; // Write operation +AccessContext.EXECUTE = 'EXECUTE'; // Execute operation + +AccessContext.DEFAULT = 'DEFAULT'; // Not specified +AccessContext.ALLOW = 'ALLOW'; // Allow +AccessContext.ALARM = 'ALARM'; // Warn - send an alarm +AccessContext.AUDIT = 'AUDIT'; // Audit - record the access +AccessContext.DENY = 'DENY'; // Deny + +AccessContext.permissionOrder = { + DEFAULT: 0, + ALLOW: 1, + ALARM: 2, + AUDIT: 3, + DENY: 4 +}; + + +/** + * Add a principal to the context + * @param {String} principalType The principal type + * @param {*} principalId The principal id + * @param {String} [principalName] The principal name + * @returns {boolean} + */ +AccessContext.prototype.addPrincipal = function (principalType, principalId, principalName) { + var principal = new Principal(principalType, principalId, principalName); + for (var i = 0; i < this.principals.length; i++) { + var p = this.principals[i]; + if (p.equals(principal)) { + return false; + } + } + this.principals.push(principal); + + debug('adding principal %j', principal); + return true; +}; + +/** + * Get the user id + * @returns {*} + */ +AccessContext.prototype.getUserId = function() { + for (var i = 0; i < this.principals.length; i++) { + var p = this.principals[i]; + if (p.type === Principal.USER) { + return p.id; + } + } + return null; +}; + + +/** + * Get the application id + * @returns {*} + */ +AccessContext.prototype.getAppId = function() { + for (var i = 0; i < this.principals.length; i++) { + var p = this.principals[i]; + if (p.type === Principal.APPLICATION) { + return p.id; + } + } + return null; +}; + +/** + * Check if the access context has authenticated principals + * @returns {boolean} + */ +AccessContext.prototype.isAuthenticated = function() { + return !!(this.getUserId() || this.getAppId()); +}; + +/** + * Print debug info for access context. + */ + +AccessContext.prototype.debug = function() { + if(debug.enabled) { + debug('---AccessContext---'); + if(this.principals && this.principals.length) { + debug('principals:') + this.principals.forEach(function(principal) { + debug('principal: %j', principal) + }); + } else { + debug('principals: %j', this.principals); + } + debug('modelName %s', this.modelName); + debug('modelId %s', this.modelId); + debug('property %s', this.property); + debug('method %s', this.method); + debug('accessType %s', this.accessType); + if(this.accessToken) { + debug('accessToken:') + debug(' id %j', this.accessToken.id); + debug(' ttl %j', this.accessToken.ttl); + } + debug('getUserId() %s', this.getUserId()); + debug('isAuthenticated() %s', this.isAuthenticated()); + } +} + +/** + * This class represents the abstract notion of a principal, which can be used + * to represent any entity, such as an individual, a corporation, and a login id + * @param {String} type The principal type + * @param {*} id The princiapl id + * @param {String} [name] The principal name + * @returns {Principal} + * @class + */ +function Principal(type, id, name) { + if (!(this instanceof Principal)) { + return new Principal(type, id, name); + } + this.type = type; + this.id = id; + this.name = name; +} + +// Define constants for principal types +Principal.USER = 'USER'; +Principal.APP = Principal.APPLICATION = 'APP'; +Principal.ROLE = 'ROLE'; +Principal.SCOPE = 'SCOPE'; + +/** + * Compare if two principals are equal + * @param p The other principal + * @returns {boolean} + */ +Principal.prototype.equals = function (p) { + if (p instanceof Principal) { + return this.type === p.type && String(this.id) === String(p.id); + } + return false; +}; + +/** + * A request to access protected resources + * @param {String} model The model name + * @param {String} property + * @param {String} accessType The access type + * @param {String} permission The permission + * @returns {AccessRequest} + * @class + */ +function AccessRequest(model, property, accessType, permission) { + if (!(this instanceof AccessRequest)) { + return new AccessRequest(model, property, accessType); + } + this.model = model || AccessContext.ALL; + this.property = property || AccessContext.ALL; + this.accessType = accessType || AccessContext.ALL; + this.permission = permission || AccessContext.DEFAULT; + + if(debug.enabled) { + debug('---AccessRequest---'); + debug(' model %s', this.model); + debug(' property %s', this.property); + debug(' accessType %s', this.accessType); + debug(' permission %s', this.permission); + debug(' isWildcard() %s', this.isWildcard()); + } +} + +/** + * Is the request a wildcard + * @returns {boolean} + */ +AccessRequest.prototype.isWildcard = function () { + return this.model === AccessContext.ALL || + this.property === AccessContext.ALL || + this.accessType === AccessContext.ALL; +}; + +module.exports.AccessContext = AccessContext; +module.exports.Principal = Principal; +module.exports.AccessRequest = AccessRequest; + + + + +},{"../loopback":7,"./access-token":9,"debug":57}],9:[function(require,module,exports){ +var process=require("__browserify_process");/*! + * Module Dependencies. + */ + +var Model = require('../loopback').Model + , loopback = require('../loopback') + , assert = require('assert') + , crypto = require('crypto') + , uid = require('uid2') + , DEFAULT_TTL = 1209600 // 2 weeks in seconds + , DEFAULT_TOKEN_LEN = 64 + , Role = require('./role').Role + , ACL = require('./acl').ACL; + +/*! + * Default AccessToken properties. + */ + +var properties = { + id: {type: String, generated: true, id: 1}, + ttl: {type: Number, ttl: true, default: DEFAULT_TTL}, // time to live in seconds + created: {type: Date, default: function() { + return new Date(); + }} +}; + +/** + * Token based authentication and access control. + * + * @property id {String} Generated token ID + * @property ttl {Number} Time to live + * @property created {Date} When the token was created + * + * **Default ACLs** + * + * - DENY EVERYONE `*` + * - ALLOW EVERYONE create + * + * @class + * @inherits {Model} + */ + +var AccessToken = module.exports = Model.extend('AccessToken', properties, { + acls: [ + { + principalType: ACL.ROLE, + principalId: Role.EVERYONE, + permission: 'DENY' + }, + { + principalType: ACL.ROLE, + principalId: Role.EVERYONE, + property: 'create', + permission: 'ALLOW' + } + ] +}); + +/** + * Anonymous Token + * + * ```js + * assert(AccessToken.ANONYMOUS.id === '$anonymous'); + * ``` + */ + +AccessToken.ANONYMOUS = new AccessToken({id: '$anonymous'}); + +/** + * Create a cryptographically random access token id. + * + * @callback {Function} callback + * @param {Error} err + * @param {String} token + */ + +AccessToken.createAccessTokenId = function (fn) { + uid(this.settings.accessTokenIdLength || DEFAULT_TOKEN_LEN, function(err, guid) { + if(err) { + fn(err); + } else { + fn(null, guid); + } + }); +} + +/*! + * Hook to create accessToken id. + */ + +AccessToken.beforeCreate = function (next, data) { + data = data || {}; + + AccessToken.createAccessTokenId(function (err, id) { + if(err) { + next(err); + } else { + data.id = id; + + next(); + } + }); +} + +/** + * Find a token for the given `ServerRequest`. + * + * @param {ServerRequest} req + * @param {Object} [options] Options for finding the token + * @callback {Function} callback + * @param {Error} err + * @param {AccessToken} token + */ + +AccessToken.findForRequest = function(req, options, cb) { + var id = tokenIdForRequest(req, options); + + if(id) { + this.findById(id, function(err, token) { + if(err) { + cb(err); + } else if(token) { + token.validate(function(err, isValid) { + if(err) { + cb(err); + } else if(isValid) { + cb(null, token); + } else { + cb(new Error('Invalid Access Token')); + } + }); + } else { + cb(); + } + }); + } else { + process.nextTick(function() { + cb(); + }); + } +} + +/** + * Validate the token. + * + * @callback {Function} callback + * @param {Error} err + * @param {Boolean} isValid + */ + +AccessToken.prototype.validate = function(cb) { + try { + assert( + this.created && typeof this.created.getTime === 'function', + 'token.created must be a valid Date' + ); + assert(this.ttl !== 0, 'token.ttl must be not be 0'); + assert(this.ttl, 'token.ttl must exist'); + assert(this.ttl >= -1, 'token.ttl must be >= -1'); + + var now = Date.now(); + var created = this.created.getTime(); + var elapsedSeconds = (now - created) / 1000; + var secondsToLive = this.ttl; + var isValid = elapsedSeconds < secondsToLive; + + if(isValid) { + cb(null, isValid); + } else { + this.destroy(function(err) { + cb(err, isValid); + }); + } + } catch(e) { + cb(e); + } +} + +function tokenIdForRequest(req, options) { + var params = options.params || []; + var headers = options.headers || []; + var cookies = options.cookies || []; + var i = 0; + var length; + var id; + + params.push('access_token'); + headers.push('X-Access-Token'); + headers.push('authorization'); + cookies.push('access_token'); + cookies.push('authorization'); + + for(length = params.length; i < length; i++) { + id = req.param(params[i]); + + if(typeof id === 'string') { + return id; + } + } + + for(i = 0, length = headers.length; i < length; i++) { + id = req.header(headers[i]); + + if(typeof id === 'string') { + return id; + } + } + + if(req.signedCookies) { + for(i = 0, length = headers.length; i < length; i++) { + id = req.signedCookies[cookies[i]]; + + if(typeof id === 'string') { + return id; + } + } + } + return null; +} + +},{"../loopback":7,"./acl":10,"./role":16,"__browserify_process":36,"assert":21,"crypto":24,"uid2":94}],10:[function(require,module,exports){ +var process=require("__browserify_process");/*! + Schema ACL options + + Object level permissions, for example, an album owned by a user + + Factors to be authorized against: + + * model name: Album + * model instance properties: userId of the album, friends, shared + * methods + * app and/or user ids/roles + ** loggedIn + ** roles + ** userId + ** appId + ** none + ** everyone + ** relations: owner/friend/granted + + Class level permissions, for example, Album + * model name: Album + * methods + + URL/Route level permissions + * url pattern + * application id + * ip addresses + * http headers + + Map to oAuth 2.0 scopes + + */ + +var loopback = require('../loopback'); +var async = require('async'); +var assert = require('assert'); +var debug = require('debug')('loopback:security:acl'); + +var ctx = require('./access-context'); +var AccessContext = ctx.AccessContext; +var Principal = ctx.Principal; +var AccessRequest = ctx.AccessRequest; + +var role = require('./role'); +var Role = role.Role; + +/** + * System grants permissions to principals (users/applications, can be grouped + * into roles). + * + * Protected resource: the model data and operations + * (model/property/method/relation/…) + * + * For a given principal, such as client application and/or user, is it allowed + * to access (read/write/execute) + * the protected resource? + */ +var ACLSchema = { + model: String, // The name of the model + property: String, // The name of the property, method, scope, or relation + + /** + * Name of the access type - READ/WRITE/EXEC + */ + accessType: String, + + /** + * ALARM - Generate an alarm, in a system dependent way, the access specified + * in the permissions component of the ACL entry. + * ALLOW - Explicitly grants access to the resource. + * AUDIT - Log, in a system dependent way, the access specified in the + * permissions component of the ACL entry. + * DENY - Explicitly denies access to the resource. + */ + permission: String, + /** + * Type of the principal - Application/User/Role + */ + principalType: String, + /** + * Id of the principal - such as appId, userId or roleId + */ + principalId: String +}; + +/** + * A Model for access control meta data. + * + * @header ACL + * @class + * @inherits Model + */ + +var ACL = loopback.createModel('ACL', ACLSchema); + +ACL.ALL = AccessContext.ALL; + +ACL.DEFAULT = AccessContext.DEFAULT; // Not specified +ACL.ALLOW = AccessContext.ALLOW; // Allow +ACL.ALARM = AccessContext.ALARM; // Warn - send an alarm +ACL.AUDIT = AccessContext.AUDIT; // Audit - record the access +ACL.DENY = AccessContext.DENY; // Deny + +ACL.READ = AccessContext.READ; // Read operation +ACL.WRITE = AccessContext.WRITE; // Write operation +ACL.EXECUTE = AccessContext.EXECUTE; // Execute operation + +ACL.USER = Principal.USER; +ACL.APP = ACL.APPLICATION = Principal.APPLICATION; +ACL.ROLE = Principal.ROLE; +ACL.SCOPE = Principal.SCOPE; + +/** + * Calculate the matching score for the given rule and request + * @param {ACL} rule The ACL entry + * @param {AccessRequest} req The request + * @returns {number} + */ +ACL.getMatchingScore = function getMatchingScore(rule, req) { + var props = ['model', 'property', 'accessType']; + var score = 0; + for (var i = 0; i < props.length; i++) { + // Shift the score by 4 for each of the properties as the weight + score = score * 4; + var val1 = rule[props[i]] || ACL.ALL; + var val2 = req[props[i]] || ACL.ALL; + if (val1 === val2) { + // Exact match + score += 3; + } else if (val1 === ACL.ALL) { + // Wildcard match + score += 2; + } else if (val2 === ACL.ALL) { + // Doesn't match at all + score += 1; + } else { + return -1; + } + } + score = score * 4; + score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1; + return score; +}; + +/*! + * Resolve permission from the ACLs + * @param {Object[]) acls The list of ACLs + * @param {Object} req The request + * @returns {AccessRequest} result The effective ACL + */ +ACL.resolvePermission = function resolvePermission(acls, req) { + // Sort by the matching score in descending order + acls = acls.sort(function (rule1, rule2) { + return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req); + }); + var permission = ACL.DEFAULT; + var score = 0; + for (var i = 0; i < acls.length; i++) { + score = ACL.getMatchingScore(acls[i], req); + if (score < 0) { + break; + } + if (!req.isWildcard()) { + // We should stop from the first match for non-wildcard + permission = acls[i].permission; + break; + } else { + if(acls[i].model === req.model && + acls[i].property === req.property && + acls[i].accessType === req.accessType + ) { + // We should stop at the exact match + permission = acls[i].permission; + break; + } + // For wildcard match, find the strongest permission + if(AccessContext.permissionOrder[acls[i].permission] + > AccessContext.permissionOrder[permission]) { + permission = acls[i].permission; + } + } + } + + var res = new AccessRequest(req.model, req.property, req.accessType, + permission || ACL.DEFAULT); + return res; +}; + +/*! + * Get the static ACLs from the model definition + * @param {String} model The model name + * @param {String} property The property/method/relation name + * + * @return {Object[]} An array of ACLs + */ +ACL.getStaticACLs = function getStaticACLs(model, property) { + var modelClass = loopback.getModel(model); + var staticACLs = []; + if (modelClass && modelClass.settings.acls) { + modelClass.settings.acls.forEach(function (acl) { + staticACLs.push(new ACL({ + model: model, + property: acl.property || ACL.ALL, + principalType: acl.principalType, + principalId: acl.principalId, // TODO: Should it be a name? + accessType: acl.accessType, + permission: acl.permission + })); + + staticACLs[staticACLs.length - 1].debug('Adding ACL'); + }); + } + var prop = modelClass && + (modelClass.definition.properties[property] // regular property + || (modelClass._scopeMeta && modelClass._scopeMeta[property]) // relation/scope + || modelClass[property] // static method + || modelClass.prototype[property]); // prototype method + if (prop && prop.acls) { + prop.acls.forEach(function (acl) { + staticACLs.push(new ACL({ + model: modelClass.modelName, + property: property, + principalType: acl.principalType, + principalId: acl.principalId, + accessType: acl.accessType, + permission: acl.permission + })); + }); + } + return staticACLs; +}; + +/** + * Check if the given principal is allowed to access the model/property + * @param {String} principalType The principal type + * @param {String} principalId The principal id + * @param {String} model The model name + * @param {String} property The property/method/relation name + * @param {String} accessType The access type + * @param {Function} callback The callback function + * + * @callback callback + * @param {String|Error} err The error object + * @param {AccessRequest} result The access permission + */ +ACL.checkPermission = function checkPermission(principalType, principalId, + model, property, accessType, + callback) { + property = property || ACL.ALL; + var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]}; + accessType = accessType || ACL.ALL; + var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]}; + + var req = new AccessRequest(model, property, accessType); + + var acls = this.getStaticACLs(model, property); + + var resolved = this.resolvePermission(acls, req); + + if(resolved && resolved.permission === ACL.DENY) { + debug('Permission denied by statically resolved permission'); + debug(' Resolved Permission: %j', resolved); + process.nextTick(function() { + callback && callback(null, resolved); + }); + return; + } + + var self = this; + this.find({where: {principalType: principalType, principalId: principalId, + model: model, property: propertyQuery, accessType: accessTypeQuery}}, + function (err, dynACLs) { + if (err) { + callback && callback(err); + return; + } + acls = acls.concat(dynACLs); + resolved = self.resolvePermission(acls, req); + if(resolved && resolved.permission === ACL.DEFAULT) { + var modelClass = loopback.getModel(model); + resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW; + } + callback && callback(null, resolved); + }); +}; + +ACL.prototype.debug = function() { + if(debug.enabled) { + debug('---ACL---'); + debug('model %s', this.model); + debug('property %s', this.property); + debug('principalType %s', this.principalType); + debug('principalId %s', this.principalId); + debug('accessType %s', this.accessType); + debug('permission %s', this.permission); + } +} + +/** + * Check if the request has the permission to access + * @param {Object} context + * @property {Object[]} principals An array of principals + * @property {String|Model} model The model name or model class + * @property {*} id The model instance id + * @property {String} property The property/method/relation name + * @property {String} accessType The access type + * @param {Function} callback + */ +ACL.checkAccess = function (context, callback) { + if(!(context instanceof AccessContext)) { + context = new AccessContext(context); + } + + var model = context.model; + var property = context.property; + var accessType = context.accessType; + + var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]}; + var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]}; + + var req = new AccessRequest(model.modelName, property, accessType); + + var effectiveACLs = []; + var staticACLs = this.getStaticACLs(model.modelName, property); + + var self = this; + var roleModel = loopback.getModelByType(Role); + this.find({where: {model: model.modelName, property: propertyQuery, + accessType: accessTypeQuery}}, function (err, acls) { + if (err) { + callback && callback(err); + return; + } + var inRoleTasks = []; + + acls = acls.concat(staticACLs); + + acls.forEach(function (acl) { + // Check exact matches + for (var i = 0; i < context.principals.length; i++) { + var p = context.principals[i]; + if (p.type === acl.principalType + && String(p.id) === String(acl.principalId)) { + effectiveACLs.push(acl); + return; + } + } + + // Check role matches + if (acl.principalType === ACL.ROLE) { + inRoleTasks.push(function (done) { + roleModel.isInRole(acl.principalId, context, + function (err, inRole) { + if (!err && inRole) { + effectiveACLs.push(acl); + } + done(err, acl); + }); + }); + } + }); + + async.parallel(inRoleTasks, function (err, results) { + if(err) { + callback && callback(err, null); + return; + } + var resolved = self.resolvePermission(effectiveACLs, req); + if(resolved && resolved.permission === ACL.DEFAULT) { + resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW; + } + debug('checkAccess() returns: %j', resolved); + callback && callback(null, resolved); + }); + }); +}; + + +/** + * Check if the given access token can invoke the method + * @param {AccessToken} token The access token + * @param {String} model The model name + * @param {*} modelId The model id + * @param {String} method The method name + * @end + * @callback {Function} callback + * @param {String|Error} err The error object + * @param {Boolean} allowed is the request allowed + */ +ACL.checkAccessForToken = function (token, model, modelId, method, callback) { + assert(token, 'Access token is required'); + + var context = new AccessContext({ + accessToken: token, + model: model, + property: method, + method: method, + modelId: modelId + }); + + context.accessType = context.model._getAccessTypeForMethod(method); + + context.debug(); + + this.checkAccess(context, function (err, access) { + if (err) { + callback && callback(err); + return; + } + callback && callback(null, access.permission !== ACL.DENY); + }); +}; + +/*! + * Schema for Scope which represents the permissions that are granted to client + * applications by the resource owner + */ +var ScopeSchema = { + name: {type: String, required: true}, + description: String +}; + +/** + * Resource owner grants/delegates permissions to client applications + * + * For a protected resource, does the client application have the authorization + * from the resource owner (user or system)? + * + * Scope has many resource access entries + * @class + */ +var Scope = loopback.createModel('Scope', ScopeSchema); + + +/** + * Check if the given scope is allowed to access the model/property + * @param {String} scope The scope name + * @param {String} model The model name + * @param {String} property The property/method/relation name + * @param {String} accessType The access type + * @callback {Function} callback + * @param {String|Error} err The error object + * @param {AccessRequest} result The access permission + */ +Scope.checkPermission = function (scope, model, property, accessType, callback) { + this.findOne({where: {name: scope}}, function (err, scope) { + if (err) { + callback && callback(err); + } else { + var aclModel = loopback.getModelByType(ACL); + aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback); + } + }); +}; + +module.exports.ACL = ACL; +module.exports.Scope = Scope; + +},{"../loopback":7,"./access-context":8,"./role":16,"__browserify_process":36,"assert":21,"async":18,"debug":57}],11:[function(require,module,exports){ +var loopback = require('../loopback'); +var assert = require('assert'); + +// Authentication schemes +var AuthenticationSchemeSchema = { + scheme: String, // local, facebook, google, twitter, linkedin, github + credential: Object // Scheme-specific credentials +}; + +// See https://github.com/argon/node-apn/blob/master/doc/apn.markdown +var APNSSettingSchema = { + /** + * production or development mode. It denotes what default APNS servers to be + * used to send notifications + * - true (production mode) + * - push: gateway.push.apple.com:2195 + * - feedback: feedback.push.apple.com:2196 + * - false (development mode, the default) + * - push: gateway.sandbox.push.apple.com:2195 + * - feedback: feedback.sandbox.push.apple.com:2196 + */ + production: Boolean, + certData: String, // The certificate data loaded from the cert.pem file + keyData: String, // The key data loaded from the key.pem file + + pushOptions: {type: { + gateway: String, + port: Number + }}, + + feedbackOptions: {type: { + gateway: String, + port: Number, + batchFeedback: Boolean, + interval: Number + }} +}; + +var GcmSettingsSchema = { + serverApiKey: String +}; + +// Push notification settings +var PushNotificationSettingSchema = { + apns: APNSSettingSchema, + gcm: GcmSettingsSchema +}; + +/** + * Data model for Application + */ +var ApplicationSchema = { + id: {type: String, id: true, generated: true}, + // Basic information + name: {type: String, required: true}, // The name + description: String, // The description + icon: String, // The icon image url + + owner: String, // The user id of the developer who registers the application + collaborators: [String], // A list of users ids who have permissions to work on this app + + // EMail + email: String, // e-mail address + emailVerified: Boolean, // Is the e-mail verified + + // oAuth 2.0 settings + url: String, // The application url + callbackUrls: [String], // oAuth 2.0 code/token callback url + permissions: [String], // A list of permissions required by the application + + // Keys + clientKey: String, + javaScriptKey: String, + restApiKey: String, + windowsKey: String, + masterKey: String, + + // Push notification + pushSettings: PushNotificationSettingSchema, + + // User Authentication + authenticationEnabled: {type: Boolean, default: true}, + anonymousAllowed: {type: Boolean, default: true}, + authenticationSchemes: [AuthenticationSchemeSchema], + + status: {type: String, default: 'sandbox'}, // Status of the application, production/sandbox/disabled + + // Timestamps + created: {type: Date, default: Date}, + modified: {type: Date, default: Date} +}; + +/** + * Application management functions + */ + +var crypto = require('crypto'); + +function generateKey(hmacKey, algorithm, encoding) { + hmacKey = hmacKey || 'loopback'; + algorithm = algorithm || 'sha256'; + encoding = encoding || 'base64'; + var hmac = crypto.createHmac(algorithm, hmacKey); + var buf = crypto.randomBytes(64); + hmac.update(buf); + return hmac.digest('base64'); +} + +/** + * Manage client applications and organize their users. + * @class + * @inherits {Model} + */ + +var Application = loopback.createModel('Application', ApplicationSchema); + +/*! + * A hook to generate keys before creation + * @param next + */ +Application.beforeCreate = function (next) { + var app = this; + app.created = app.modified = new Date(); + app.id = generateKey('id', 'sha1'); + app.clientKey = generateKey('client'); + app.javaScriptKey = generateKey('javaScript'); + app.restApiKey = generateKey('restApi'); + app.windowsKey = generateKey('windows'); + app.masterKey = generateKey('master'); + next(); +}; + +/** + * Register a new application + * @param owner Owner's user id + * @param name Name of the application + * @param options Other options + * @param cb Callback function + */ +Application.register = function (owner, name, options, cb) { + assert(owner, 'owner is required'); + assert(name, 'name is required'); + + if (typeof options === 'function' && !cb) { + cb = options; + options = {}; + } + var props = {owner: owner, name: name}; + for (var p in options) { + if (!(p in props)) { + props[p] = options[p]; + } + } + this.create(props, cb); +}; + +/** + * Reset keys for the application instance + * @callback {Function} callback + * @param {Error} err + */ +Application.prototype.resetKeys = function (cb) { + this.clientKey = generateKey('client'); + this.javaScriptKey = generateKey('javaScript'); + this.restApiKey = generateKey('restApi'); + this.windowsKey = generateKey('windows'); + this.masterKey = generateKey('master'); + this.modified = new Date(); + this.save(cb); +}; + +/** + * Reset keys for a given application by the appId + * @param {Any} appId + * @callback {Function} callback + * @param {Error} err + */ +Application.resetKeys = function (appId, cb) { + this.findById(appId, function (err, app) { + if (err) { + cb && cb(err, app); + return; + } + app.resetKeys(cb); + }); +}; + +/** + * Authenticate the application id and key. + * + * `matched` will be one of + * + * - clientKey + * - javaScriptKey + * - restApiKey + * - windowsKey + * - masterKey + * + * @param {Any} appId + * @param {String} key + * @callback {Function} callback + * @param {Error} err + * @param {String} matched - The matching key + */ +Application.authenticate = function (appId, key, cb) { + this.findById(appId, function (err, app) { + if (err || !app) { + cb && cb(err, null); + return; + } + var matched = null; + ['clientKey', 'javaScriptKey', 'restApiKey', 'windowsKey', 'masterKey'].forEach(function (k) { + if (app[k] === key) { + matched = k; + } + }); + cb && cb(null, matched); + }); +}; + +module.exports = Application; + + +},{"../loopback":7,"assert":21,"crypto":24}],12:[function(require,module,exports){ +/** + * Module Dependencies. + */ + +var Model = require('../loopback').Model + , loopback = require('../loopback') + , crypto = require('crypto') + , CJSON = {stringify: require('canonical-json')} + , async = require('async') + , assert = require('assert'); + +/** + * Properties + */ + +var properties = { + id: {type: String, generated: true, id: true}, + rev: {type: String}, + prev: {type: String}, + checkpoint: {type: Number}, + modelName: {type: String}, + modelId: {type: String} +}; + +/** + * Options + */ + +var options = { + trackChanges: false +}; + +/** + * Change list entry. + * + * @property id {String} Hash of the modelName and id + * @property rev {String} the current model revision + * @property prev {String} the previous model revision + * @property checkpoint {Number} the current checkpoint at time of the change + * @property modelName {String} the model name + * @property modelId {String} the model id + * + * @class + * @inherits {Model} + */ + +var Change = module.exports = Model.extend('Change', properties, options); + +/*! + * Constants + */ + +Change.UPDATE = 'update'; +Change.CREATE = 'create'; +Change.DELETE = 'delete'; +Change.UNKNOWN = 'unknown'; + +/*! + * Conflict Class + */ + +Change.Conflict = Conflict; + +/*! + * Setup the extended model. + */ + +Change.setup = function() { + var Change = this; + + Change.getter.id = function() { + var hasModel = this.modelName && this.modelId; + if(!hasModel) return null; + + return Change.idForModel(this.modelName, this.modelId); + } +} +Change.setup(); + + +/** + * Track the recent change of the given modelIds. + * + * @param {String} modelName + * @param {Array} modelIds + * @callback {Function} callback + * @param {Error} err + * @param {Array} changes Changes that were tracked + */ + +Change.track = function(modelName, modelIds, callback) { + var tasks = []; + var Change = this; + + modelIds.forEach(function(id) { + tasks.push(function(cb) { + Change.findOrCreate(modelName, id, function(err, change) { + if(err) return Change.handleError(err, cb); + change.rectify(cb); + }); + }); + }); + async.parallel(tasks, callback); +} + +/** + * Get an identifier for a given model. + * + * @param {String} modelName + * @param {String} modelId + * @return {String} + */ + +Change.idForModel = function(modelName, modelId) { + return this.hash([modelName, modelId].join('-')); +} + +/** + * Find or create a change for the given model. + * + * @param {String} modelName + * @param {String} modelId + * @callback {Function} callback + * @param {Error} err + * @param {Change} change + * @end + */ + +Change.findOrCreate = function(modelName, modelId, callback) { + var id = this.idForModel(modelName, modelId); + var Change = this; + + this.findById(id, function(err, change) { + if(err) return callback(err); + if(change) { + callback(null, change); + } else { + var ch = new Change({ + id: id, + modelName: modelName, + modelId: modelId + }); + ch.save(callback); + } + }); +} + +/** + * Update (or create) the change with the current revision. + * + * @callback {Function} callback + * @param {Error} err + * @param {Change} change + */ + +Change.prototype.rectify = function(cb) { + var change = this; + var tasks = [ + updateRevision, + updateCheckpoint + ]; + + if(this.rev) this.prev = this.rev; + + async.parallel(tasks, function(err) { + if(err) return cb(err); + change.save(cb); + }); + + function updateRevision(cb) { + // get the current revision + change.currentRevision(function(err, rev) { + if(err) return Change.handleError(err, cb); + change.rev = rev; + cb(); + }); + } + + function updateCheckpoint(cb) { + change.constructor.getCheckpointModel().current(function(err, checkpoint) { + if(err) return Change.handleError(err); + change.checkpoint = ++checkpoint; + cb(); + }); + } +} + +/** + * Get a change's current revision based on current data. + * @callback {Function} callback + * @param {Error} err + * @param {String} rev The current revision + */ + +Change.prototype.currentRevision = function(cb) { + var model = this.getModelCtor(); + model.findById(this.modelId, function(err, inst) { + if(err) return Change.handleError(err, cb); + if(inst) { + cb(null, Change.revisionForInst(inst)); + } else { + cb(null, null); + } + }); +} + +/** + * Create a hash of the given `string` with the `options.hashAlgorithm`. + * **Default: `sha1`** + * + * @param {String} str The string to be hashed + * @return {String} The hashed string + */ + +Change.hash = function(str) { + return crypto + .createHash(Change.settings.hashAlgorithm || 'sha1') + .update(str) + .digest('hex'); +} + +/** + * Get the revision string for the given object + * @param {Object} inst The data to get the revision string for + * @return {String} The revision string + */ + +Change.revisionForInst = function(inst) { + return this.hash(CJSON.stringify(inst)); +} + +/** + * Get a change's type. Returns one of: + * + * - `Change.UPDATE` + * - `Change.CREATE` + * - `Change.DELETE` + * - `Change.UNKNOWN` + * + * @return {String} the type of change + */ + +Change.prototype.type = function() { + if(this.rev && this.prev) { + return Change.UPDATE; + } + if(this.rev && !this.prev) { + return Change.CREATE; + } + if(!this.rev && this.prev) { + return Change.DELETE; + } + return Change.UNKNOWN; +} + +/** + * Get the `Model` class for `change.modelName`. + * @return {Model} + */ + +Change.prototype.getModelCtor = function() { + // todo - not sure if this works with multiple data sources + return loopback.getModel(this.modelName); +} + +/** + * Compare two changes. + * @param {Change} change + * @return {Boolean} + */ + +Change.prototype.equals = function(change) { + return change.rev === this.rev; +} + +/** + * Determine if the change is based on the given change. + * @param {Change} change + * @return {Boolean} + */ + +Change.prototype.isBasedOn = function(change) { + return this.prev === change.rev; +} + +/** + * Determine the differences for a given model since a given checkpoint. + * + * The callback will contain an error or `result`. + * + * **result** + * + * ```js + * { + * deltas: Array, + * conflicts: Array + * } + * ``` + * + * **deltas** + * + * An array of changes that differ from `remoteChanges`. + * + * **conflicts** + * + * An array of changes that conflict with `remoteChanges`. + * + * @param {String} modelName + * @param {Number} since Compare changes after this checkpoint + * @param {Change[]} remoteChanges A set of changes to compare + * @callback {Function} callback + * @param {Error} err + * @param {Object} result See above. + */ + +Change.diff = function(modelName, since, remoteChanges, callback) { + var remoteChangeIndex = {}; + var modelIds = []; + remoteChanges.forEach(function(ch) { + modelIds.push(ch.modelId); + remoteChangeIndex[ch.modelId] = new Change(ch); + }); + + // normalize `since` + since = Number(since) || 0; + this.find({ + where: { + modelName: modelName, + modelId: {inq: modelIds}, + checkpoint: {gt: since} + } + }, function(err, localChanges) { + if(err) return callback(err); + var deltas = []; + var conflicts = []; + var localModelIds = []; + + localChanges.forEach(function(localChange) { + localModelIds.push(localChange.modelId); + var remoteChange = remoteChangeIndex[localChange.modelId]; + if(!localChange.equals(remoteChange)) { + if(remoteChange.isBasedOn(localChange)) { + deltas.push(remoteChange); + } else { + conflicts.push(localChange); + } + } + }); + + modelIds.forEach(function(id) { + if(localModelIds.indexOf(id) === -1) { + deltas.push(remoteChangeIndex[id]); + } + }); + + callback(null, { + deltas: deltas, + conflicts: conflicts + }); + }); +} + +/** + * Correct all change list entries. + * @param {Function} callback + */ + +Change.rectifyAll = function(cb) { + // this should be optimized + this.find(function(err, changes) { + if(err) return cb(err); + changes.forEach(function(change) { + change.rectify(); + }); + }); +} + +/** + * Get the checkpoint model. + * @return {Checkpoint} + */ + +Change.getCheckpointModel = function() { + var checkpointModel = this.Checkpoint; + if(checkpointModel) return checkpointModel; + this.checkpoint = checkpointModel = require('./checkpoint').extend('checkpoint'); + checkpointModel.attachTo(this.dataSource); + return checkpointModel; +} + + +/** + * When two changes conflict a conflict is created. + * + * **Note: call `conflict.fetch()` to get the `target` and `source` models. + * + * @param {Change} sourceChange The change object for the source model + * @param {Change} targetChange The conflicting model's change object + * @property {Model} source The source model instance + * @property {Model} target The target model instance + */ + +function Conflict(sourceChange, targetChange) { + this.sourceChange = sourceChange; + this.targetChange = targetChange; +} + +Conflict.prototype.fetch = function(cb) { + var conflict = this; + var tasks = [ + getSourceModel, + getTargetModel + ]; + + async.parallel(tasks, cb); + + function getSourceModel(change, cb) { + conflict.sourceModel.getModel(function(err, model) { + if(err) return cb(err); + conflict.source = model; + cb(); + }); + } + + function getTargetModel(cb) { + conflict.targetModel.getModel(function(err, model) { + if(err) return cb(err); + conflict.target = model; + cb(); + }); + } +} + +Conflict.prototype.resolve = function(cb) { + this.sourceChange.prev = this.targetChange.rev; + this.sourceChange.save(cb); +} + +},{"../loopback":7,"./checkpoint":13,"assert":21,"async":18,"canonical-json":56,"crypto":24}],13:[function(require,module,exports){ +/** + * Module Dependencies. + */ + +var Model = require('../loopback').Model + , loopback = require('../loopback') + , assert = require('assert'); + +/** + * Properties + */ + +var properties = { + id: {type: Number, generated: true, id: true}, + time: {type: Number, generated: true, default: Date.now}, + sourceId: {type: String} +}; + +/** + * Options + */ + +var options = { + +}; + +/** + * Checkpoint list entry. + * + * @property id {Number} the sequencial identifier of a checkpoint + * @property time {Number} the time when the checkpoint was created + * @property sourceId {String} the source identifier + * + * @class + * @inherits {Model} + */ + +var Checkpoint = module.exports = Model.extend('Checkpoint', properties, options); + +/** + * Get the current checkpoint id + * @callback {Function} callback + * @param {Error} err + * @param {Number} checkpointId The current checkpoint id + */ + +Checkpoint.current = function(cb) { + this.find({ + limit: 1, + sort: 'id DESC' + }, function(err, checkpoints) { + if(err) return cb(err); + var checkpoint = checkpoints[0] || {id: 0}; + cb(null, checkpoint.id); + }); +} + + +},{"../loopback":7,"assert":21}],14:[function(require,module,exports){ +/*! + * Module Dependencies. + */ + +var Model = require('../loopback').Model + , loopback = require('../loopback'); + +var properties = { + to: {type: String, required: true}, + from: {type: String, required: true}, + subject: {type: String, required: true}, + text: {type: String}, + html: {type: String} +}; + +/** + * The Email Model. + * + * **Properties** + * + * - `to` - **{ String }** **required** + * - `from` - **{ String }** **required** + * - `subject` - **{ String }** **required** + * - `text` - **{ String }** + * - `html` - **{ String }** + * + * @class + * @inherits {Model} + */ + +var Email = module.exports = Model.extend('Email', properties); + +/** + * Send an email with the given `options`. + * + * Example Options: + * + * ```json + * { + * from: "Fred Foo ✔ ", // sender address + * to: "bar@blurdybloop.com, baz@blurdybloop.com", // list of receivers + * subject: "Hello ✔", // Subject line + * text: "Hello world ✔", // plaintext body + * html: "Hello world ✔" // html body + * } + * ``` + * + * See https://github.com/andris9/Nodemailer for other supported options. + * + * @param {Object} options + * @param {Function} callback Called after the e-mail is sent or the sending failed + */ + +Email.prototype.send = function() { + throw new Error('You must connect the Email Model to a Mail connector'); +} +},{"../loopback":7}],15:[function(require,module,exports){ +/*! + * Module Dependencies. + */ +var loopback = require('../loopback'); +var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder; +var modeler = new ModelBuilder(); +var async = require('async'); +var assert = require('assert'); + +/** + * The base class for **all models**. + * + * **Inheriting from `Model`** + * + * ```js + * var properties = {...}; + * var options = {...}; + * var MyModel = loopback.Model.extend('MyModel', properties, options); + * ``` + * + * **Options** + * + * - `trackChanges` - If true, changes to the model will be tracked. **Required + * for replication.** + * + * **Events** + * + * #### Event: `changed` + * + * Emitted after a model has been successfully created, saved, or updated. + * + * ```js + * MyModel.on('changed', function(inst) { + * console.log('model with id %s has been changed', inst.id); + * // => model with id 1 has been changed + * }); + * ``` + * + * #### Event: `deleted` + * + * Emitted after an individual model has been deleted. + * + * ```js + * MyModel.on('deleted', function(inst) { + * console.log('model with id %s has been deleted', inst.id); + * // => model with id 1 has been deleted + * }); + * ``` + * + * #### Event: `deletedAll` + * + * Emitted after an individual model has been deleted. + * + * ```js + * MyModel.on('deletedAll', function(where) { + * if(where) { + * console.log('all models where', where, 'have been deleted'); + * // => all models where + * // => {price: {gt: 100}} + * // => have been deleted + * } + * }); + * ``` + * + * #### Event: `attached` + * + * Emitted after a `Model` has been attached to an `app`. + * + * #### Event: `dataSourceAttached` + * + * Emitted after a `Model` has been attached to a `DataSource`. + * + * @class + * @param {Object} data + * @property {String} modelName The name of the model + * @property {DataSource} dataSource + */ + +var Model = module.exports = modeler.define('Model'); + +Model.shared = true; + +/*! + * Called when a model is extended. + */ + +Model.setup = function () { + var ModelCtor = this; + var options = this.settings; + + ModelCtor.sharedCtor = function (data, id, fn) { + if(typeof data === 'function') { + fn = data; + data = null; + id = null; + } else if (typeof id === 'function') { + fn = id; + + if(typeof data !== 'object') { + id = data; + data = null; + } else { + id = null; + } + } + + if(id && data) { + var model = new ModelCtor(data); + model.id = id; + fn(null, model); + } else if(data) { + fn(null, new ModelCtor(data)); + } else if(id) { + ModelCtor.findById(id, function (err, model) { + if(err) { + fn(err); + } else if(model) { + fn(null, model); + } else { + err = new Error('could not find a model with id ' + id); + err.statusCode = 404; + + fn(err); + } + }); + } else { + fn(new Error('must specify an id or data')); + } + } + + // before remote hook + ModelCtor.beforeRemote = function (name, fn) { + var self = this; + if(this.app) { + var remotes = this.app.remotes(); + remotes.before(self.pluralModelName + '.' + name, function (ctx, next) { + fn(ctx, ctx.result, next); + }); + } else { + var args = arguments; + this.once('attached', function () { + self.beforeRemote.apply(self, args); + }); + } + }; + + // after remote hook + ModelCtor.afterRemote = function (name, fn) { + var self = this; + if(this.app) { + var remotes = this.app.remotes(); + remotes.after(self.pluralModelName + '.' + name, function (ctx, next) { + fn(ctx, ctx.result, next); + }); + } else { + var args = arguments; + this.once('attached', function () { + self.afterRemote.apply(self, args); + }); + } + }; + + // Map the prototype method to /:id with data in the body + ModelCtor.sharedCtor.accepts = [ + {arg: 'id', type: 'any', http: {source: 'path'}} + // {arg: 'instance', type: 'object', http: {source: 'body'}} + ]; + + ModelCtor.sharedCtor.http = [ + {path: '/:id'} + ]; + + ModelCtor.sharedCtor.returns = {root: true}; + + ModelCtor.once('dataSourceAttached', function() { + // enable change tracking (usually for replication) + if(options.trackChanges) { + ModelCtor.enableChangeTracking(); + } + }); + + return ModelCtor; +}; + +/*! + * Get the reference to ACL in a lazy fashion to avoid race condition in require + */ +var ACL = null; +function getACL() { + return ACL || (ACL = require('./acl').ACL); +} + +/** + * Check if the given access token can invoke the method + * + * @param {AccessToken} token The access token + * @param {*} modelId The model id + * @param {String} method The method name + * @param callback The callback function + * + * @callback {Function} callback + * @param {String|Error} err The error object + * @param {Boolean} allowed is the request allowed + */ + +Model.checkAccess = function(token, modelId, method, callback) { + var ANONYMOUS = require('./access-token').ANONYMOUS; + token = token || ANONYMOUS; + var ACL = getACL(); + var methodName = 'string' === typeof method? method: method && method.name; + ACL.checkAccessForToken(token, this.modelName, modelId, methodName, callback); +}; + +/*! + * Determine the access type for the given `RemoteMethod`. + * + * @api private + * @param {RemoteMethod} method + */ + +Model._getAccessTypeForMethod = function(method) { + if(typeof method === 'string') { + method = {name: method}; + } + assert( + typeof method === 'object', + 'method is a required argument and must be a RemoteMethod object' + ); + + var ACL = getACL(); + + switch(method.name) { + case'create': + return ACL.WRITE; + case 'updateOrCreate': + return ACL.WRITE; + case 'upsert': + return ACL.WRITE; + case 'exists': + return ACL.READ; + case 'findById': + return ACL.READ; + case 'find': + return ACL.READ; + case 'findOne': + return ACL.READ; + case 'destroyById': + return ACL.WRITE; + case 'deleteById': + return ACL.WRITE; + case 'removeById': + return ACL.WRITE; + case 'count': + return ACL.READ; + break; + default: + return ACL.EXECUTE; + break; + } +} + +// setup the initial model +Model.setup(); + +/** + * Get a set of deltas and conflicts since the given checkpoint. + * + * See `Change.diff()` for details. + * + * @param {Number} since Find changes since this checkpoint + * @param {Array} remoteChanges An array of change objects + * @param {Function} callback + */ + +Model.diff = function(since, remoteChanges, callback) { + var Change = this.getChangeModel(); + Change.diff(this.modelName, since, remoteChanges, callback); +} + +/** + * Get the changes to a model since a given checkpoing. Provide a filter object + * to reduce the number of results returned. + * @param {Number} since Only return changes since this checkpoint + * @param {Object} filter Only include changes that match this filter + * (same as `Model.find(filter, ...)`) + * @callback {Function} callback + * @param {Error} err + * @param {Array} changes An array of `Change` objects + * @end + */ + +Model.changes = function(since, filter, callback) { + var idName = this.dataSource.idName(this.modelName); + var Change = this.getChangeModel(); + var model = this; + + filter = filter || {}; + filter.fields = {}; + filter.where = filter.where || {}; + filter.fields[idName] = true; + + // this whole thing could be optimized a bit more + Change.find({ + checkpoint: {gt: since}, + modelName: this.modelName + }, function(err, changes) { + if(err) return cb(err); + var ids = changes.map(function(change) { + return change.modelId.toString(); + }); + filter.where[idName] = {inq: ids}; + model.find(filter, function(err, models) { + if(err) return cb(err); + var modelIds = models.map(function(m) { + return m[idName].toString(); + }); + callback(null, changes.filter(function(ch) { + if(ch.type() === Change.DELETE) return true; + return modelIds.indexOf(ch.modelId) > -1; + })); + }); + }); +} + +/** + * Create a checkpoint. + * + * @param {Function} callback + */ + +Model.checkpoint = function(cb) { + var Checkpoint = this.getChangeModel().getCheckpointModel(); + this.getSourceId(function(err, sourceId) { + if(err) return cb(err); + Checkpoint.create({ + sourceId: sourceId + }, cb); + }); +} + +/** + * Get the current checkpoint id. + * + * @callback {Function} callback + * @param {Error} err + * @param {Number} currentCheckpointId + * @end + */ + +Model.currentCheckpoint = function(cb) { + var Checkpoint = this.getChangeModel().getCheckpointModel(); + Checkpoint.current(cb); +} + +/** + * Replicate changes since the given checkpoint to the given target model. + * + * @param {Number} since Since this checkpoint + * @param {Model} targetModel Target this model class + * @options {Object} options + * @property {Object} filter Replicate models that match this filter + * @callback {Function} callback + * @param {Error} err + * @param {Array} conflicts A list of changes that could not be replicated + * due to conflicts. + */ + +Model.replicate = function(since, targetModel, options, callback) { + var sourceModel = this; + var diff; + var updates; + var Change = this.getChangeModel(); + var TargetChange = targetModel.getChangeModel(); + + var tasks = [ + getLocalChanges, + getDiffFromTarget, + createSourceUpdates, + bulkUpdate, + checkpoint + ]; + + async.waterfall(tasks, function(err) { + if(err) return callback(err); + var conflicts = diff.conflicts.map(function(change) { + var sourceChange = new Change({ + modelName: sourceModel.modelName, + modelId: change.modelId + }); + var targetChange = new TargetChange(change); + return new Change.Conflict(sourceChange, targetChange); + }); + + callback(null, conflicts); + }); + + function getLocalChanges(cb) { + sourceModel.changes(since, options.filter, cb); + } + + function getDiffFromTarget(sourceChanges, cb) { + targetModel.diff(since, sourceChanges, cb); + } + + function createSourceUpdates(_diff, cb) { + diff = _diff; + diff.conflicts = diff.conflicts || []; + sourceModel.createUpdates(diff.deltas, cb); + } + + function bulkUpdate(updates, cb) { + targetModel.bulkUpdate(updates, cb); + } + + function checkpoint() { + var cb = arguments[arguments.length - 1]; + sourceModel.checkpoint(cb); + } +} + +/** + * Create an update list (for `Model.bulkUpdate()`) from a delta list + * (result of `Change.diff()`). + * + * @param {Array} deltas + * @param {Function} callback + */ + +Model.createUpdates = function(deltas, cb) { + var Change = this.getChangeModel(); + var updates = []; + var Model = this; + var tasks = []; + + deltas.forEach(function(change) { + var change = new Change(change); + var type = change.type(); + var update = {type: type, change: change}; + switch(type) { + case Change.CREATE: + case Change.UPDATE: + tasks.push(function(cb) { + Model.findById(change.modelId, function(err, inst) { + if(err) return cb(err); + if(inst.toObject) { + update.data = inst.toObject(); + } else { + update.data = inst; + } + updates.push(update); + cb(); + }); + }); + break; + case Change.DELETE: + updates.push(update); + break; + } + }); + + async.parallel(tasks, function(err) { + if(err) return cb(err); + cb(null, updates); + }); +} + +/** + * Apply an update list. + * + * **Note: this is not atomic** + * + * @param {Array} updates An updates list (usually from Model.createUpdates()) + * @param {Function} callback + */ + +Model.bulkUpdate = function(updates, callback) { + var tasks = []; + var Model = this; + var idName = this.dataSource.idName(this.modelName); + var Change = this.getChangeModel(); + + updates.forEach(function(update) { + switch(update.type) { + case Change.UPDATE: + case Change.CREATE: + // var model = new Model(update.data); + // tasks.push(model.save.bind(model)); + tasks.push(function(cb) { + var model = new Model(update.data); + debugger; + model.save(cb); + }); + break; + case Change.DELETE: + var data = {}; + data[idName] = update.change.modelId; + var model = new Model(data); + tasks.push(model.destroy.bind(model)); + break; + } + }); + + async.parallel(tasks, callback); +} + +/** + * Get the `Change` model. + * + * @return {Change} + */ + +Model.getChangeModel = function() { + var changeModel = this.Change; + if(changeModel) return changeModel; + this.Change = changeModel = require('./change').extend(this.modelName + '-change'); + changeModel.attachTo(this.dataSource); + return changeModel; +} + +/** + * Get the source identifier for this model / dataSource. + * + * @callback {Function} callback + * @param {Error} err + * @param {String} sourceId + */ + +Model.getSourceId = function(cb) { + var dataSource = this.dataSource; + if(!dataSource) { + this.once('dataSourceAttached', this.getSourceId.bind(this, cb)); + } + assert( + dataSource.connector.name, + 'Model.getSourceId: cannot get id without dataSource.connector.name' + ); + var id = [dataSource.connector.name, this.modelName].join('-'); + cb(null, id); +} + +/** + * Enable the tracking of changes made to the model. Usually for replication. + */ + +Model.enableChangeTracking = function() { + var Model = this; + var Change = Model.getChangeModel(); + var cleanupInterval = Model.settings.changeCleanupInterval || 30000; + + Model.on('changed', function(obj) { + Change.track(Model.modelName, [obj.id], function(err) { + if(err) { + console.error(Model.modelName + ' Change Tracking Error:'); + console.error(err); + } + }); + }); + + Model.on('deleted', function(obj) { + Change.track(Model.modelName, [obj.id], function(err) { + if(err) { + console.error(Model.modelName + ' Change Tracking Error:'); + console.error(err); + } + }); + }); + + Model.on('deletedAll', cleanup); + + // initial cleanup + cleanup(); + + // cleanup + setInterval(cleanup, cleanupInterval); + + function cleanup() { + Change.rectifyAll(function(err) { + if(err) { + console.error(Model.modelName + ' Change Cleanup Error:'); + console.error(err); + } + }); + } +} + +},{"../loopback":7,"./access-token":9,"./acl":10,"./change":12,"assert":21,"async":18,"loopback-datasource-juggler":62}],16:[function(require,module,exports){ +var process=require("__browserify_process");var loopback = require('../loopback'); +var debug = require('debug')('loopback:security:role'); +var assert = require('assert'); +var async = require('async'); + +var AccessContext = require('./access-context').AccessContext; + +// Role model +var RoleSchema = { + id: {type: String, id: true}, // Id + name: {type: String, required: true}, // The name of a role + description: String, // Description + + // Timestamps + created: {type: Date, default: Date}, + modified: {type: Date, default: Date} +}; + +/** + * Map principals to roles + */ +var RoleMappingSchema = { + id: {type: String, id: true}, // Id + roleId: String, // The role id + principalType: String, // The principal type, such as user, application, or role + principalId: String // The principal id +}; + +/** + * Map Roles to + */ + +var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, { + relations: { + role: { + type: 'belongsTo', + model: 'Role', + foreignKey: 'roleId' + } + } +}); + +// Principal types +RoleMapping.USER = 'USER'; +RoleMapping.APP = RoleMapping.APPLICATION = 'APP'; +RoleMapping.ROLE = 'ROLE'; + +/** + * Get the application principal + * @callback {Function} callback + * @param {Error} err + * @param {Application} application + */ +RoleMapping.prototype.application = function (callback) { + if (this.principalType === RoleMapping.APPLICATION) { + var applicationModel = this.constructor.Application + || loopback.getModelByType(loopback.Application); + applicationModel.findById(this.principalId, callback); + } else { + process.nextTick(function () { + callback && callback(null, null); + }); + } +}; + +/** + * Get the user principal + * @callback {Function} callback + * @param {Error} err + * @param {User} user + */ +RoleMapping.prototype.user = function (callback) { + if (this.principalType === RoleMapping.USER) { + var userModel = this.constructor.User + || loopback.getModelByType(loopback.User); + userModel.findById(this.principalId, callback); + } else { + process.nextTick(function () { + callback && callback(null, null); + }); + } +}; + +/** + * Get the child role principal + * @callback {Function} callback + * @param {Error} err + * @param {User} childUser + */ +RoleMapping.prototype.childRole = function (callback) { + if (this.principalType === RoleMapping.ROLE) { + var roleModel = this.constructor.Role || loopback.getModelByType(Role); + roleModel.findById(this.principalId, callback); + } else { + process.nextTick(function () { + callback && callback(null, null); + }); + } +}; + +/** + * The Role Model + * @class + */ +var Role = loopback.createModel('Role', RoleSchema, { + relations: { + principals: { + type: 'hasMany', + model: 'RoleMapping', + foreignKey: 'roleId' + } + } +}); + +// Set up the connection to users/applications/roles once the model +Role.once('dataSourceAttached', function () { + var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping); + Role.prototype.users = function (callback) { + roleMappingModel.find({where: {roleId: this.id, + principalType: RoleMapping.USER}}, function (err, mappings) { + if (err) { + callback && callback(err); + return; + } + return mappings.map(function (m) { + return m.principalId; + }); + }); + }; + + Role.prototype.applications = function (callback) { + roleMappingModel.find({where: {roleId: this.id, + principalType: RoleMapping.APPLICATION}}, function (err, mappings) { + if (err) { + callback && callback(err); + return; + } + return mappings.map(function (m) { + return m.principalId; + }); + }); + }; + + Role.prototype.roles = function (callback) { + roleMappingModel.find({where: {roleId: this.id, + principalType: RoleMapping.ROLE}}, function (err, mappings) { + if (err) { + callback && callback(err); + return; + } + return mappings.map(function (m) { + return m.principalId; + }); + }); + }; + +}); + +// Special roles +Role.OWNER = '$owner'; // owner of the object +Role.RELATED = "$related"; // any User with a relationship to the object +Role.AUTHENTICATED = "$authenticated"; // authenticated user +Role.UNAUTHENTICATED = "$unauthenticated"; // authenticated user +Role.EVERYONE = "$everyone"; // everyone + +/** + * Add custom handler for roles + * @param role + * @param resolver The resolver function decides if a principal is in the role + * dynamically + * + * function(role, context, callback) + */ +Role.registerResolver = function(role, resolver) { + if(!Role.resolvers) { + Role.resolvers = {}; + } + Role.resolvers[role] = resolver; +}; + +Role.registerResolver(Role.OWNER, function(role, context, callback) { + if(!context || !context.model || !context.modelId) { + process.nextTick(function() { + callback && callback(null, false); + }); + return; + } + var modelClass = context.model; + var modelId = context.modelId; + var userId = context.getUserId(); + Role.isOwner(modelClass, modelId, userId, callback); +}); + +function isUserClass(modelClass) { + return modelClass === loopback.User || + modelClass.prototype instanceof loopback.User; +} + +/** + * Check if a given userId is the owner the model instance + * @param {Function} modelClass The model class + * @param {*} modelId The model id + * @param {*) userId The user id + * @param {Function} callback + */ +Role.isOwner = function isOwner(modelClass, modelId, userId, callback) { + assert(modelClass, 'Model class is required'); + debug('isOwner(): %s %s %s', modelClass && modelClass.modelName, modelId, userId); + // No userId is present + if(!userId) { + process.nextTick(function() { + callback(null, false); + }); + return; + } + + // Is the modelClass User or a subclass of User? + if(isUserClass(modelClass)) { + process.nextTick(function() { + callback(null, modelId == userId); + }); + return; + } + + modelClass.findById(modelId, function(err, inst) { + if(err || !inst) { + callback && callback(err, false); + return; + } + debug('Model found: %j', inst); + if(inst.userId || inst.owner) { + callback && callback(null, (inst.userId || inst.owner) === userId); + return; + } else { + // Try to follow belongsTo + for(var r in modelClass.relations) { + var rel = modelClass.relations[r]; + if(rel.type === 'belongsTo' && isUserClass(rel.modelTo)) { + debug('Checking relation %s to %s: %j', r, rel.modelTo.modelName, rel); + inst[r](function(err, user) { + if(!err && user) { + debug('User found: %j', user.id); + callback && callback(null, user.id === userId); + } else { + callback && callback(err, false); + } + }); + return; + } + } + callback && callback(null, false); + } + }); +}; + +Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) { + if(!context) { + process.nextTick(function() { + callback && callback(null, false); + }); + return; + } + Role.isAuthenticated(context, callback); +}); + +/** + * Check if the user id is authenticated + * @param {Object} context The security context + * @callback {Function} callback + * @param {Error} err + * @param {Boolean} isAuthenticated + */ +Role.isAuthenticated = function isAuthenticated(context, callback) { + process.nextTick(function() { + callback && callback(null, context.isAuthenticated()); + }); +}; + +Role.registerResolver(Role.UNAUTHENTICATED, function(role, context, callback) { + process.nextTick(function() { + callback && callback(null, !context || !context.isAuthenticated()); + }); +}); + +Role.registerResolver(Role.EVERYONE, function (role, context, callback) { + process.nextTick(function () { + callback && callback(null, true); // Always true + }); +}); + +/** + * Check if a given principal is in the role + * + * @param {String} role The role name + * @param {Object} context The context object + * @callback {Function} callback + * @param {Error} err + * @param {Boolean} isInRole + */ +Role.isInRole = function (role, context, callback) { + debug('isInRole(): %s %j', role, context); + + if (!(context instanceof AccessContext)) { + context = new AccessContext(context); + } + + var resolver = Role.resolvers[role]; + if (resolver) { + debug('Custom resolver found for role %s', role); + resolver(role, context, callback); + return; + } + + if (context.principals.length === 0) { + debug('isInRole() returns: false'); + process.nextTick(function () { + callback && callback(null, false); + }); + return; + } + + var inRole = context.principals.some(function (p) { + + var principalType = p.type || undefined; + var principalId = p.id || undefined; + + // Check if it's the same role + return principalType === RoleMapping.ROLE && principalId === role; + }); + + if (inRole) { + debug('isInRole() returns: %j', inRole); + process.nextTick(function () { + callback && callback(null, true); + }); + return; + } + + var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping); + this.findOne({where: {name: role}}, function (err, result) { + if (err) { + callback && callback(err); + return; + } + if (!result) { + callback && callback(null, false); + return; + } + debug('Role found: %j', result); + + // Iterate through the list of principals + async.some(context.principals, function (p, done) { + var principalType = p.type || undefined; + var principalId = p.id || undefined; + if (principalType && principalId) { + roleMappingModel.findOne({where: {roleId: result.id, + principalType: principalType, principalId: principalId}}, + function (err, result) { + debug('Role mapping found: %j', result); + done(!err && result); // The only arg is the result + }); + } else { + process.nextTick(function () { + done(false); + }); + } + }, function (inRole) { + debug('isInRole() returns: %j', inRole); + callback && callback(null, inRole); + }); + }); + +}; + +/** + * List roles for a given principal + * @param {Object} context The security context + * @param {Function} callback + * + * @callback {Function} callback + * @param err + * @param {String[]} An array of role ids + */ +Role.getRoles = function (context, callback) { + debug('getRoles(): %j', context); + + if(!(context instanceof AccessContext)) { + context = new AccessContext(context); + } + var roles = []; + + var addRole = function (role) { + if (role && roles.indexOf(role) === -1) { + roles.push(role); + } + }; + + var self = this; + // Check against the smart roles + var inRoleTasks = []; + Object.keys(Role.resolvers).forEach(function (role) { + inRoleTasks.push(function (done) { + self.isInRole(role, context, function (err, inRole) { + if (!err && inRole) { + addRole(role); + done(); + } else { + done(err, null); + } + }); + }); + }); + + var roleMappingModel = this.RoleMapping || loopback.getModelByType(RoleMapping); + context.principals.forEach(function (p) { + // Check against the role mappings + var principalType = p.type || undefined; + var principalId = p.id || undefined; + + // Add the role itself + if (principalType === RoleMapping.ROLE && principalId) { + addRole(principalId); + } + + if (principalType && principalId) { + // Please find() treat undefined matches all values + inRoleTasks.push(function (done) { + roleMappingModel.find({where: {principalType: principalType, + principalId: principalId}}, function (err, mappings) { + debug('Role mappings found: %s %j', err, mappings); + if (err) { + done && done(err); + return; + } + mappings.forEach(function (m) { + addRole(m.roleId); + }); + done && done(); + }); + }); + } + }); + + async.parallel(inRoleTasks, function (err, results) { + debug('getRoles() returns: %j %j', err, roles); + callback && callback(err, roles); + }); +}; + +module.exports = { + Role: Role, + RoleMapping: RoleMapping +}; + + + + +},{"../loopback":7,"./access-context":8,"__browserify_process":36,"assert":21,"async":18,"debug":57}],17:[function(require,module,exports){ +var __dirname="/lib/models";/** + * Module Dependencies. + */ + +var Model = require('../loopback').Model + , loopback = require('../loopback') + , path = require('path') + , SALT_WORK_FACTOR = 10 + , crypto = require('crypto') + , bcrypt = require('bcryptjs') + , passport = require('passport') + , LocalStrategy = require('passport-local').Strategy + , BaseAccessToken = require('./access-token') + , DEFAULT_TTL = 1209600 // 2 weeks in seconds + , DEFAULT_RESET_PW_TTL = 15 * 60 // 15 mins in seconds + , DEFAULT_MAX_TTL = 31556926 // 1 year in seconds + , Role = require('./role').Role + , ACL = require('./acl').ACL + , assert = require('assert'); + +/** + * Default User properties. + */ + +var properties = { + realm: {type: String}, + username: {type: String}, + password: {type: String, required: true}, + email: {type: String, required: true}, + emailVerified: Boolean, + verificationToken: String, + + credentials: [ + 'UserCredential' // User credentials, private or public, such as private/public keys, Kerberos tickets, oAuth tokens, facebook, google, github ids + ], + challenges: [ + 'Challenge' // Security questions/answers + ], + // https://en.wikipedia.org/wiki/Multi-factor_authentication + /* + factors: [ + 'AuthenticationFactor' + ], + */ + status: String, + created: Date, + lastUpdated: Date +}; + +var options = { + acls: [ + { + principalType: ACL.ROLE, + principalId: Role.EVERYONE, + permission: ACL.DENY, + }, + { + principalType: ACL.ROLE, + principalId: Role.EVERYONE, + permission: ACL.ALLOW, + property: 'create' + }, + { + principalType: ACL.ROLE, + principalId: Role.OWNER, + permission: ACL.ALLOW, + property: 'removeById' + }, + { + principalType: ACL.ROLE, + principalId: Role.EVERYONE, + permission: ACL.ALLOW, + property: "login" + }, + { + principalType: ACL.ROLE, + principalId: Role.EVERYONE, + permission: ACL.ALLOW, + property: "logout" + }, + { + principalType: ACL.ROLE, + principalId: Role.OWNER, + permission: ACL.ALLOW, + property: "findById" + }, + { + principalType: ACL.ROLE, + principalId: Role.OWNER, + permission: ACL.ALLOW, + property: "updateAttributes" + } + ] +}; + +/** + * Extends from the built in `loopback.Model` type. + * + * Default `User` ACLs. + * + * - DENY EVERYONE `*` + * - ALLOW EVERYONE `create` + * - ALLOW OWNER `removeById` + * - ALLOW EVERYONE `login` + * - ALLOW EVERYONE `logout` + * - ALLOW EVERYONE `findById` + * - ALLOW OWNER `updateAttributes` + * + * @class + * @inherits {Model} + */ + +var User = module.exports = Model.extend('User', properties, options); + +/** + * Login a user by with the given `credentials`. + * + * ```js + * User.login({username: 'foo', password: 'bar'}, function (err, token) { + * console.log(token.id); + * }); + * ``` + * + * @param {Object} credentials + * @callback {Function} callback + * @param {Error} err + * @param {AccessToken} token + */ + +User.login = function (credentials, fn) { + var UserCtor = this; + var query = {}; + + if(credentials.email) { + query.email = credentials.email; + } else if(credentials.username) { + query.username = credentials.username; + } else { + return fn(new Error('must provide username or email')); + } + + this.findOne({where: query}, function(err, user) { + var defaultError = new Error('login failed'); + + if(err) { + fn(defaultError); + } else if(user) { + user.hasPassword(credentials.password, function(err, isMatch) { + if(err) { + fn(defaultError); + } else if(isMatch) { + user.accessTokens.create({ + ttl: Math.min(credentials.ttl || User.settings.ttl, User.settings.maxTTL) + }, fn); + } else { + fn(defaultError); + } + }); + } else { + fn(defaultError); + } + }); +} + +/** + * Logout a user with the given accessToken id. + * + * ```js + * User.logout('asd0a9f8dsj9s0s3223mk', function (err) { + * console.log(err || 'Logged out'); + * }); + * ``` + * + * @param {String} accessTokenID + * @callback {Function} callback + * @param {Error} err + */ + +User.logout = function (tokenId, fn) { + this.relations.accessTokens.modelTo.findById(tokenId, function (err, accessToken) { + if(err) { + fn(err); + } else if(accessToken) { + accessToken.destroy(fn); + } else { + fn(new Error('could not find accessToken')); + } + }); +} + +/** + * Compare the given `password` with the users hashed password. + * + * @param {String} password The plain text password + * @returns {Boolean} + */ + +User.prototype.hasPassword = function (plain, fn) { + if(this.password && plain) { + bcrypt.compare(plain, this.password, function(err, isMatch) { + if(err) return fn(err); + fn(null, isMatch); + }); + } else { + fn(null, false); + } +} + +/** + * Verify a user's identity by sending them a confirmation email. + * + * ```js + * var options = { + * type: 'email', + * to: user.email, + * template: 'verify.ejs', + * redirect: '/' + * }; + * + * user.verify(options, next); + * ``` + * + * @param {Object} options + */ + +User.prototype.verify = function (options, fn) { + var user = this; + assert(typeof options === 'object', 'options required when calling user.verify()'); + assert(options.type, 'You must supply a verification type (options.type)'); + assert(options.type === 'email', 'Unsupported verification type'); + assert(options.to || this.email, 'Must include options.to when calling user.verify() or the user must have an email property'); + assert(options.from, 'Must include options.from when calling user.verify() or the user must have an email property'); + + options.redirect = options.redirect || '/'; + options.template = path.resolve(options.template || path.join(__dirname, '..', '..', 'templates', 'verify.ejs')); + options.user = this; + options.protocol = options.protocol || 'http'; + options.host = options.host || 'localhost'; + options.verifyHref = options.verifyHref || + options.protocol + + '://' + + options.host + + (User.sharedCtor.http.path || '/' + User.pluralModelName) + + User.confirm.http.path; + + + + // Email model + var Email = options.mailer || this.constructor.email || loopback.getModelByType(loopback.Email); + + crypto.randomBytes(64, function(err, buf) { + if(err) { + fn(err); + } else { + user.verificationToken = buf.toString('base64'); + user.save(function (err) { + if(err) { + fn(err); + } else { + sendEmail(user); + } + }); + } + }); + + // TODO - support more verification types + function sendEmail(user) { + options.verifyHref += '?token=' + user.verificationToken; + + options.text = options.text || 'Please verify your email by opening this link in a web browser:\n\t{href}'; + + options.text = options.text.replace('{href}', options.verifyHref); + + var template = loopback.template(options.template); + Email.send({ + to: options.to || user.email, + subject: options.subject || 'Thanks for Registering', + text: options.text, + html: template(options) + }, function (err, email) { + if(err) { + fn(err); + } else { + fn(null, {email: email, token: user.verificationToken, uid: user.id}); + } + }); + } +} + + +/** + * Confirm the user's identity. + * + * @param {Any} userId + * @param {String} token The validation token + * @param {String} redirect URL to redirect the user to once confirmed + * @callback {Function} callback + * @param {Error} err + */ +User.confirm = function (uid, token, redirect, fn) { + this.findById(uid, function (err, user) { + if(err) { + fn(err); + } else { + if(user.verificationToken === token) { + user.verificationToken = undefined; + user.emailVerified = true; + user.save(function (err) { + if(err) { + fn(err) + } else { + fn(); + } + }); + } else { + fn(new Error('invalid token')); + } + } + }); +} + +/** + * Create a short lived acess token for temporary login. Allows users + * to change passwords if forgotten. + * + * @options {Object} options + * @prop {String} email The user's email address + * @callback {Function} callback + * @param {Error} err + */ + +User.resetPassword = function(options, cb) { + var UserModel = this; + var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL; + + options = options || {}; + if(typeof options.email === 'string') { + UserModel.findOne({email: options.email}, function(err, user) { + if(err) { + cb(err); + } else if(user) { + // create a short lived access token for temp login to change password + // TODO(ritch) - eventually this should only allow password change + user.accessTokens.create({ttl: ttl}, function(err, accessToken) { + if(err) { + cb(err); + } else { + cb(); + UserModel.emit('resetPasswordRequest', { + email: options.email, + accessToken: accessToken + }); + } + }) + } else { + cb(); + } + }); + } else { + var err = new Error('email is required'); + err.statusCode = 400; + + cb(err); + } +} + +/*! + * Setup an extended user model. + */ + +User.setup = function () { + // We need to call the base class's setup method + Model.setup.call(this); + var UserModel = this; + + // max ttl + this.settings.maxTTL = this.settings.maxTTL || DEFAULT_MAX_TTL; + this.settings.ttl = DEFAULT_TTL; + + UserModel.setter.password = function (plain) { + var salt = bcrypt.genSaltSync(this.constructor.settings.saltWorkFactor || SALT_WORK_FACTOR); + this.$password = bcrypt.hashSync(plain, salt); + } + + loopback.remoteMethod( + UserModel.login, + { + accepts: [ + {arg: 'credentials', type: 'object', required: true, http: {source: 'body'}} + ], + returns: {arg: 'accessToken', type: 'object', root: true}, + http: {verb: 'post'} + } + ); + + loopback.remoteMethod( + UserModel.logout, + { + accepts: [ + {arg: 'access_token', type: 'string', required: true, http: function(ctx) { + var req = ctx && ctx.req; + var accessToken = req && req.accessToken; + var tokenID = accessToken && accessToken.id; + + return tokenID; + }} + ], + http: {verb: 'all'} + } + ); + + loopback.remoteMethod( + UserModel.confirm, + { + accepts: [ + {arg: 'uid', type: 'string', required: true}, + {arg: 'token', type: 'string', required: true}, + {arg: 'redirect', type: 'string', required: true} + ], + http: {verb: 'get', path: '/confirm'} + } + ); + + loopback.remoteMethod( + UserModel.resetPassword, + { + accepts: [ + {arg: 'options', type: 'object', required: true, http: {source: 'body'}} + ], + http: {verb: 'post', path: '/reset'} + } + ); + + UserModel.on('attached', function () { + UserModel.afterRemote('confirm', function (ctx, inst, next) { + if(ctx.req) { + ctx.res.redirect(ctx.req.param('redirect')); + } else { + fn(new Error('transport unsupported')); + } + }); + }); + + // default models + UserModel.email = require('./email'); + UserModel.accessToken = require('./access-token'); + + UserModel.validatesUniquenessOf('email', {message: 'Email already exists'}); + var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + + UserModel.validatesFormatOf('email', {with: re, message: 'Must provide a valid email'}); + + return UserModel; +} + +/*! + * Setup the base user. + */ + +User.setup(); + +},{"../loopback":7,"./access-token":9,"./acl":10,"./email":14,"./role":16,"assert":21,"bcryptjs":19,"crypto":24,"passport":20,"passport-local":22,"path":40}],18:[function(require,module,exports){ +var process=require("__browserify_process");/*global setImmediate: false, setTimeout: false, console: false */ +(function () { + + var async = {}; + + // global on the server, window in the browser + var root, previous_async; + + root = this; + if (root != null) { + previous_async = root.async; + } + + async.noConflict = function () { + root.async = previous_async; + return async; + }; + + function only_once(fn) { + var called = false; + return function() { + if (called) throw new Error("Callback was already called."); + called = true; + fn.apply(root, arguments); + } + } + + //// cross-browser compatiblity functions //// + + var _each = function (arr, iterator) { + if (arr.forEach) { + return arr.forEach(iterator); + } + for (var i = 0; i < arr.length; i += 1) { + iterator(arr[i], i, arr); + } + }; + + var _map = function (arr, iterator) { + if (arr.map) { + return arr.map(iterator); + } + var results = []; + _each(arr, function (x, i, a) { + results.push(iterator(x, i, a)); + }); + return results; + }; + + var _reduce = function (arr, iterator, memo) { + if (arr.reduce) { + return arr.reduce(iterator, memo); + } + _each(arr, function (x, i, a) { + memo = iterator(memo, x, i, a); + }); + return memo; + }; + + var _keys = function (obj) { + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + keys.push(k); + } + } + return keys; + }; + + //// exported async module functions //// + + //// nextTick implementation with browser-compatible fallback //// + if (typeof process === 'undefined' || !(process.nextTick)) { + if (typeof setImmediate === 'function') { + async.nextTick = function (fn) { + // not a direct alias for IE10 compatibility + setImmediate(fn); + }; + async.setImmediate = async.nextTick; + } + else { + async.nextTick = function (fn) { + setTimeout(fn, 0); + }; + async.setImmediate = async.nextTick; + } + } + else { + async.nextTick = process.nextTick; + if (typeof setImmediate !== 'undefined') { + async.setImmediate = setImmediate; + } + else { + async.setImmediate = async.nextTick; + } + } + + async.each = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + _each(arr, function (x) { + iterator(x, only_once(function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(null); + } + } + })); + }); + }; + async.forEach = async.each; + + async.eachSeries = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + var iterate = function () { + iterator(arr[completed], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(null); + } + else { + iterate(); + } + } + }); + }; + iterate(); + }; + async.forEachSeries = async.eachSeries; + + async.eachLimit = function (arr, limit, iterator, callback) { + var fn = _eachLimit(limit); + fn.apply(null, [arr, iterator, callback]); + }; + async.forEachLimit = async.eachLimit; + + var _eachLimit = function (limit) { + + return function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length || limit <= 0) { + return callback(); + } + var completed = 0; + var started = 0; + var running = 0; + + (function replenish () { + if (completed >= arr.length) { + return callback(); + } + + while (running < limit && started < arr.length) { + started += 1; + running += 1; + iterator(arr[started - 1], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + running -= 1; + if (completed >= arr.length) { + callback(); + } + else { + replenish(); + } + } + }); + } + })(); + }; + }; + + + var doParallel = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.each].concat(args)); + }; + }; + var doParallelLimit = function(limit, fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [_eachLimit(limit)].concat(args)); + }; + }; + var doSeries = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.eachSeries].concat(args)); + }; + }; + + + var _asyncMap = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (err, v) { + results[x.index] = v; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + }; + async.map = doParallel(_asyncMap); + async.mapSeries = doSeries(_asyncMap); + async.mapLimit = function (arr, limit, iterator, callback) { + return _mapLimit(limit)(arr, iterator, callback); + }; + + var _mapLimit = function(limit) { + return doParallelLimit(limit, _asyncMap); + }; + + // reduce only has a series version, as doing reduce in parallel won't + // work in many situations. + async.reduce = function (arr, memo, iterator, callback) { + async.eachSeries(arr, function (x, callback) { + iterator(memo, x, function (err, v) { + memo = v; + callback(err); + }); + }, function (err) { + callback(err, memo); + }); + }; + // inject alias + async.inject = async.reduce; + // foldl alias + async.foldl = async.reduce; + + async.reduceRight = function (arr, memo, iterator, callback) { + var reversed = _map(arr, function (x) { + return x; + }).reverse(); + async.reduce(reversed, memo, iterator, callback); + }; + // foldr alias + async.foldr = async.reduceRight; + + var _filter = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.filter = doParallel(_filter); + async.filterSeries = doSeries(_filter); + // select alias + async.select = async.filter; + async.selectSeries = async.filterSeries; + + var _reject = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (!v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.reject = doParallel(_reject); + async.rejectSeries = doSeries(_reject); + + var _detect = function (eachfn, arr, iterator, main_callback) { + eachfn(arr, function (x, callback) { + iterator(x, function (result) { + if (result) { + main_callback(x); + main_callback = function () {}; + } + else { + callback(); + } + }); + }, function (err) { + main_callback(); + }); + }; + async.detect = doParallel(_detect); + async.detectSeries = doSeries(_detect); + + async.some = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (v) { + main_callback(true); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(false); + }); + }; + // any alias + async.any = async.some; + + async.every = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (!v) { + main_callback(false); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(true); + }); + }; + // all alias + async.all = async.every; + + async.sortBy = function (arr, iterator, callback) { + async.map(arr, function (x, callback) { + iterator(x, function (err, criteria) { + if (err) { + callback(err); + } + else { + callback(null, {value: x, criteria: criteria}); + } + }); + }, function (err, results) { + if (err) { + return callback(err); + } + else { + var fn = function (left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }; + callback(null, _map(results.sort(fn), function (x) { + return x.value; + })); + } + }); + }; + + async.auto = function (tasks, callback) { + callback = callback || function () {}; + var keys = _keys(tasks); + if (!keys.length) { + return callback(null); + } + + var results = {}; + + var listeners = []; + var addListener = function (fn) { + listeners.unshift(fn); + }; + var removeListener = function (fn) { + for (var i = 0; i < listeners.length; i += 1) { + if (listeners[i] === fn) { + listeners.splice(i, 1); + return; + } + } + }; + var taskComplete = function () { + _each(listeners.slice(0), function (fn) { + fn(); + }); + }; + + addListener(function () { + if (_keys(results).length === keys.length) { + callback(null, results); + callback = function () {}; + } + }); + + _each(keys, function (k) { + var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k]; + var taskCallback = function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + if (err) { + var safeResults = {}; + _each(_keys(results), function(rkey) { + safeResults[rkey] = results[rkey]; + }); + safeResults[k] = args; + callback(err, safeResults); + // stop subsequent errors hitting callback multiple times + callback = function () {}; + } + else { + results[k] = args; + async.setImmediate(taskComplete); + } + }; + var requires = task.slice(0, Math.abs(task.length - 1)) || []; + var ready = function () { + return _reduce(requires, function (a, x) { + return (a && results.hasOwnProperty(x)); + }, true) && !results.hasOwnProperty(k); + }; + if (ready()) { + task[task.length - 1](taskCallback, results); + } + else { + var listener = function () { + if (ready()) { + removeListener(listener); + task[task.length - 1](taskCallback, results); + } + }; + addListener(listener); + } + }); + }; + + async.waterfall = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor !== Array) { + var err = new Error('First argument to waterfall must be an array of functions'); + return callback(err); + } + if (!tasks.length) { + return callback(); + } + var wrapIterator = function (iterator) { + return function (err) { + if (err) { + callback.apply(null, arguments); + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } + else { + args.push(callback); + } + async.setImmediate(function () { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(async.iterator(tasks))(); + }; + + var _parallel = function(eachfn, tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + eachfn.map(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + eachfn.each(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.parallel = function (tasks, callback) { + _parallel({ map: async.map, each: async.each }, tasks, callback); + }; + + async.parallelLimit = function(tasks, limit, callback) { + _parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback); + }; + + async.series = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + async.mapSeries(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.eachSeries(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.iterator = function (tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1): null; + }; + return fn; + }; + return makeCallback(0); + }; + + async.apply = function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + return function () { + return fn.apply( + null, args.concat(Array.prototype.slice.call(arguments)) + ); + }; + }; + + var _concat = function (eachfn, arr, fn, callback) { + var r = []; + eachfn(arr, function (x, cb) { + fn(x, function (err, y) { + r = r.concat(y || []); + cb(err); + }); + }, function (err) { + callback(err, r); + }); + }; + async.concat = doParallel(_concat); + async.concatSeries = doSeries(_concat); + + async.whilst = function (test, iterator, callback) { + if (test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.whilst(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doWhilst = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + if (test()) { + async.doWhilst(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.until = function (test, iterator, callback) { + if (!test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.until(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doUntil = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + if (!test()) { + async.doUntil(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.queue = function (worker, concurrency) { + if (concurrency === undefined) { + concurrency = 1; + } + function _insert(q, data, pos, callback) { + if(data.constructor !== Array) { + data = [data]; + } + _each(data, function(task) { + var item = { + data: task, + callback: typeof callback === 'function' ? callback : null + }; + + if (pos) { + q.tasks.unshift(item); + } else { + q.tasks.push(item); + } + + if (q.saturated && q.tasks.length === concurrency) { + q.saturated(); + } + async.setImmediate(q.process); + }); + } + + var workers = 0; + var q = { + tasks: [], + concurrency: concurrency, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + _insert(q, data, false, callback); + }, + unshift: function (data, callback) { + _insert(q, data, true, callback); + }, + process: function () { + if (workers < q.concurrency && q.tasks.length) { + var task = q.tasks.shift(); + if (q.empty && q.tasks.length === 0) { + q.empty(); + } + workers += 1; + var next = function () { + workers -= 1; + if (task.callback) { + task.callback.apply(task, arguments); + } + if (q.drain && q.tasks.length + workers === 0) { + q.drain(); + } + q.process(); + }; + var cb = only_once(next); + worker(task.data, cb); + } + }, + length: function () { + return q.tasks.length; + }, + running: function () { + return workers; + } + }; + return q; + }; + + async.cargo = function (worker, payload) { + var working = false, + tasks = []; + + var cargo = { + tasks: tasks, + payload: payload, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + if(data.constructor !== Array) { + data = [data]; + } + _each(data, function(task) { + tasks.push({ + data: task, + callback: typeof callback === 'function' ? callback : null + }); + if (cargo.saturated && tasks.length === payload) { + cargo.saturated(); + } + }); + async.setImmediate(cargo.process); + }, + process: function process() { + if (working) return; + if (tasks.length === 0) { + if(cargo.drain) cargo.drain(); + return; + } + + var ts = typeof payload === 'number' + ? tasks.splice(0, payload) + : tasks.splice(0); + + var ds = _map(ts, function (task) { + return task.data; + }); + + if(cargo.empty) cargo.empty(); + working = true; + worker(ds, function () { + working = false; + + var args = arguments; + _each(ts, function (data) { + if (data.callback) { + data.callback.apply(null, args); + } + }); + + process(); + }); + }, + length: function () { + return tasks.length; + }, + running: function () { + return working; + } + }; + return cargo; + }; + + var _console_fn = function (name) { + return function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + fn.apply(null, args.concat([function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (typeof console !== 'undefined') { + if (err) { + if (console.error) { + console.error(err); + } + } + else if (console[name]) { + _each(args, function (x) { + console[name](x); + }); + } + } + }])); + }; + }; + async.log = _console_fn('log'); + async.dir = _console_fn('dir'); + /*async.info = _console_fn('info'); + async.warn = _console_fn('warn'); + async.error = _console_fn('error');*/ + + async.memoize = function (fn, hasher) { + var memo = {}; + var queues = {}; + hasher = hasher || function (x) { + return x; + }; + var memoized = function () { + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + var key = hasher.apply(null, args); + if (key in memo) { + callback.apply(null, memo[key]); + } + else if (key in queues) { + queues[key].push(callback); + } + else { + queues[key] = [callback]; + fn.apply(null, args.concat([function () { + memo[key] = arguments; + var q = queues[key]; + delete queues[key]; + for (var i = 0, l = q.length; i < l; i++) { + q[i].apply(null, arguments); + } + }])); + } + }; + memoized.memo = memo; + memoized.unmemoized = fn; + return memoized; + }; + + async.unmemoize = function (fn) { + return function () { + return (fn.unmemoized || fn).apply(null, arguments); + }; + }; + + async.times = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.map(counter, iterator, callback); + }; + + async.timesSeries = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.mapSeries(counter, iterator, callback); + }; + + async.compose = function (/* functions... */) { + var fns = Array.prototype.reverse.call(arguments); + return function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + async.reduce(fns, args, function (newargs, fn, cb) { + fn.apply(that, newargs.concat([function () { + var err = arguments[0]; + var nextargs = Array.prototype.slice.call(arguments, 1); + cb(err, nextargs); + }])) + }, + function (err, results) { + callback.apply(that, [err].concat(results)); + }); + }; + }; + + var _applyEach = function (eachfn, fns /*args...*/) { + var go = function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + return eachfn(fns, function (fn, cb) { + fn.apply(that, args.concat([cb])); + }, + callback); + }; + if (arguments.length > 2) { + var args = Array.prototype.slice.call(arguments, 2); + return go.apply(this, args); + } + else { + return go; + } + }; + async.applyEach = doParallel(_applyEach); + async.applyEachSeries = doSeries(_applyEach); + + async.forever = function (fn, callback) { + function next(err) { + if (err) { + if (callback) { + return callback(err); + } + throw err; + } + fn(next); + } + next(); + }; + + // AMD / RequireJS + if (typeof define !== 'undefined' && define.amd) { + define([], function () { + return async; + }); + } + // Node.js + else if (typeof module !== 'undefined' && module.exports) { + module.exports = async; + } + // included directly via