UNPKG

hunt

Version:

High level nodejs framework build on top of expressjs, mongoose, sequilize, socketio and passportjs

1,204 lines (1,138 loc) 38.2 kB
'use strict'; var util = require('util'), winston = require('winston'), fieldsReadable = [ '_id', 'id', 'accountVerified', 'lastSeenOnline', 'name', 'familyName', 'middleName', 'givenName', 'displayName', 'roles', 'root', 'isBanned', 'gravatar', 'gravatar30', 'gravatar50', 'gravatar80', 'gravatar100', '$subscribeToken', 'isOnline', 'lastSeenOnlineAgo', 'lastSeenOnline' ], fieldsReadableByRootOrSelf = [ '_id', 'id', 'accountVerified', 'keychain.username', 'keychain.email', 'keychain.google', 'keychain.github', 'keychain.twitter', 'keychain.facebook', 'keychain.vkontakte', 'keychain.steam', 'lastSeenOnline', 'name', 'familyName', 'middleName', 'givenName', 'displayName', 'roles', 'root', 'isBanned', 'profile', 'gravatar', 'gravatar30', 'gravatar50', 'gravatar80', 'gravatar100', '$subscribeToken', 'isOnline', 'lastSeenOnlineAgo', 'lastSeenOnline' ], fieldsWritable = [ 'displayName', 'familyName', 'middleName', 'givenName', 'password', 'email' ], fieldsWritableByRoot = [ 'name', 'displayName', 'roles', 'root', 'isBanned', 'familyName', 'middleName', 'givenName', 'profile', 'password', 'email', 'keychain' ]; /** * @class User * @classdesc * Active record class to represent users of application, with mongodb as backend. * This class is ancestor of mongoose active record object, described here {@link http://mongoosejs.com/docs/documents.html}, * {@link http://mongoosejs.com/docs/queries.html} * * @property {ObjectId} _id * @property {string} id * @property {Boolean} accountVerified * @property {string} salt * @property {string} password * @property {object} keychain - object to hold all authorization methods- oauth profile ids,emails, usernames... * @property {string} huntKey - unique key, used for authorization, invalidating sessions, account confirmation, api interactions... * @property {Date} huntKeyCreatedAt * @property {Date} lastSeenOnline * @property {Number} lastSeenOnlineAgo - how long ago, in seconds, user was online. * @property {Boolean} isOnline - true, if user was online less than 60 seconds * @property {string} displayName * @property {object} name -consists of 'title','familyName', 'middleName', 'givenName' * @property {Boolean} root - does user have root permissions? default is false - no * @property {Boolean} isBanned - is user banned? default is false - no * @property {object} roles - hash (indexed in mongodb) to store user's roles * @property {object} profile - free form object, that can store any structured data associated with user account * @property {string} email - E-mail address of user, unique. Part of `keychain` * @property {string} username - Username, unique. Part of `keychain` * @property {string} gravatar - link to gravatar image of user with 300x300 pixels size * @property {string} gravatar30 - link to gravatar image of user with 30x30 pixels size * @property {string} gravatar50 - link to gravatar image of user with 50x50 pixels size * @property {string} gravatar80 - link to gravatar image of user with 80x80 pixels size * @property {string} gravatar100 - link to gravatar image of user with 100x100 pixels size * @fires Hunt#user:* * @fires Hunt#user:save * @fires Hunt#user:signup * @fires Hunt#user:signin:* * @fires Hunt#user:signin:providerName * @fires Hunt#user:notify:* * @fires Hunt#user:notify:sio * @fires Hunt#user:notify:email * @tutorial authorization */ /** * Namespace for events related to {@link User} model. * * @event Hunt#user:* * @type {object} * @property {string} type - type of event - `auth`,`save`... * @property {object} info additional information * @property {User} user - user, related to this event * @see User * */ module.exports = exports = function (core) { var rack = core.rack, sha512 = core.sha512, md5 = core.md5, ValidationError = core.mongoose.Error.ValidationError, ValidatorError = core.mongoose.Error.ValidatorError, async = core.async, Q = core.Q, validator = core.validator, UserSchema = new core.mongoose.Schema( { accountVerified: {type: Boolean, default: false, index: true}, _salt: {type: String, default: rack},//string to hash password _password: {type: String, default: rack},//hashed password keychain: {type: Object, index: true, unique: true, sparse: true}, huntKey: { type: String, required: true, index: true, unique: true, default: rack, match: /^[a-zA-Z0-9_]+$/ }, huntKeyCreatedAt: {type: Date, default: Date.now}, lastSeenOnline: Date, displayName: {type: String, trim: true}, name: { title: {type: String, trim: true}, familyName: {type: String, trim: true}, middleName: {type: String, trim: true}, givenName: {type: String, trim: true} }, root: {type: Boolean, default: false}, isBanned: {type: Boolean, default: false}, profile: {}, roles: {} }, { toObject: {getters: true, virtuals: true}, //http://mongoosejs.com/docs/api.html#document_Document-toObject toJSON: {getters: true, virtuals: true} } ); //long stacktraces if (core.config.env === 'development') { Q.longStackSupport = true; } function promiseSave(target) { var deferred = Q.defer(); target.save(function (err, result) { if (err) { deferred.reject(err); } else { deferred.resolve(result); } }); return deferred.promise; } UserSchema.index({ huntKey: 1, keychain: 1, roles: 1 }); UserSchema.pre('save', function (next) { var err = null; if (this.email) { if (!validator.isEmail(this.email)) { err = new ValidationError(this); err.errors.email = new ValidatorError('email', 'Email has invalid syntax!', 'notvalid', this.email); } } this.markModified('keychain'); this.markModified('roles'); this.markModified('profile'); //http://mongoosejs.com/docs/schematypes.html next(err); }); /** * Event is emitted each time the user profile is updated and saved * with payload of user being saved. * * @event Hunt#user:save * @type {object} * @property {string} type - type of event `save`... * @property {object} info additional information * @property {User} user - user, related to this event * @see User * */ UserSchema.post('save', function (user) { core.emit(['user', 'save'], { 'type': 'save', 'user': user, 'info': {} }); }); //validation for keychain - strictly no duplicates UserSchema.path('keychain').validate(function (value, callback) { var t = this; if (value) { core.async.each(Object.keys(value), function (v, cb) { core.model.User.findOneByKeychain(v, value[v], function (error, userFound) { if (error) { cb(error); } else { if (userFound) { if (userFound._id.equals(t._id)) { //http://stackoverflow.com/a/11638106/1885921 cb(null); } else { cb(new Error('User with keychain[' + v + ']=' + value[v] + ' already exists!')); } } else { cb(null); } } }); }, function (error) { if (error) { callback(false); } else { callback(true); } }); } else { callback(true); } }, 'Keychain is in use!'); UserSchema.methods.toJSON = function () { return { 'id': this.id, 'name': this.name, // 'profile': this.profile, 'displayName': this.displayName, 'gravatar': this.gravatar, 'gravatar30': this.gravatar30, 'gravatar50': this.gravatar50, 'gravatar80': this.gravatar80, 'gravatar100': this.gravatar100, 'roles': this.roles, 'isBanned': this.isBanned, 'root': this.root, 'online': this.isOnline }; }; UserSchema.methods.toString = function () { var t = this, name, out = []; ['title', 'familyName', 'middleName', 'givenName'].map(function (n) { if (t.name[n]) { out.push(t.name[n]); } }); name = (out.length > 0) ? out.join(' ') : null; return name || this.displayName || this.username || ('User #' + this.id); }; UserSchema.virtual('email') .get(function () { return (this.keychain) ? (this.keychain.email || null) : null; }) .set(function (email) { if (email !== undefined) { this.set('accountVerified', false); this.set('keychain.email', email.toString().toLowerCase()); } }); UserSchema.virtual('myself') .get(function () { return true; }); UserSchema.virtual('username') .get(function () { if (this.keychain) { return this.keychain.username || null; } return this.displayName || 'User#' + this.id; }) .set(function (username) { this.set('keychain.username', username); }); UserSchema.virtual('familyName') .get(function () { return (this.name) ? (this.name.familyName || null) : null; }) .set(function (val) { this.set('name.familyName', val); }); UserSchema.virtual('middleName') .get(function () { return (this.name) ? (this.name.middleName || null) : null; }) .set(function (val) { this.set('name.middleName', val); }); UserSchema.virtual('givenName') .get(function () { return (this.name) ? (this.name.givenName || null) : null; }) .set(function (val) { this.set('name.givenName', val); }); UserSchema.virtual('password') .get(function () { return function (password) { return (sha512(password + this._salt) === this._password); }; }) .set(function (value) { var salt = rack(); this._salt = salt; this._password = sha512(value + salt); }); /** * @method User#getGravatar * @param {number} [size=300] - image size in pixels * @param {string} [type='wavatar'] - type * @param {string} [rating='g'] - image rating, default - suitable for all sites * @returns {string} url to gravatar * @description * Get {@link http://en.gravatar.com/ | gravatar} for user, based on his/her email or id */ UserSchema.methods.getGravatar = function (size, type, rating) { size = size || 300; type = type || 'wavatar'; rating = rating || 'g'; var key = (this.email) ? (this.email.toLowerCase().trim()) : this.id; return 'https://secure.gravatar.com/avatar/' + md5(key) + '.jpg?s=' + size + '&d=' + type + '&r=' + rating; }; UserSchema.virtual('gravatar').get(function () { return this.getGravatar(80); }); UserSchema.virtual('gravatar30').get(function () { return this.getGravatar(30); }); UserSchema.virtual('gravatar50').get(function () { return this.getGravatar(50); }); UserSchema.virtual('gravatar80').get(function () { return this.getGravatar(80); }); UserSchema.virtual('gravatar100').get(function () { return this.getGravatar(100); }); UserSchema.virtual('lastSeenOnlineAgo').get(function () { if (this.lastSeenOnline) { return ((new Date().getTime() - this.lastSeenOnline.getTime())); } return 10 * 365 * 24 * 60 * 60 * 1000; //month))) }); UserSchema.virtual('isOnline').get(function () { return (this.lastSeenOnlineAgo < 60000); }); /** * @method User#invalidateSession * @this User * @param {function} callback - function(err, newhuntKey) * @see User#huntKey * @description * Invalidates session, forcing user to be logged out on every clien by means of regenerating huntkey */ UserSchema.methods.invalidateSession = function (callback) { var newhuntKey = rack(); this.huntKey = newhuntKey; this.huntKeyCreatedAt = new Date(); return promiseSave(this) .then(function () { return newhuntKey; }) .nodeify(callback); }; /** * @method User#ban * @this User * @param {function} callback - function(err, userSaved) */ UserSchema.methods.ban = function (callback) { this.isBanned = true; return promiseSave(this).nodeify(callback); }; /** * @method User#unban * @this User * @param {function} callback - function(err, userSaved) */ UserSchema.methods.unban = function (callback) { this.isBanned = false; return promiseSave(this).nodeify(callback); }; /** * @method User#notify * @this User * @description * Send notifications to this user. Note, that notifications ARE not stored in database * @param {string} [channel="all"] channel- channel to use for notifying users * @param {(object|string)} message * @fires Hunt#user:notify:* * @fires Hunt#user:notify:all * @fires Hunt#user:notify:email * @fires Hunt#user:notify:sio * @returns {Boolean} - true, if some listener processed the event */ UserSchema.methods.notify = function (channel, message) { var channelToUse, messageToSend; if (message === undefined && (typeof channel === 'object' || typeof channel === 'string')) { channelToUse = 'all'; messageToSend = channel; } else { if (typeof channel === 'string' && (typeof message === 'object' || typeof message === 'string')) { channelToUse = channel; messageToSend = message; } else { throw new Error('Function User.notify([channelNameString],messageObj) has wrong arguments!'); } } /** * Emitted when any of users is notified by any channel * * @see User#notify * @event Hunt#user:notify:* * @type {object} * @property {string} channel - name of channel being used * @property {User} user - user being notified * @property {(string|object)} message */ /** * Emitted when any of users is notified by all channels * * @see User#notify * @event Hunt#user:notify:all * @type {object} * @property {string} channel - name of channel being used * @property {User} user - user being notified * @property {(string|object)} message */ return core.emit('user:notify:' + channelToUse, { 'channel': channelToUse, 'user': this, 'message': messageToSend }); }; /** * @method User#notifyByEmail * @this User * @description * Send email notification to user * @param {(object|string)} message * @fires Hunt#user:notify:email * @returns {Boolean} - true, if some listener processed the event, false if user do not have an email */ UserSchema.methods.notifyByEmail = function (message) { if (this.keychain.email) { /** * Emitted when any of users is notified by email, and when * the user have email address, of course * * @see User#notifyByEmail * @event Hunt#user:notify:email * @type {object} * @property {User} user - user being notified * @property {(string|object)} message */ return this.notify('email', message); } return false; }; /** * @method User#notifyBySocketIo * @this User * @description * Send socket.io notification to this particular user only. * @param {(object|string)} message * @param {string} [socketIoEventType='notify:sio'] - event name * @fires Hunt#user:notify:sio * @returns {Boolean} - true, if some listener processed the event */ UserSchema.methods.notifyBySocketIo = function (message, socketIoEventType) { /** * Emitted to notify any of users by socket.io * * @see User#notifyBySocketIo * @event Hunt#user:notify:sio * @type {object} * @property {User} user - user being notified * @property {(string|object)} message - message object * @property {string} type - type of socket.io event to emit, default is `notify:sio` */ var type = socketIoEventType || message.type || 'notify:sio'; //return core.emit('notify:sio', {'user': this, 'type': type, 'message': message}); return core.emit(['user', 'notify', 'sio'], { 'channel': 'sio', 'user': this, 'type': type, 'message': message }); }; //finders /** * @method User.findOneByEmailOrUsername * @param {string} emailOrUsername * @param {function} callback - function(err,userFound) */ UserSchema.statics.findOneByEmailOrUsername = function (emailOrUsername, callback) { var needle = { '$or': [ {'keychain.email': emailOrUsername}, {'keychain.username': emailOrUsername} ] }; return Q.ninvoke(this, 'findOne', needle).nodeify(callback); }; /** * @method User.findOneByHuntKey * @param {string} huntKey * @param {function} callback - function(err,userFound) */ UserSchema.statics.findOneByHuntKey = function (huntKey, callback) { //this.findOne({ 'huntKey': huntKey }, callback); return Q.ninvoke(this, 'findOne', {'huntKey': huntKey}).nodeify(callback); }; /** * @method User.findOneByKeychain * @param {string} provider * @param {string} id * @param {function} callback - function(err,userFound) * @tutorial authorization */ UserSchema.statics.findOneByKeychain = function (provider, id, callback) { var key = 'keychain.' + provider, needle = {}; needle[key] = id; return Q.ninvoke(this, 'findOne', needle).nodeify(callback); }; /** * @method User.findOneFuzzy * @param {string} idOrEmailOrHuntKey * @param {function} callback - function(err,userFound){...} * @tutorial authorization * @description * Find one user by _id, username,email, huntKey */ UserSchema.statics.findOneFuzzy = function (idOrEmailOrHuntKey, callback) { if (/^[0-9a-fA-F]{24}$/.test(idOrEmailOrHuntKey)) { return Q.ninvoke(this, 'findById', idOrEmailOrHuntKey).nodeify(callback); } var needle = { $or: [ {'keychain.email': idOrEmailOrHuntKey}, {'keychain.username': idOrEmailOrHuntKey}, {'huntKey': idOrEmailOrHuntKey} ] }; return Q.ninvoke(this, 'findOne', needle).nodeify(callback); }; //local strategy (username/email and password) /** * @method User.signUp * @param {string} username * @param {string} email * @param {string} password * @param {function} callback - function(error, userSignedUp){} * @tutorial authorization * @fires Hunt#user:signup */ UserSchema.statics.signUp = function (username, email, password, callback) { var User = this; return Q .fcall(function () { if (!core.validator.isEmail(email)) { throw new Error('Malformed email!'); } }) .then(function () { var user = new User(); user.accountVerified = false; user.keychain = {}; user.keychain.email = email.toString().toLowerCase(); if (username) { user.displayName = username.toString().toLowerCase(); user.keychain.username = username.toString().toLowerCase(); } user.keychain.welcomeLink = core.rack(); user.huntKey = core.rack(); user.password = password; //return Q.ninvoke(user, 'save'); return promiseSave(user); }) .then(function (newUser) { /** * Event is emited each time user sign's up * * @event Hunt#user:signup * @type {object} * @property {string} type - type of event `signup` * @property {object} info additional information * @property {User} user - user, related to this event * @tutorial authorization * @see User * * @example * * Hunt.on('user:signup', function(payload){ * console.log('We catch '+this.event.join(':') + 'event, which means '+ payload.user.displayName + ' signed up by local strategy!'); * }); */ core.emit(['user', 'signup'], { 'user': newUser, 'type': 'signup', 'info': { 'email': email } }); return newUser; }) .nodeify(callback); }; /** * @method User.signIn * @param {string} email * @param {string} password * @param {function} callback - function(error, userSigned){} * @tutorial authorization * @fires Hunt#user:signin:local */ UserSchema.statics.signIn = function (email, password, callback) { return Q.ninvoke(this, 'findOneByEmailOrUsername', email) .then(function (userFound) { if (userFound) { if (userFound.password(password)) { return userFound; } } return false; }) .nodeify(callback); }; /** * @method User.findOneByWelcomeLinkAndVerifyEmail * @param {string} welcomeLink * @tutorial authorization * @param {function} callback - function(error, userSigned){} */ UserSchema.statics.findOneByWelcomeLinkAndVerifyEmail = function (welcomeLink, callback) { return Q.ninvoke(this, 'findOneByKeychain', 'welcomeLink', welcomeLink) .then(function (userFound) { if (userFound && userFound.accountVerified === false) { delete userFound.keychain.welcomeLink; userFound.markModified('keychain'); userFound.accountVerified = true; return promiseSave(userFound); } throw new Error('Activation key is wrong or outdated!'); }) .nodeify(callback); }; /** * @method User.findOneByHuntKeyAndVerifyEmail * @param {string} welcomeLink * @tutorial authorization * @param {function} callback - function(error, userSigned){} */ UserSchema.statics.findOneByHuntKeyAndVerifyEmail = function (welcomeLink, callback) { return Q .ninvoke(this, 'findOneByWelcomeLinkAndVerifyEmail', welcomeLink) .nodeify(callback); }; /** * @method User.findOneByHuntKeyAndResetPassword * @tutorial authorization * @description * This function is used for reseting users password by link in email with submitting form later * @param {string} welcomeLink - welcome lint to use * @param {string} password - new password to set * @param {function} callback - function(error, userFoundAndUpdated) is fired when user is saved */ UserSchema.statics.findOneByHuntKeyAndResetPassword = function (welcomeLink, password, callback) { return Q.ninvoke(this, 'findOneByKeychain', 'welcomeLink', welcomeLink) .then(function (userFound) { if (userFound && (new Date().getTime() - userFound.huntKeyCreatedAt.getTime()) < 30 * 60 * 1000) { userFound.password = password; return promiseSave(userFound); } throw new Error('Activation key is wrong or outdated!'); }) .nodeify(callback); }; //oauth providers - twitter, facebook, github, other ones /** * @method User#setKeyChain * @tutorial authorization * @param {string} provider - provider name, for example - twitter, github, google * @param {string} id - id of user on this provider * @param {function} callback - function(err,userSaved){..} */ UserSchema.methods.setKeyChain = function (provider, id, callback) { this.keychain = this.keychain || {}; this.keychain[provider] = id; return promiseSave(this).nodeify(callback); }; /** * @method User#revokeKeyChain * @tutorial authorization * @param {string} provider - provider name, for example - twitter, github, google * @param {function} callback - function(err,userSaved) */ UserSchema.methods.revokeKeyChain = function (provider, callback) { this.keychain[provider] = null; this.markModified('keychain'); //http://mongoosejs.com/docs/schematypes.html return promiseSave(this).nodeify(callback); }; /** * @method User.processOAuthProfile * @param {request} request - current request (from controllers `request`) * @param {object} profile * @param {function} callback - function(err,userAuthorized) * @fires Hunt#user:signin:providerName * @tutorial authorization * @see request * @this User * @since 0.4.18 * @description * Function to process oauth profile, and if user is authorized, we attach this profile to his keychain * If user is not authorized and found, we sign up him/her * with accountVerified */ UserSchema.statics.processOAuthProfile = function (request, profile, callback) { var User = this; if (profile && profile.provider && typeof profile.provider === 'string') { if (!profile.id) { callback(new Error('User profile do not have `id` - ' + JSON.stringify(profile))); return; } } else { callback(new Error('User profile do not have `provider` - ' + JSON.stringify(profile))); return; } if (request.user) { //attaching profile to user winston.info('user:signup:%s:%s', profile.provider, request.user.id, { 'ip': request.ip, 'ips': request.ips, 'userAgent': request.headers['user-agent'], 'userId': request.user.id, 'userEmail': request.user.email, 'userName': request.user.username, 'user': request.user.toString() }); request.user.setKeyChain(profile.provider, profile.id, callback); } else { User.findOneByKeychain(profile.provider, profile.id, function (err, userFound) { if (err) { callback(err); } else { if (userFound) { /** * Event is emitted each time user signs in by keychain * * @event Hunt#user:signin:providerName * @type {object} * @property {string} method - name of provider user * @property {object} profile - profile as received from Oauth provider * @property {User} user - user, related to this event * @property {string} ip - IP address being used by user * @property {string} ips - IP addresses chain being used including proxies * @property {string} userAgent - user agent being used by user * @tutorial authorization * @since 0.4.18 * @see User * * @example * * Hunt.on('user:signin:github', function(payload){ * console.log('We catch '+this.event.join(':') + 'event, which means '+ payload.user.displayName + ' authorized by github!'); * }); * */ core.emit(['user', 'signin', profile.provider], { 'ip': request.ip, 'ips': request.ips, 'userAgent': request.headers['user-agent'], 'method': profile.provider, 'profile': profile, 'user': userFound }); winston.info('user:signin:%s:%s', profile.provider, userFound.id, { 'ip': request.ip, 'ips': request.ips, 'userAgent': request.headers['user-agent'], 'userId': userFound.id, 'userEmail': userFound.email, 'userName': userFound.username, 'user': userFound.toString() }); callback(null, userFound); } else { var keychain = {}; keychain[profile.provider] = profile.id; User.create({ 'accountVerified': true, 'lastSeenOnline': Date.now(), 'displayName': profile.displayName, 'name': profile.name, 'keychain': keychain }, function (err, userCreated) { if (err) { callback(err); } else { core.emit(['user', 'signup', profile.provider], { 'ip': request.ip, 'ips': request.ips, 'userAgent': request.headers['user-agent'], 'method': profile.provider, 'profile': profile, 'user': userCreated }); winston.info('user:signup:%s:%s', profile.provider, userCreated.id, { 'ip': request.ip, 'ips': request.ips, 'userAgent': request.headers['user-agent'], 'userId': userCreated.id, 'userEmail': userCreated.email, 'userName': userCreated.username, 'user': userCreated.toString() }); callback(null, userCreated); } }); } } }); } }; //dialog messages system /** * @method User#sendMessage * @description * Sends private message from this user to other one * @example * * User1.sendMessage(User2,'hello!',function(err){if(err) throw err;}); * //User1 sends message to User2 * * @param {User/string} to - receiver of message * @param {string} message - text of message * @param {function} callback -function to be called on message delivery */ UserSchema.methods.sendMessage = function (to, message, callback) { var thisUser = this; async.waterfall([ function (cb) { if (typeof to === 'string') { core.model.User.findOneFuzzy(to, cb); } else { if (to._id) { cb(null, to); } else { cb(new Error('to have to be user instance or string of username or email')); } } }, function (userFound, cb) { core.model.Message.create({ 'to': userFound._id, 'from': thisUser._id, 'message': message }, function (err, messageCreated) { if (err) { cb(err); } else { cb(null, userFound, messageCreated); } }); }, function (to, messageCreated, cb) { core.emit('user:notify:sio', { 'user': to, 'from': thisUser, 'type': 'dialog',//':incoming', 'message': messageCreated.message }); /** * Event is emitted each time user recieves a message via dialog api * * @event Hunt#user:notify:pm * @type {object} * @property {User} to - reciever of the message * @property {User} from - sender of the message * @property {type} type of message * @see User * * @example * * Hunt.on('user:notify:pm', function(payload){ * console.log(payload.from+ ' says to '+payload.user + ' ' + payload.message + ' via ' + payload.type ); * }); * */ core.emit('user:notify:pm', { 'user': to, 'from': thisUser, 'type': 'dialog', 'message': messageCreated.message }); cb(null, messageCreated); } ], callback); }; /** * @method User#receiveMessage * @description * Sends private message to this user from other one * @example * * User1.sendMessage(User2,'hello!',function(err){if(err) throw err;}); * //User1 sends message to User2 * * @param {User/string} from - receiver of message * @param {string} message - text of message * @param {function} callback -function to be called on message delivery */ UserSchema.methods.receiveMessage = function (from, message, callback) { var thisUser = this; async.waterfall([ function (cb) { if (typeof from === 'string') { core.model.User.findOneFuzzy(from, cb); } else { if (from._id) { cb(null, from); } else { cb(new Error('to have to be user instance or string of username or email')); } } }, function (userFound, cb) { core.model.Message.create({ 'from': userFound._id, 'to': thisUser._id, 'message': message }, function (err, messageCreated) { if (err) { cb(err); } else { cb(null, userFound, messageCreated); } }); }, function (from, messageCreated, cb) { /** * Event is emitted each time user recieves a message via dialog api * * @event Hunt#user:notify:pm * @type {object} * @property {User} to - reciever of the message * @property {User} from - sender of the message * @property {type} type of message * @see User * * @example * * Hunt.on('user:notify:pm', function(payload){ * console.log(payload.from+ ' says to '+payload.user + ' ' + payload.message + ' via ' + payload.type ); * }); * */ core.emit('user:notify:sio', { 'user': thisUser, 'from': from, 'type': 'dialog',//':incoming', //probab;y? 'message': messageCreated.message }); core.emit('user:notify:pm', { 'user': thisUser, 'from': from, 'type': 'dialog', 'message': messageCreated.message }); cb(null, messageCreated); } ], callback); }; /** * @method User#getRecentMessages * @description * Get recent messages in reverse chronological order - the most recent on top * @param {int} messageLimit - limit of messages * @param {int} messageOffset - offset * @param {function} callback -function(err,messages) to be called with message object */ UserSchema.methods.getRecentMessages = function (messageLimit, messageOffset, callback) { var thisUser = this; messageLimit = messageLimit || 100; messageOffset = messageOffset || 0; return core.model.Message .find({ $or: [ {'to': thisUser._id}, {'from': thisUser._id} ] }) .skip(messageOffset) .limit(messageLimit) .sort('-createdAt') .populate('to from') // .populate('from') .exec(callback); }; /** * @method User#getDialog * @description * Get recent messages for dialog with this and user with username in reverse chronological order - the most recent on top * @param {User} usernameOrUser - author of message - string of username/email or user object * @param {int} messageLimit - limit of messages * @param {int} messageOffset - offset * @param {function} callback -function(err,messages) to be called with message object */ UserSchema.methods.getDialog = function (usernameOrUser, messageLimit, messageOffset, callback) { var thisUser = this; messageLimit = messageLimit || 100; messageOffset = messageOffset || 0; async.waterfall([ function (cb) { core.model.User.findOneFuzzy(usernameOrUser, cb); }, function (userFound, cb) { core.model.Message .count({ $or: [ {'to': thisUser._id, 'from': userFound._id}, {'from': thisUser._id, 'to': userFound._id} ] }) .exec(function (error, numberOfMessages) { cb(error, userFound, numberOfMessages); }); }, function (userFound, numberOfMessages, cb) { if (userFound) { core.model.Message .find({ $or: [ {'to': thisUser._id, 'from': userFound._id}, {'from': thisUser._id, 'to': userFound._id} ] }) .limit(messageLimit) .skip(messageOffset) .sort('-createdAt') .populate('to from') //todo - is it cached??? // .populate('from') .exec(function (error, messagesFound) { cb(error, messagesFound, numberOfMessages); }); } else { cb(new Error('User do not exists!')); } } ], callback); }; /** * @method User#hasRole * @param {String} roleName - name of the role * @returns {Boolean} - true, if user has the Role required * @tutorial authorization * @example * * Hunt.model.User.find({}, function(error, usersFound){ * if(error) { * throw error; * } else { * usersFound.map(function(user){ * if(!user.hasRole('hunter')){ * throw new Error('User #'+user.id +' is not a Hunter! We need to punish him/her!'); * } * }); * } * }); * */ UserSchema.methods.hasRole = function (roleName) { return (this.roles && this.roles[roleName]) ? true : false; }; //default ACL for exporting model UserSchema.statics.canCreate = function (user, callback) { return callback(null, false); }; UserSchema.statics.listFilter = function (user, callback) { if (user) { if (user.root) { return callback(null, {}, fieldsReadableByRootOrSelf); } else { return callback(null, {'accountVerified': true}, fieldsReadable); } } return callback(null, false); }; UserSchema.methods.canRead = function (user, callback) { var t = this; if (user) { if (t._id.equals(user._id)) { return callback(null, true, fieldsReadableByRootOrSelf.concat(['myself', 'huntKey'])); } if (user.root) { return callback(null, true, fieldsReadableByRootOrSelf); } } return callback(null, true, fieldsReadable); }; UserSchema.methods.canUpdate = function (user, callback) { var t = this; if (user && t._id.equals(user._id)) { return callback(null, true, fieldsWritable); } if (user && user.root) { return callback(null, true, fieldsWritableByRoot); } return callback(null, false); }; UserSchema.methods.canDelete = function (user, callback) { callback(null, (user && user.root)); }; return core.mongoConnection.model('User', UserSchema); };