server/models/user.js

const _ = require('lodash');
const bcrypt = require('bcryptjs');

const Abstract = require('./abstract');
const config = require('../../config');
const db = require('../db');
const jwt = require('../utils/jwt');

const proto = Abstract.prototype;

/**
 * A user of the BioPocket platform.
 *
 * ## Database columns
 *
 * * **id** (`bigint`) - Internal ID (used for joins).
 * * **api_id** (`uuid`) - External ID (used in the API).
 * * **email** (`string`) - E-mail address.
 * * **password_hash** (`string`) - Bcrypt hash of the user's password.
 * * **active** (`boolean`) - Indicates whether the user can use the platform or has been deactivated by an administrator.
 * * **roles** (`string[]`) - Roles of the user (used for authorization).
 * * **created_at** (`datetime`) - Time at which the user was created.
 * * **updated_at** (`datetime`) - Time at which the user was last modified (equal to the creation date if never modified).
 *
 * ## Virtual properties
 *
 * * **password** (`string`) - Setting this property generates a new bcrypt hash and updates the `password_hash` column.
 *
 * @class
 * @extends Abstract
 * @see http://bookshelfjs.org
 */
const User = Abstract.extend({
  tableName: 'users',

  timestamps: true,

  virtuals: _.merge({
    password: {
      get: function() {
        return this._password;
      },

      set: function(password) {
        this._password = password;

        if (_.isString(password) && password.length) {
          const salt = bcrypt.genSaltSync(config.bcryptCost);
          this.set('password_hash', bcrypt.hashSync(password, salt));
        } else {
          this.unset('password_hash');
        }
      }
    }
  }, proto.virtuals),

  /**
   * Returns a JWT that can be used to authenticate as this user.
   *
   * @instance
   * @memberof User
   * @param {object} properties - JWT properties, passed to `generateToken` in the `utils/jwt` module.
   * @returns {string} A JWT.
   */
  generateJwt: function(properties) {
    return jwt.generateToken(_.extend({
      sub: this.get('api_id')
    }, properties));
  },

  /**
   * Indicates whether this user has the specified password or not.
   *
   * **WARNING:** this method is slow and blocking, as it computes a bcrypt
   * hash synchronously.  Do not overuse it.
   *
   * @instance
   * @memberof User
   * @param {string} password - The password to check.
   * @returns {boolean} True if the user's password is the same as the specified one.
   */
  hasPassword: function(password) {
    return !!password && bcrypt.compareSync(password, this.get('password_hash'));
  },

  /**
   * Indicates whether this user has the specified role.
   *
   * **WARNING:** this methods always returns true if the user has the role,
   * even if the user is inactive. It is not sufficient to determine whether
   * the user is currently authorized to perform the role.
   *
   * @instance
   * @memberof User
   * @param {string} role - The role to check.
   * @returns {boolean} True if the specified role is among the user's assigned roles.
   */
  hasRole: function(role) {
    return _.includes(this.get('roles'), role);
  },

  /**
   * Indicates whether this user is active. Users may be deactivated by administrators.
   *
   * @instance
   * @memberof User
   * @returns {boolean} True if this user is active.
   */
  isActive: function() {
    return !!this.get('active');
  }
});

module.exports = db.bookshelf.model('User', User);