<commit_before>'use strict';

/**
 * Interesting reads:
 * https://blogs.dropbox.com/tech/2016/09/how-dropbox-securely-stores-your-passwords/
 * https://news.ycombinator.com/item?id=12548210
 * https://pulse.michalspacek.cz/passwords/storages
 *
 */

/**
 * @apiDefine AuthorizationRequiredError
 *
 * @apiHeader {String} x-apikey the secret API key which corresponds to the <code>senseBoxId</code> parameter.
 * @apiHeaderExample {String} x-apikey header example:
 *   x-apikey: <KEY>
 * @apiError {Object} 403 the request has invalid or missing credentials.
 * @apiErrorExample {json} Error-Response:
 *     HTTP/1.1 403 Forbidden
 *     {"code":"NotAuthorized","message":"ApiKey is invalid or missing"}
 */

/**
 * define for nested user parameter for box creation request
 * @apiDefine User Parameter for creating the user for a senseBox
 */

/**
 * @apiDefine UserBody
 *
 * @apiParam (User) {String} [firstname] the firstname of the user.
 * @apiParam (User) {String} [lastname] the ths lastname of the user.
 * @apiParam (User) {String} email the email for the user. Is used for signing in and for sending the arduino sketch.
 * @apiParam (User) {String} password the desired password for the user. Must be at least 8 characters long.
 * @apiParam (User) {String} [language=de_DE] the language of the user. Used for the website and mails
 *
 */

const mongoose = require('mongoose'),
  Schema = mongoose.Schema,
  bcrypt = require('bcrypt'),
  crypto = require('crypto'),
  utils = require('../utils'),
  uuid = require('uuid'),
  jwt = require('jsonwebtoken');

const { config, Honeybadger } = utils;
const { salt_factor, password_min_length, jwt_algorithm, jwt_secret, origin } = config;

const userSchema = new Schema({
  firstname: {
    type: String,
    trim: true
  },
  lastname: {
    type: String,
    trim: true
  },
  email: {
    type: String,
    required: true,
    trim: true,
    lowercase: true,
    unique: true,
  },
  boxes: [
    {
      type: String,
      trim: true
    }
  ],
  language: {
    type: String,
    trim: true,
    default: 'de_DE'
  },
  hashedPassword: {
    type: String,
    required: true
  },
  role: {
    type: String,
    required: true,
    enum: ['user', 'admin'],
    default: 'user'
  },
  resetPasswordToken: { type: String },
  resetPasswordExpires: { type: Date },
  lastUpdatedBy: {
    type: String,
    required: true,
    default: 'System'
  },
  emailConfirmationToken: { type: String, default: uuid },
  emailConfirmationExpires: { type: Date },
  emailIsConfirmed: { type: Boolean, default: false, required: true }
});

// only send out names and email..
userSchema.set('toJSON', {
  version: false,
  transform: function transform (doc, ret) {
    const { firstname, lastname, email, role, language } = ret;

    return { firstname, lastname, email, role, language };
  }
});

// ----  Password stuff -----
userSchema.virtual('password')
  .get(function () {
    return this._password;
  })
  .set(function (value) {
    this._password = value;
    this.hashedPassword = '<PASSWORD>';
  });

const passwordHasher = function passwordHasher (plaintextPassword) {
  // first round: hash plaintextPassword with sha512
  const hash = crypto.createHash('sha512');

  hash.update(plaintextPassword, 'utf8');

  const hashed = hash.digest('base64'); // base64 for more entroy than hex

  return bcrypt.hash(hashed, salt_factor); // signature <String, Number> generates a salt and hashes in one step
};

userSchema.path('hashedPassword').validate(function () {
  if (this._password) {
    if (this._password.length < password_min_length) {
      return this.invalidate('password', `must be at least ${password_min_length} characters.`);
    }
  }

  if (this.isNew && !this._password) {
    return this.invalidate('password', 'is required');
  }
});

userSchema.pre('save', function userPreHashPassword (next) {
  const user = this;

  if (!user._password) {
    return next();
  }

  passwordHasher(user._password)
    .then(function (hashedPassword) {
      user._password = <PASSWORD>;
      user.hashedPassword = <PASSWORD>Password;
      next();
    })
    .catch(function (err) {
      Honeybadger.notify(err);

      next(err);
    });
});

userSchema.methods.checkPassword = function checkPassword (plaintextPassword) {
  const hash = crypto.createHash('sha512');
  hash.update(plaintextPassword, 'utf8');
  const hashed = hash.digest('base64');

  return bcrypt.compare(hashed, this.hashedPassword);
};
// ---- End Password stuff -----

const jwtSignOptions = {
  algorithm: jwt_algorithm,
  issuer: origin,
  expiresIn: '1h'
};

userSchema.methods.createToken = function createToken () {
  const payload = { role: this.role },
    signOptions = Object.assign({ subject: this.email, jwtid: uuid() }, jwtSignOptions);

  return new Promise(function (resolve, reject) {
    jwt.sign(payload, jwt_secret, signOptions, (err, token) => {
      if (err) {
        return reject(err);
      }

      return resolve(token);
    });
  });
};

const handleE11000 = function (error, res, next) {
  if (error.name === 'MongoError' && error.code === 11000) {
    return next(new Error('Duplicate user detected'));
  }<commit_msg>don't send the role in jwt<commit_after>'use strict';

/**
 * Interesting reads:
 * https://blogs.dropbox.com/tech/2016/09/how-dropbox-securely-stores-your-passwords/
 * https://news.ycombinator.com/item?id=12548210
 * https://pulse.michalspacek.cz/passwords/storages
 *
 */

/**
 * @apiDefine AuthorizationRequiredError
 *
 * @apiHeader {String} x-apikey the secret API key which corresponds to the <code>senseBoxId</code> parameter.
 * @apiHeaderExample {String} x-apikey header example:
 *   x-apikey: <KEY>
 * @apiError {Object} 403 the request has invalid or missing credentials.
 * @apiErrorExample {json} Error-Response:
 *     HTTP/1.1 403 Forbidden
 *     {"code":"NotAuthorized","message":"ApiKey is invalid or missing"}
 */

/**
 * define for nested user parameter for box creation request
 * @apiDefine User Parameter for creating the user for a senseBox
 */

/**
 * @apiDefine UserBody
 *
 * @apiParam (User) {String} [firstname] the firstname of the user.
 * @apiParam (User) {String} [lastname] the ths lastname of the user.
 * @apiParam (User) {String} email the email for the user. Is used for signing in and for sending the arduino sketch.
 * @apiParam (User) {String} password the desired password for the user. Must be at least 8 characters long.
 * @apiParam (User) {String} [language=de_DE] the language of the user. Used for the website and mails
 *
 */

const mongoose = require('mongoose'),
  Schema = mongoose.Schema,
  bcrypt = require('bcrypt'),
  crypto = require('crypto'),
  utils = require('../utils'),
  uuid = require('uuid'),
  jwt = require('jsonwebtoken');

const { config, Honeybadger } = utils;
const { salt_factor, password_min_length, jwt_algorithm, jwt_secret, origin } = config;

const userSchema = new Schema({
  firstname: {
    type: String,
    trim: true
  },
  lastname: {
    type: String,
    trim: true
  },
  email: {
    type: String,
    required: true,
    trim: true,
    lowercase: true,
    unique: true,
  },
  boxes: [
    {
      type: String,
      trim: true
    }
  ],
  language: {
    type: String,
    trim: true,
    default: 'de_DE'
  },
  hashedPassword: {
    type: String,
    required: true
  },
  role: {
    type: String,
    required: true,
    enum: ['user', 'admin'],
    default: 'user'
  },
  resetPasswordToken: { type: String },
  resetPasswordExpires: { type: Date },
  lastUpdatedBy: {
    type: String,
    required: true,
    default: 'System'
  },
  emailConfirmationToken: { type: String, default: uuid },
  emailConfirmationExpires: { type: Date },
  emailIsConfirmed: { type: Boolean, default: false, required: true }
});

// only send out names and email..
userSchema.set('toJSON', {
  version: false,
  transform: function transform (doc, ret) {
    const { firstname, lastname, email, role, language } = ret;

    return { firstname, lastname, email, role, language };
  }
});

// ----  Password stuff -----
userSchema.virtual('password')
  .get(function () {
    return this._password;
  })
  .set(function (value) {
    this._password = value;
    this.hashedPassword = '<PASSWORD>';
  });

const passwordHasher = function passwordHasher (plaintextPassword) {
  // first round: hash plaintextPassword with sha512
  const hash = crypto.createHash('sha512');

  hash.update(plaintextPassword, 'utf8');

  const hashed = hash.digest('base64'); // base64 for more entroy than hex

  return bcrypt.hash(hashed, salt_factor); // signature <String, Number> generates a salt and hashes in one step
};

userSchema.path('hashedPassword').validate(function () {
  if (this._password) {
    if (this._password.length < password_min_length) {
      return this.invalidate('password', `must be at least ${password_min_length} characters.`);
    }
  }

  if (this.isNew && !this._password) {
    return this.invalidate('password', 'is required');
  }
});

userSchema.pre('save', function userPreHashPassword (next) {
  const user = this;

  if (!user._password) {
    return next();
  }

  passwordHasher(user._password)
    .then(function (hashedPassword) {
      user._password = <PASSWORD>;
      user.hashedPassword = <PASSWORD>;
      next();
    })
    .catch(function (err) {
      Honeybadger.notify(err);

      next(err);
    });
});

userSchema.methods.checkPassword = function checkPassword (plaintextPassword) {
  const hash = crypto.createHash('sha512');
  hash.update(plaintextPassword, 'utf8');
  const hashed = hash.digest('base64');

  return bcrypt.compare(hashed, this.hashedPassword);
};
// ---- End Password stuff -----

const jwtSignOptions = {
  algorithm: jwt_algorithm,
  issuer: origin,
  expiresIn: '1h'
};

userSchema.methods.createToken = function createToken () {
  const payload = {},
    signOptions = Object.assign({ subject: this.email, jwtid: uuid() }, jwtSignOptions);

  return new Promise(function (resolve, reject) {
    jwt.sign(payload, jwt_secret, signOptions, (err, token) => {
      if (err) {
        return reject(err);
      }

      return resolve(token);
    });
  });
};

const handleE11000 = function (error, res, next) {
  if (error.name === 'MongoError' && error.code === 11000) {
    return next(new Error('Duplicate user detected'));
  }
{'fraction_non_alphanumeric': 0.14005791505791507, 'fraction_numerical': 0.009845559845559846, 'mean_word_length': 3.512630662020906, 'pattern_counts': {'":': 4, '<': 19, '<?xml version=': 0, '>': 19, 'https://': 6, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6299493', 'n_tokens_mistral': 3343, 'n_tokens_neox': 3240, 'n_words': 887}