UNPKG

waigo

Version:

Node.js ES6 framework for reactive, data-driven apps and APIs (Koa, RethinkDB)

511 lines (428 loc) 10.9 kB
"use strict"; const crypto = require('crypto'); const waigo = global.waigo, _ = waigo._, Q = waigo.load('support/promise'), errors = waigo.load('support/errors'); const randomBytesQ = Q.promisify(crypto.pseudoRandomBytes, { context: crypto }); const ProfileSchema = { displayName: { type: String, required: true }, }; const EmailSchema = { email: { type: String, required: true }, verified: { type: Boolean }, }; const AuthSchema = { type: { type: String, required: true }, token: { type: String, required: true }, data: { type: Object }, } exports.schema = { username: { type: String, required: true, }, profile: { type: ProfileSchema, required: true, }, emails: { type: [EmailSchema], required: true, adminViewOptions: { viewSubKey: 'email' }, }, auth: { type: [AuthSchema], required: true, adminViewOptions: { viewSubKey: 'type' }, }, roles: { type: [String], required: false, }, created: { type: Date, required: true, }, lastLogin: { type: Date, required: false, }, }; exports.indexes = [ { name: 'username', }, { name: 'email', def: function(doc) { return doc('emails')('email'); }, options: { multi: true, }, }, { name: 'roles', options: { multi: true, }, }, ]; exports.docVirtuals = { isAdmin: { get: function() { return true === _.includes(this.roles, 'admin'); } }, emailAddress: { get: function() { return _.get(this.emails, '0.email'); } }, emailAddresses: { get: function() { return _.map(this.emails || [], 'email'); } }, }; exports.docMethods = { /** * Get whether user has any of given roles */ isOneOf: function() { let roles = _.toArray(arguments); return !! (_.intersection(this.roles || [], roles).length); }, /** * Check password against hash. * @param {String} password * @param {String} storedHash * @return {Boolean} true if password matches, false otherwise */ isPasswordCorrect: function*(password) { let passAuth = _.find(this.auth, function(a) { return 'password' === a.type; }); if (!passAuth) { return false; } let sepPos = passAuth.token.indexOf('-'), salt = passAuth.token.substr(0, sepPos), hash = passAuth.token.substr(sepPos + 1); let generatedHash = yield this.__model.generatePasswordHash( password, salt ); return generatedHash === passAuth.token; }, /** * Log the user into given context. * @param {Object} context waigo client request context. */ login: function*(context) { this._logger().debug(`Logging in user: ${this.id} = ${this.username}`); context.session.user = { id: this.id, username: this.username, }; // update last-login timestamp this.lastLogin = new Date(); yield this.save(); }, /** * Verify an email address. * @param {String} email Email address to verify. */ verifyEmail: function*(email) { let theEmail = _.find(this.emails, function(e) { return email === e.email; }); if (!theEmail) { return false; } theEmail.verified = true; // save this.markChanged('emails'); yield this.save(); // record this._App().emit('record', 'verify_email', this, { email: email }); }, /** * Check whether user has given email address. * @param {String} email Email address to check. * @return {Boolean} */ hasEmail: function*(email) { return 0 <= _.findIndex(this.emails || [], function(e) { return email === e.email; }); }, /** * Check whether user has verified given email address. * @param {String} email Email address to check. * @return {Boolean} */ isEmailVerified: function*(email) { let item = _.find(this.emails || [], function(e) { return email === e.email; }); return item && item.verified; }, /** * Add an email address. * @param {String} email Email address to verify. * @param {Boolea} verified Whether address is verified. */ addEmail: function*(email, verified) { let theEmail = _.find(this.emails, function(e) { return email === e.email; }); if (!theEmail) { theEmail = { email: email, }; this.emails.push(theEmail); } theEmail.verified = true; // save this.markChanged('emails'); yield this.save(); // record this._App().emit('record', 'add_email', this, { email: email }); }, /** * Update this user's password. * @param {String} newPassword New password. */ updatePassword: function*(newPassword) { this._logger().debug('Update user password', this.username); let passAuth = _.find(this.auth, function(a) { return 'password' === a.type; }); if (!passAuth) { return false; } // update password passAuth.token = yield this.__model.generatePasswordHash(newPassword); // save this.markChanged('auth'); yield this.save(); // record this._App().emit('record', 'update_password', this); }, /** * Get OAuth data. * * @param {String} provider Auth provider. * * @return {Object} null if not found. */ getOauth: function*(provider) { provider = 'oauth:' + provider; provider = _.find(this.auth, function(a) { return provider === a.type; }); return _.get(provider, 'data', null); }, /** * Save OAuth data. * * @param {String} provider Auth provider. * @param {Object} data Data. */ saveOAuth: function*(provider, data) { yield this.saveAuth('oauth:' + provider, data); }, /** * Save Auth data. * * @param {String} type Auth type. * @param {Object} data Data. */ saveAuth: function*(type, data) { this._logger().debug('Save user auth', this.id, type, data); let existing = _.find(this.auth, function(a) { return type === a.type; }); if (!existing) { existing = { type: type }; this.auth.push(existing); } existing.data = data; // save this.markChanged('auth'); yield this.save(); // record this._App().emit('record', 'save_oauth', this, _.pick(existing, 'type', 'access_token')); }, /** * Get whether user can access given resource. * * @param {String} resource The resource the user wishes to access. * * @return {Boolean} true if access is possible, false if not. */ canAccess: function*(resource) { return this._App().acl.can(resource, this); }, /** * Assert that user can access given resource. * * @param {String} resource The resource the user wishes to access. * * @throws {Error} If not allowed to access. */ assertAccess: function*(resource) { return this._App().acl.assert(resource, this); }, }; exports.modelMethods = { /** * Get user by username. * @return {User} */ getByUsername: function*(username) { let ret = yield this.rawQry().filter(function(user) { return user('username').eq(username); }).run(); return this.wrapRaw(_.get(ret, '0')); }, /** * Get user by email address. * @return {User} */ getByEmail: function*(email) { const r = this.db; let ret = yield this.rawQry().filter( r.row('emails').contains(function(e) { return e('email').eq(email); }) ).run(); return this.wrapRaw(_.get(ret, '0')); }, /** * Get user by email address or username. * @return {User} */ getByEmailOrUsername: function*(str) { let ret = yield this.rawQry().filter(function(user) { return user('emails')('email')(0).eq(str).or(user('username').eq(str)); }).run(); return this.wrapRaw(_.get(ret, '0')); }, /** * Get user by email address or username. * @return {User} */ findWithIds: function*(ids) { let qry = this.rawQry(); qry = qry.getAll.apply(qry, ids.concat([{index: 'id'}])); return this.wrapRaw(yield qry.run()); }, /** * Find all admin users. * @return {Array} */ findAdminUsers: function*() { let ret = yield this.rawQry().filter(function(user) { return user('roles').contains('admin') }).run(); return this.wrapRaw(ret); }, /** * Get whether any admin users exist. * @return {Number} */ haveAdminUsers: function*() { let count = yield this.rawQry().count(function(user) { return user('roles').contains('admin') }).run(); return count > 0; }, /** * Generate a secure SHA256 representing given password. * @param {String} password The password. * @param {String} [salt] Salt to use. * @return {String} hash to store */ generatePasswordHash: function*(password, salt) { let hash = crypto.createHash('sha256'); salt = salt || (yield randomBytesQ(64)).toString('hex'); hash.update(salt); hash.update(password); return salt + '-' + hash.digest('hex'); }, /** * Register a new user * @param {Object} properties User props. * @param {String} properties.username Username. * @param {Object} [properties.roles] Roles * @param {String} [properties.email] Email address. * @param {Boolean} [properties.emailVerified] Whether email address is verified. * @param {String} [properties.password] User's password. * @return {User} The registered user. */ register: function*(properties) { // create user let attrs = { username: properties.username, emails: [], auth: [], profile: _.extend({ displayName: properties.username, }, properties.profile), roles: properties.roles || [], }; if (properties.email) { attrs.emails.push( { email: properties.email, verified: !!properties.emailVerified, } ); } if (properties.password) { attrs.auth.push( { type: 'password', token: yield this.generatePasswordHash(properties.password), } ); } attrs.created = new Date(); let user = yield this.insert(attrs); if (!user) { throw new Error('Error creating new user: ' + properties.username); } // log activity this._App().emit('record', 'register', user); // notify admins this._App().emit('notify', 'admins', `New user: ${user.id} - ${user.username}`); return user; }, loadLoggedIn: function*(context) { let userId = _.get(context, 'session.user.id'); if (!userId) { return null; } return yield this.get(userId); }, getUsersCreatedSince: function*(date) { let ret = yield this.rawQry().filter(function(doc) { return doc('created').ge(date) }).run(); return this.wrapRaw(ret); }, };