Link to home
Start Free TrialLog in
Avatar of Bruce Gust
Bruce GustFlag for United States of America

asked on

How can I ensure a current password field matches what's in the database?

I'm new to Nodejs and I'm working on a ticket where I'm being tasked with first checking the user's current password before they're allowed to change it.

Here's the current route:

router.post('/profile/save', async (req, res) => {
  try {
    if (typeof req.body.user == 'undefined') {
      return res.send(user.error('Invalid request.'));
      }
          
      //checking to ensure current password matches what's in the database before updating profile information
      
      let check = await(user.getOne(global._user._id // here's where I get stuck
      
    let resp = await user.save(global._user._id, req.body.user);
    if (resp.error) {
      return res.send(resp);
    }

  #1) let data = await user.getOne( global._user._id, [
      { path: 'account', model: 'Account'}
    ] );

    req.session.user = Object.assign({}, req.session.user, data);

    flash.add(req, 'Your profile has been successfully saved.', 'success');

    res.send(resp);
  } catch (err) {
    res.status(500).send(User.error());
  }
});

You can see where I've notated "here's where I get stuck," but I've got an idea of what the flow needs to look like.

I need to first find the row that coincides with the global user_id. After I get that array, I need to parse it out and decrypt the password as it exists in the database and compare that to what the user entered in the "current password" field.

One other thing: #1 - what is user.getOne? I've seen "findOne," but can't find any info about "getOne." what is that?

Thanks!
Avatar of Chris Stanyon
Chris Stanyon
Flag of United Kingdom of Great Britain and Northern Ireland image

Hey Bruce,

Not sure how you're persisting data to the database, so I can't comment on that, but one thing I will point out is this bit:

I need to parse it out and decrypt the password as it exists in the database and compare that to what the user entered in the "current password" field.
That's the wrong way round. When you store passwords in a Database, you do a one-way encryption - once they're in the DB, there' no decrypting it to see what the actual password is.

To check a password, you encrypt what the user enters and compare that with what's in the database - if they entered the correct password, then the encrypted strings will match.

The flow could potentially be done in a couple of ways, for example, by adding a checkPassword method to your user model:

let check = await(user.checkPassword(global._user._id, req.body.currentPassword);
if (check.error) {
    return res.send(check);
}

Open in new window

That would assume your User has a checkPassword method and your request has a property called currentPassword.

getOne() looks to be a method on your User model. It's either something you've coded yourself, or something that's been added by a PlugIn. Don't know how you've got your app configured so can't really be more specific than that. Have a look at how your User model is defined, as it's likely to be a method on that.
My thinking is that it is better to store the salted hash of the password (and not the password as plaintext or encrypted) and then compare the salted hash of what the user entered with the salted hash saved in the database.
Yeah - should have said Hashing instead of Encrypting. Just replace Encrypt with Hash in my comments above and you should be good to go :)
Avatar of Bruce Gust

ASKER

Chris, I'm a teacup. I'm asking to be spoon fed here, so just go with me.

I found "getOne." It's part of "service.js" which is referenced in some of the other files that are required and it looks like this:

    getOne(id, populateWith = []) {
        return new Promise(async (resolve, reject) => {
            id = this._getId(id);

            // ensure that models are loaded
            let m, s, match;
            let paths = [];

            for (let i in populateWith) {
                m = populateWith[i];

                s = require(`../schemas/${m.model}Schema`);

                mongoose.model(m.model, s);

                paths.push(m.path);
            }

            // add a lifecycle method for adding additional filters
            try {
                match = await this._beforeGetOne(this._enforceScope({ _id: id }));
            } catch (err) {
                return reject(err);
            }

            // grab the record
            this.model.findOne(match).populate(paths.join(' ')).exec((err, record) => {
                if (err) {
                    console.log('Populate Error: ', err);
                    reject(err);
                } else {
                    resolve(record);
                }
            });

        });
    }

Open in new window


Here again is the "router.post('/profile/save')' function:

router.post('/profile/save', async (req, res) => {
  try {
    if (typeof req.body.user == 'undefined') {
      return res.send(user.error('Invalid request.'));
      }
      //if (req.body.avatar.legth > 0) {
      //    if (req.body.avatar.index('base64') > 0) {
      //        var base64Data = req.body.avatar.replace(/^data:image\/png;base64,/, "");
      //        fs.writeFile("out.png", base64Data, 'base64', function (err) {
      //            console.log(err);
      //        });
      //    }
          
      //}
	
	//checking to ensure current password matches what's in the database before updating profile information
	
	if(req.body.user[password] -> right here
	
	let check = await(user.getOne(global._user._id))
	
    let resp = await user.save(global._user._id, req.body.user);
    if (resp.error) {
      return res.send(resp);
    }

    let data = await user.getOne( global._user._id, [
      { path: 'account', model: 'Account'}
    ] );

    req.session.user = Object.assign({}, req.session.user, data);

    flash.add(req, 'Your profile has been successfully saved.', 'success');

    res.send(resp);
  } catch (err) {
    res.status(500).send(User.error());
  }
});

Open in new window



You see that "right here?" I need to check whether or not the user has a value in that field. If they do, that means that they want to change their password.

How do you check for the presence of any characters in that field? I'm thinking "isset" "NULL," but what is that going to look like in Nodejs?

Once I get that concept locked in, the next thing will be to check whether or not they've got their "current password" in place. I'll use the "getOne" function to grab the record, compare values and then go from there.

They do have a "checkPassword" method and it looks like this:

  async checkPasswordHash(password, hash) {
    return new Promise((resolve, reject) => {
      bcrypt.compare(password, hash, function(err, res) {
        if (err) {
          reject(err);
        } else {
            if (res === true) { resolve(); } else { reject();}          
        }
      });
    });
  }

Open in new window


So, how do you put all this mess together?
Hey Bruce,

I can't test, but something along these lines might give you some traction:

let user = await(user.getOne(global._user._id))
console.log(user) // have a look at what was returned in your Browser Console. Hopefully it's a fully formed User!

if (request.body.user.password) {
    // we have a password entry so let's check it.

    // You need access to the password hash on the model. Not sure where that's stored
    // Also, the checkPasswordHash maybe a method on a class, so you might need to edit this
    let passCheck = await checkPasswordHash(request.body.user.password, user.hash);
    if (passCheck.error) {
        // password didn't match so send the error
        return res.send(passCheck);
    }

    // Now save the User (you might need to check that the new password get's hashed properly in the save() method
    let resp = await user.save(global._user._id, req.body.user);
    ...

Open in new window

Dang it, Chris! I'm almost there!

Here's what I've got:

let check = await(user.getOne(global._user._id));
      //console.log(check.password);
      //console.log(req.body.current_password);

      let PassCheck = await user.checkPasswordHash(req.body.current_password, check.password);
      console.log(PassCheck);

Where you can see the commented out "check.password" etc, those are good! So, I've got data to evaluate.

"checkPasswordHash" sits on the "user.js" service and it looks like this:

  async checkPasswordHash(password, hash) {
    return new Promise((resolve, reject) => {
      bcrypt.compare(password, hash, function(err, res) {
        if (err) {
          reject(err);
        } else {
            if (res === true) { resolve(); } else { reject();}          
        }
      });
    });
  }

Open in new window


The code that I've got above yields a result of "undefined."

If I do this:

if(PassCheck) {
            console.log("good");
      }

I get this:

(node:7260) UnhandledPromiseRejectionWarning: ReferenceError: User is not defined
    at C:\wamp64\www\bsmart\server\routes\profile.js:78:26
(node:7260) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:7260) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

I"ve got this at the top of the page:

const user = require('../services/user');

..and I've got this as part of the user.js service:

  async checkPasswordHash(password, hash) {
    return new Promise((resolve, reject) => {
      bcrypt.compare(password, hash, function(err, res) {
        if (err) {
          reject(err);
        } else {
            if (res === true) { resolve(); } else { reject();}          
        }
      });
    });
  }

What am I missing?

Just to ensure that I'm giving you everything you need, I've got the "profile.js" route and the "user.js" service below in their entirety..

profile.js route...

const express = require('express');
const router = express.Router();
const { Util } = require('node-utils');
const flash = require('../lib/Flash');
const user = require('../services/user');
const fs = require('fs');
const AccountManagement = require('../services/account-management');

router.get('/profile', async (req, res) => {
  let vars = {
    selected: '#settings',
    msg: flash.show(req),
    user: await user.getOne( global._user._id )
  };

  res.render('profile.html.twig', vars);
});

router.get('/company-info', async (req, res) => {
    let vars = {
        selected: '#settings',
        msg: flash.show(req),
        account: await AccountManagement.getOne(global._user.account._id)
    };
    res.render('company-info.html.twig', vars);
});
router.post('/company-info', async (req, res) => {
    if (req.body.account == 'undefined') { return res.send(user.error('Invalid request.')); }
    let resp = await AccountManagement.save(global._user.account._id, req.body.account);
    res.send(user.success(resp));
});

router.post('/profile/save', async (req, res) => {
  try {
    if (typeof req.body.user == 'undefined') {
      return res.send(user.error('Invalid request.'));
      }
      //if (req.body.avatar.legth > 0) {
      //    if (req.body.avatar.index('base64') > 0) {
      //        var base64Data = req.body.avatar.replace(/^data:image\/png;base64,/, "");
      //        fs.writeFile("out.png", base64Data, 'base64', function (err) {
      //            console.log(err);
      //        });
      //    }
          
      //}
	
	//checking to ensure current password matches what's in the database before updating profile information
	
	//let user = await(user.getOne(global._user._id));
	//console.log(user);
	
	let check = await(user.getOne(global._user._id));
	//console.log(check.password);
	//console.log(req.body.current_password);

	let PassCheck = await user.checkPasswordHash(req.body.current_password, check.password);
	if(PassCheck) {
		console.log("good");
	}

	
    let resp = await user.save(global._user._id, req.body.user);
    if (resp.error) {
      return res.send(resp);
    }

    let data = await user.getOne( global._user._id, [
      { path: 'account', model: 'Account'}
    ] );

    req.session.user = Object.assign({}, req.session.user, data);

    flash.add(req, 'Your profile has been successfully saved.', 'success');

    res.send(resp);
  } catch (err) {
    res.status(500).send(User.error());
  }
});


module.exports = router;

Open in new window


user.js service

const Service = require('./service');
const Stats = require('../lib/Stats');
const { ObjectID } = require('mongodb');
const moment = require('moment-timezone');
const bcrypt = require('bcrypt');
const mongoose = require('mongoose');
const UserSchema = require('../schemas/UserSchema');
const AccountSchema = require('../schemas/AccountSchema');
const ActivitySchema = require('../schemas/ActivitySchema');
const ProposalSchema = require('../schemas/ProposalSchema');
const Company = require('../services/company.js');
const Prospect = require('../services/prospect.js');

class User extends Service {

  constructor() {
    super();

    this.collection = 'users';
    this.plural = 'users';
    this.singular = 'user';
    this.model = mongoose.model('User', UserSchema);
    this.fields = {
      account: {
        type: 'id',
        required: true,
        lookup: 'accounts'
      },
      firstName: {
        type: 'string',
        required: true
      },
      lastName: {
        type: 'string',
        required: true
      },
      email: {
        type: 'string',
        required: true
      },
      password: {
        type: 'string',
        required: false
      },
      mobile: {
        type: 'string',
        required: false
      },
      settings: {
        type: 'object',
        required: true,
        fields: {
          timezone: {
            type: 'string',
            required: true,
            default: 'America/Chicago'
          }
        }
      }
    };

    this.passwordRegex = {
      upper: /[A-Z]{1,}/,
      lower: /[a-z]{1,}/,
      number: /[0-9]{1,}/,
      special: /[\!\@\#\$\%\^\&\*]{1,}/,
      total: /.{8,}/
    };
  }

    async lastLogin() {
        try {
            this.model.updateOne({ _id: global._user._id }, { $set: { "dates.lastLogin": new Date(moment().toISOString()) } }, { runValidators: true }, (err, raw) => {
                if (err) { return this.error(); }
                return this.success();
            });
        } catch (e) {
            return this.error();
        }
    }
  async auth(email, password) {
    const isDev = (process.env.ENV == 'local' || process.env.ENV == 'dev');
    let match = {
      email: email,
      active: true
    };

    try {
        let user = await this.loadUser(match);        
      // validate the password hash
      if (!isDev) {
        await this.checkPasswordHash(password, user.password);
      }

      return this.success(user);
    } catch (err) {
      console.log('Error: ', err);
      return this.error(err);
    }
  }

  async sso(decryptedToken) {
    try {
      let resp = await this.checkForSSOAccount(decryptedToken);

      // the user already exists so log them in
      if (!resp.error) {
        return resp;
      }

      // create the user from the token details
      return await this.createSSOAccount(decryptedToken);
    } catch (err) {
      return this.error('An unexpected error was encountered while trying to process your sign in.');
    }
  }

  async checkForSSOAccount(decryptedToken) {
    try {
      return await this.loadUser({
        'biq.cpID': decryptedToken.cpID,
        'biq.credID': decryptedToken.credID,
        active: true
      });
    } catch (err) {
      return this.error();
    }
  }

  async createSSOAccount(decryptedToken) {
    let account = {},
        user = {
          firstName: decryptedToken.firstName,
          lastName: decryptedToken.lastName,
          email: decryptedToken.email,
          mobile: decryptedToken.mobile,
          biq: {
            cpID: decryptedToken.cpID,
            credID: decryptedToken.credID
          },
          settings: {
            timezone: 'America/Chicago'
          },
          active : true,
          dates: {
            created: new Date()
          }
        };

    try {
      // ensure we have an account
      account = await this._ensureSSOAccount(cpID);

      // set account id on the user record
      user.account = account._id;

      // create the user
      let userId = await this._createSSOUser(user);

      // update the user record to return
      user.account = account;
      user._id = userId;

      return this.success(user);
    } catch (err) {
      return this.error();
    }
  }

  select2(query) {
    return new Promise((resolve, reject) => {
      this.model.aggregate([
        {'$match': {
          account: global._user.account._id
        }},
        {'$addFields': {
          name: {
            '$concat': [ '$firstName', ' ', '$lastName' ]
            },
            image: {
                '$concat': ['https://ui-avatars.com/api/?name=', '$firstName', '%20', '$lastName', '&background=33b5e5&size=32&color=000000']
            }
        }},
        {'$match': {
          name: {
            '$regex': query,
            '$options': 'i'
          }
        }},
        {'$sort': {
          name: 1
        }},
        {'$limit': 25},
        {'$project': {
          id: '$_id',
          text: '$name',
            image: '$image',
          avatar:'$avatar'
        }}
      ]).then((rows) => {
        resolve(rows);
      }).catch((err) => {
        reject(err);
      });
    });
  }

  getUsersList() {
      return new Promise((resolve, reject) => {
        let pipeline=[
        {$match: {account: global._user.account._id,active:true}},
        {$sort: {lastName: 1,firstName: 1}},
        {$addFields:{name:{"$concat":["$firstName"," ","$lastName"]}}},
        { $unwind:{path: '$team',preserveNullAndEmptyArrays: true} },
        {
            $lookup: {
                from: 'users', let: { user: '$team.user', "active": true }, pipeline: [
                    { $match: { $expr: { $and: [{ $eq: ['$_id', '$$user'] }, { $eq: ['$active', '$$active'] }] } } },
                    { $project: { "name":{"$concat":["$firstName"," ","$lastName"]}, avatar: 1, role: 1, email: 1, dates: 1, active: 1 } }
                ],
                as: 'team'
            }
        },
        { "$project": { "name":"$name", "avatar": 1, "role": 1, "email": 1, "dates": 1, team: 1 } },
        { $unwind:{path: '$team',preserveNullAndEmptyArrays: true} },
        { $group: { _id: {_id:"$_id","name":"$name","role":"$role","avatar":"$avatar"}, team: { $push: "$team" } } },
        {"$project":{_id:"$_id._id","name":'$_id.name',"role":"$_id.role","avatar":"$_id.avatar","team":"$team"}},
        {"$group":{"_id":"$role","users":{"$push":{"_id":"$_id","name":"$name","role":"$role","avatar":"$avatar","team":"$team"}}}},
        {"$project":{"_id":0, "role":"$_id","users":"$users"}}
      ];
      this.model.aggregate(pipeline).then((rows) => {
          let peline = [{ $match: { account: global._user.account._id, active: true } },
          { $addFields: { name: { "$concat": ["$firstName", " ", "$lastName"] } } },
              { "$project": { "name": "$name", "avatar": 1, "role": 1, "email": 1, "dates": 1 } }];
          this.model.aggregate(peline).then((uniqueMembers) => {
              resolve({ uniqueMembers: uniqueMembers, members: rows });
          }).catch((err) => {
              console.log(err); reject(err);
          });
        //resolve(rows);
      }).catch((err) => {
          console.log(err);
        resolve([]);
      });
    });
  }

  getActivity(userId, limit=20) {
    return new Promise((resolve, reject) => {
      const Activity = mongoose.model('Activity', ActivitySchema);

      Activity.aggregate([
        {$match: {
          account: global._user.account._id,
          user: this._getId(userId)
        }},
        {$sort: {
          date: -1
        }},
        {$limit: limit},
        {$lookup: {
          from: 'users',
          let: {
            userId: '$user'
          },
          pipeline: [
            {$match: {
              $expr: {
                $eq: ['$$userId', '$_id']
              }
            }},
            {$project: {
              firstName: 1,
              lastName: 1
            }}
          ],
          as: 'user'
        }},
        {$unwind: '$user'}
      ]).then((rows) => {
        rows.map((row) => {
          row.icon = this.getIconByType(row.type);
        });

        resolve(rows);
      }).catch((err) => {
        reject(err);
      });
    });
  }

  getActiveProposals(userId) {
    return new Promise((resolve, reject) => {
      const Proposal = mongoose.model('Proposal', ProposalSchema);

      Proposal.aggregate([
        {$match: {
          account: global._user.account._id,
          owner: this._getId(userId),
          pipeline: {
            $in: ['target', 'contact', 'contract']
          }
        }},
        {$lookup: {
          from: 'companies',
          let: {
            companyId: '$company'
          },
          pipeline: [
            {$match: {
              $expr: {
                $eq: ['$$companyId', '$_id']
              }
            }},
            {$project: {
              name: '$name',
              ftes: '$meta.ftes'
            }}
          ],
          as: 'company'
        }},
        {$unwind: '$company'},
        {$sort: {
          pipeline: 1,
          pipelineIndex: 1
        }}
      ]).then((rows) => {
        let value = 0;

        rows.forEach((row) => {
          // loop through each service for each row
          row.services.forEach((service) => {
            switch (service.model) {
              case 'perFte':
                value += row.company.ftes * service.amount;
                break;

              case 'oneTime':
                value += service.amount;
                break;

              default:
                return true;
            }
          });
        });

        resolve({
          records: rows,
          totalPipelineValue: value
        });
      }).catch((err) => {
        reject(err);
      });
    });
  }

  getStats(userId, timeframe='30 days') {
    return new Promise(async (resolve, reject) => {
      userId = this._getId(userId);

      let win = await Stats.getWinPercentChart({ owner: userId });
      let sales = await Stats.getSalesCycleLengthChart({ owner: userId });

        resolve({
            winPercent: (typeof win[0].winPercent[0] != 'undefined' ? win[0].winPercent[0].total : '0'),
            avgSalesCycle: (typeof sales[0] != 'undefined' ? sales[0].days:'0')
      });
    });
  }

  renderInfoTemplate(data) {
    return new Promise((resolve, reject) => {
      const Twig = require('twig');

      data.settings = {
        'twig options': {
          async: false
        }
      };

      Twig.renderFile(global._rootPath + '/views/partials/includes/user-info.html.twig', data, (err, html) => {
        if (err) {
          reject(err);
        } else {
          resolve(html);
        }
      });
    });
  }

  _ensureSSOAccount(account) {
    return new Promise((resolve, reject) => {
      const Account = mongoose.model('Account', AccountSchema);

      // check for an existing account
      Account.find({
        'biq.cpID': decryptedToken.cpID
      }).exec((err, rows) => {
        if (err) {
          reject(err);
        } else if (rows.length === 0) {
          // create the account
          Account.create({
            name: `bIQ Channel Partner ${decryptedToken.cpID}`,            
            biq: {
              cpID: decryptedToken.cpID
            },
            postalCodes: [ '00000' ],
            active: true,
            dates: {
              created: new Date()
            }
          }, (err, record) => {
            if (err) {
              reject(err);
            } else {
              resolve(record);
            }
          });
        } else {
          resolve(rows[0]);
        }
      });
    });
  }

  _createSSOUser(user) {
    return new Promise((resolve, reject) => {
      this.model.create(user, (err, record) => {
        if (err) {
          reject(err);
        } else {
          resolve(record._id);
        }
      });
    });
  }

    _startPipeline(pipeline) {
        //Super user
        //if (typeof global._user.su != 'undefined') {
          //  if (global._user.su == true) {
                pipeline.push({ $lookup: { from: "accounts", localField: "account", foreignField: "_id", as: "account" } });
                pipeline.push({ $unwind: "$account" });
            //}
        //}
    // combine the name for simple searching
    pipeline.push({
      $addFields: {
        name: {
          $concat: ['$firstName', ' ', '$lastName']
        }
      }
    });

    return pipeline;
  }

  _beforeSave(data, id) {
    return new Promise(async (resolve, reject) => {
      // validate the data provided
      if ((id === '' || id == 'new') && typeof data.password == 'undefined') {
        // throw an error because password is required for new users
        reject('Please provide a password for new users.');
      }

      // enforce the scope here
      data.account = global._user.account._id;

      // validate and encrypt the password if it is provided or the record is new
      if (typeof data.password != 'undefined' && data.password !== '') {
        data.password = await this.validateAndEncryptPassword(data.password);
        if (!data.password) {
          return reject('The provided password does not meet our minimum requirements. Please try again.');
        }
      } else if (typeof data.password != 'undefined') {
        // remove it from the object for the save
        delete data.password;
      }

      resolve(data);
    });
  }

  async validateAndEncryptPassword(pass) {
    for (var k in this.passwordRegex) {
      if (!this.passwordRegex[k].test(pass)) {
        return false;
      }
    }

    try {
      return await this.encryptPassword(pass);
    } catch (err) {
      console.log('Hash Error: ', err);
      return false;
    }
  }

  async encryptPassword(pass) {
    return new Promise((resolve, reject) => {
      bcrypt.hash(pass, 10, function(err, hash) {
        if (err) {
          reject(err);
        } else {
          resolve(hash);
        }
      });
    });
  }

  async checkPasswordHash(password, hash) {
    return new Promise((resolve, reject) => {
      bcrypt.compare(password, hash, function(err, res) {
        if (err) {
          reject(err);
        } else {
            if (res === true) { resolve(); } else { reject();}          
        }
      });
    });
  }

  loadUser(match={}) {
    return new Promise( async (resolve, reject) => {
      this.model.aggregate([
        {$match: match},
        {$limit: 1},
        {$lookup: {
          from: 'accounts',
          localField: 'account',
          foreignField: '_id',
          as: 'account'
        }},
        {$unwind: '$account'}
      ]).then(async (data) => {
        if (data.length === 0) {
          return reject( 'The provided email address and password combination is invalid. Please try again. If you need further assistance, please call 855.581.9910.' );
        } else if (!data[0].account.active) {
          return reject( 'The parent account is no longer active. Please contact your account administrator.' );
          }           
          let pipelineColumns = await this.getPipelineColumns(data[0].account._id);          
          if (pipelineColumns == null || pipelineColumns.length == 0) {
              // Seed built-in user specific pipelineColumn
              pipelineColumns = await this.seedBuiltInPipelineColumns(data[0].account._id);              
          }
          data[0].account.pipelineColumns = pipelineColumns;
          data[0].avatar = typeof data[0].avatar != 'undefined' ? data[0].avatar.length == 0 ? 'https://ui-avatars.com/api/?name=' + data[0].firstName + '%20' + data[0].lastName + '&background=33b5e5&size=200&color=000000' : data[0].avatar : data[0].avatar;
          resolve(data[0]);
      }).catch((err) => {
        console.log('Error: ', err);
        reject( 'An unexpected error was encountered. Please try again.' );
      });
    });
    }
    save(id, data) {
        return new Promise(async (resolve, reject) => {
            try {               
                // run the lifecycle method               
                data = await this._beforeSave(data, id);
            } catch (err) {
                console.log('Save Error: ', err);
                reject(err);
            }

            // save or update appropriately based on the id provided
            if (id === '' || id == 'new') {
                // insert a new record
                this.model.create(data, (err, record) => {
                    if (err) {
                        reject(err);
                    } else {
                        resolve(record._id);
                    }
                });
            } else {
                // check the id provided
                id = this._getId(id);
                if (!id) {
                    reject('Invalid record ID provided.');
                }

                // update the record
                this.model.updateOne({ _id: id }, { '$set': data }, { runValidators: true }, (err) => {
                    if (err) {
                        reject(err);
                    } else {
                        resolve(id);
                    }
                });
            }
        });
    }
    getUser(id) {
        return new Promise(async (resolve, reject) => {
            id = this._getId(id);
            this.model.findOne({  _id: id }).exec((err, record) => {
                if (err) {
                    console.log('Populate Error: ', err);
                    reject(err);
                } else {
                    resolve(record);
                }
            });

        });
    }
    getTeamByRole(id,role) {
        return new Promise(async (resolve, reject) => {
            
            let pipeline = [
                { $match: { account: global._user.account._id, role: (role == 'Sales Manager' ? 'Sales Representative' : (role == 'Sales Representative' ?'Sales Manager':role)), active: true } },
                    { $project: { firstName: 1, lastName: 1, avatar: 1, role: 1, email: 1, dates: 1 } }
                ];
                this.model.aggregate(pipeline).then((available) => {
                    let data = { team: [], members: available };
                    if (id == 'new') {
                        resolve(data);
                    } else {
                        id = this._getId(id);
                        if (role == 'Sales Manager') {
                            pipeline = [
                                { $match: { _id: id, account: global._user.account._id } },
                                { $unwind: '$team' },
                                {
                                    $lookup: {
                                        from: 'users', let: { user: '$team.user', "active": true }, pipeline: [
                                            {
                                                $match: {
                                                    $expr: {
                                                        $and: [
                                                            { $eq: ['$_id', '$$user'] },
                                                            { $eq: ['$active', '$$active'] }
                                                        ]
                                                    }
                                                }
                                            },
                                            { $project: { firstName: 1, lastName: 1, avatar: 1, role: 1, email: 1, dates: 1, active: 1 } },
                                        ],
                                        as: 'team'
                                    }
                                },
                                { "$project": { "firstName": 1, "lastName": 1, "avatar": 1, "role": 1, "email": 1, "dates": 1, team: 1 } },
                                { $unwind: '$team' },
                                { $group: { _id: "$_id", team: { $push: "$team" } } }
                            ];
                            this.model.aggregate(pipeline).then((user) => {
                                let data = { team: (user[0] ? user[0].team : []), members: available };                               
                                resolve(data);
                            }).catch((err) => {
                                console.log('Get Team: ', err);
                                reject(err);
                            });
                        } else if (role == 'Sales Representative') {
                            pipeline = [
                                { $match: { 'team.user': id, account: global._user.account._id, role: 'Sales Manager', active: true } },
                                { $project: { firstName: 1, lastName: 1, avatar: 1, role: 1, email: 1, dates: 1 } }
                            ];
                            this.model.aggregate(pipeline).then((managers) => {
                                let data = { team: managers, members: available };
                                resolve(data);
                            }).catch((err) => {
                                console.log('Get Team: ', err);
                                reject(err);
                            });
                        }
                    }                    
                }).catch((err) => {
                    console.log(err);
                    reject(err);
                });
            
        });
    }
    // We need to get both active and inactive user list
    _beforeDatatablesPipeline(pipeline) {
        delete pipeline[0]['$match'].active;
        return pipeline;
    }
    reporting(opt = {}) {
        return new Promise((resolve, reject) => {
            try {
                let ids = [];
                if (typeof opt.user != 'undefined') {
                    opt.user.forEach((id) => { ids.push(this._getId(id)); });
                }
                let lowerDate, higherDate;
                if (typeof opt._range != 'undefined') {
                    let dates = opt._range.split('-');                    
                    if (typeof dates != 'undefined') {
                        if (typeof dates[0] != 'undefined' && typeof dates[1] != 'undefined') {
                            lowerDate = new Date(moment(dates[0].trim(), 'MM/DD/YYYY'));
                            higherDate = new Date(moment(dates[1].trim(), 'MM/DD/YYYY').add('1', 'days'));
                            //opt.date = {
                            //    "$gte": lowerDate,
                            //    "$lte": higherDate
                            //};
                        }
                    }
                }
                let pipeline = [
                    { $match: { active: true, _id: { $in: ids }, account: global._user.account._id } },
                    { $lookup: { from: "companies", let: { ownerId: '$_id', active: true, lDate: lowerDate, hDate: higherDate }, pipeline: [{ $match: { $expr: { $and: [{ $eq: ['$active', '$$active'] }, { $eq: ['$owner', '$$ownerId'] }, { "$gte": ["$dates.assigned", "$$lDate"] }, { "$lte": ["$dates.assigned", "$$hDate"] }] } } }, { $project: { _id: 1 } }], as: "companies" } },
                    { $lookup: { from: "activities", let: { userId: '$_id', active: true, lDate: lowerDate, hDate: higherDate }, pipeline: [{ $match: { $expr: { $and: [{ $eq: ['$active', '$$active'] }, { $eq: ['$user', '$$userId'] }, { $in: ['$type', ["call", "visit", "reminder", "event", "email"]] }, { "$gte": ["$date", "$$lDate"] }, { "$lte": ["$date", "$$hDate"] }] } } }, { $group: { _id: '$type', count: { $sum: 1 } } }], as: 'activities' } },
                    {
                        $lookup: {
                            from: "proposals", let: { ownerId: '$_id', active: true, lDate: lowerDate, hDate: higherDate }, pipeline: [
                                { $match: { $expr: { $and: [{ $eq: ['$active', '$$active'] }, { $eq: ['$owner', '$$ownerId'] }, { "$gte": ["$_pipelineDate", "$$lDate"] }, { "$lte": ["$_pipelineDate", "$$hDate"] }] } } },
                                { $lookup: { from: "companies", let: { companyId: '$company' }, pipeline: [{ $match: { $expr: { $eq: ['$_id', '$$companyId'] } } }, { $project: { _id: 0, 'meta.ftes': 1 } }], as: "company" } },
                                { $unwind: '$company' },
                                { "$unwind": { "path": "$services", "preserveNullAndEmptyArrays": true } },
                                { "$addFields": { "services.price": { "$ifNull": ["$services.amount", "$services.price"] } } },
                                //{ $unwind: '$services' },
                                {
                                    $addFields: {
                                        'services.value': {
                                            $switch: {
                                                branches: [
                                                    { case: { $eq: ['$services.model', 'perFte'] }, then: { $multiply: ['$services.numOfEmp', '$services.price'] } },
                                                    { case: { $eq: ['$services.model', 'perPerson'] }, then: { $multiply: ['$services.numOfEmp', '$services.price'] } },
                                                    { case: { $eq: ['$services.model', 'oneTime'] }, then: { $multiply: ['$services.numOfEmp', '$services.price'] } },
                                                    //{ case: { $eq: ['$services.model', 'perMonth'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } },
                                                    //{ case: { $eq: ['$services.model', 'perQuarter'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } },
                                                    //{ case: { $eq: ['$services.model', 'semiAnnually'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } },
                                                    //{ case: { $eq: ['$services.model', 'perYear'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } }
                                                ], default: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] }
                                            }
                                        }
                                    }
                                },
                                { $group: { _id: '$pipeline', value: { $sum: '$services.value' }, count: { $addToSet: '$_id' } } },
                                { $project: { _id: 0, pipeline: '$_id', value: '$value', count: {$size:'$count'} } },
                                { $lookup: { from: 'pipelinecolumns', let: { pipelineId: '$pipeline' }, pipeline: [{ $match: { $expr: { $eq: ['$_id', '$$pipelineId'] } } }, { $project: { _id: 0, label: 1 } }], as: 'pipeline' } },
                                { $unwind: '$pipeline' }
                            ], as: "proposals"
                        }
                    },
                    { $project: { firstName: 1, lastName: 1, avatar: 1, email: 1, companies: 1, activities: 1, proposals: 1 } }
                ];
                this.model.aggregate(pipeline).then(async (rows) => {
                    let totalCompanies = await Company.countDocuments({ active: true });
                    let targetedProspects = { count: 0, companiesInMarket: 0, percentage: 0 };
                    targetedProspects.count = await Company.countDocuments({ active: true, market: { '$exists': true } });
                    targetedProspects.companiesInMarket = await Prospect.countDocumentsByPostal();
                    targetedProspects.percentage = ((targetedProspects.count / targetedProspects.companiesInMarket) * 100).toFixed(1);
                    resolve({ rows: rows, pipelineColumns: await this.getPipelineColumns(global._user.account._id, true), totalCompanies: totalCompanies, targetedProspects: targetedProspects });
                }).catch((error) => {
                    console.log(error);
                    reject(error);
                });
            } catch (e) {
                console.log(e);
                reject(e);
            }
        });
    }

    exportToCSV(opt = {}) {
        return new Promise((resolve, reject) => {
            let ids = [];
            if (typeof opt.assigned != 'undefined') {
                opt.assigned.forEach((id) => { ids.push(this._getId(id)); });
            }
            let lowerDate, higherDate;
            if (typeof opt._range != 'undefined') {
                let dates = opt._range.split('-');
                if (typeof dates != 'undefined') {
                    if (typeof dates[0] != 'undefined' && typeof dates[1] != 'undefined') {
                        lowerDate = new Date(moment(dates[0].trim(), 'MM/DD/YYYY'));
                        higherDate = new Date(moment(dates[1].trim(), 'MM/DD/YYYY').add('1', 'days'));
                    }
                }
            }
            let pipeline = [
                { $match: { active: true, _id: { $in: ids }, account: global._user.account._id } },
                { $lookup: { from: "companies", let: { ownerId: '$_id', active: true, lDate: lowerDate, hDate: higherDate }, pipeline: [{ $match: { $expr: { $and: [{ $eq: ['$active', '$$active'] }, { $eq: ['$owner', '$$ownerId'] }, { "$gte": ["$dates.assigned", "$$lDate"] }, { "$lte": ["$dates.assigned", "$$hDate"] }] } } }, { $project: { _id: 1,name:1,'meta.ftes':1 } }], as: "companies" } },                
                {
                    $lookup: {
                        from: "proposals", let: { ownerId: '$_id', active: true, lDate: lowerDate, hDate: higherDate }, pipeline: [
                            { $match: { $expr: { $and: [{ $eq: ['$active', '$$active'] }, { $eq: ['$owner', '$$ownerId'] }, { "$gte": ["$_pipelineDate", "$$lDate"] }, { "$lte": ["$_pipelineDate", "$$hDate"] }] } } },
                            { $lookup: { from: "companies", let: { companyId: '$company' }, pipeline: [{ $match: { $expr: { $eq: ['$_id', '$$companyId'] } } }, { $project: { _id: 1, name:1, 'meta.ftes': 1 } }], as: "company" } },
                            { $unwind: '$company' },
                            { $unwind: '$services' },                            
                            { "$addFields": { "services.price": { "$ifNull": ["$services.amount", "$services.price"] } } },//old schema has a field with name AMOUNT
                            {
                                $addFields: {
                                    'services.value': {
                                        $switch: {
                                            branches: [
                                                { case: { $eq: ['$services.model', 'perFte'] }, then: { $multiply: ['$services.numOfEmp', '$services.price'] } },
                                                { case: { $eq: ['$services.model', 'perPerson'] }, then: { $multiply: ['$services.numOfEmp', '$services.price'] } },
                                                { case: { $eq: ['$services.model', 'oneTime'] }, then: { $multiply: ['$services.numOfEmp', '$services.price'] } },
                                                //{ case: { $eq: ['$services.model', 'perMonth'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } },
                                                //{ case: { $eq: ['$services.model', 'perQuarter'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } },
                                                //{ case: { $eq: ['$services.model', 'semiAnnually'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } },
                                                //{ case: { $eq: ['$services.model', 'perYear'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } }
                                            ], default: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] }
                                        }
                                    }
                                }
                            },
                            { $group: { _id: '$pipeline', value: { $sum: '$services.value' }, count: { $sum: 1 } } },
                            { $project: { _id: '$_id', pipeline: '$pipeline', name:'$name', value: '$value', count: '$count' } },
                            { $lookup: { from: 'pipelinecolumns', let: { pipelineId: '$pipeline' }, pipeline: [{ $match: { $expr: { $eq: ['$_id', '$$pipelineId'] } } }, { $project: { _id: 0, label: 1 } }], as: 'pipeline' } },
                            { $unwind: '$pipeline' }
                        ], as: "proposals"
                    }
                },
                { $project: { sales_rep: {$concat:['$firstName',' ','$lastName']}, email: 1, companies: 1, proposals: 1 } }
            ];
            pipeline=[
                { $match: { active: true, _id: { $in: ids }} },
                { $lookup: { from: "companies", let: { ownerId: '$_id', active: true, lDate: lowerDate, hDate: higherDate }, pipeline: [{ $match: { $expr: { $and: [{ $eq: ['$active', '$$active'] }, { $eq: ['$owner', '$$ownerId'] }, { "$gte": ["$dates.assigned", "$$lDate"] }, { "$lte": ["$dates.assigned", "$$hDate"] }] } } }, { $project: { _id: 1,name:1,'meta.ftes':1 } }], as: "companies" } },                
                {
                    $lookup: {
                        from: "proposals", let: { ownerId: '$_id', active: true, lDate: lowerDate, hDate: higherDate}, pipeline: [
                            { $match: { $expr: { $and: [{ $eq: ['$active', '$$active'] }, { $eq: ['$owner', '$$ownerId'] }, { "$gte": ["$_pipelineDate", "$$lDate"] }, { "$lte": ["$_pipelineDate", "$$hDate"] }] } } },
                            { $lookup: { from: "companies", let: { companyId: '$company' }, pipeline: [{ $match: { $expr: { $eq: ['$_id', '$$companyId'] } } }, { $project: { _id: 1, name:1, 'meta.ftes': 1 } }], as: "company" } },
                            { $unwind: '$company' },                                                        
                            { $lookup: { from: 'pipelinecolumns', let: { pipelineId: '$pipeline' }, pipeline: [{ $match: { $expr: { $eq: ['$_id', '$$pipelineId'] } } }, { $project: { _id: 0, label: 1 } }], as: 'pipeline' } },
                            { $unwind: '$pipeline' },
                            {$addFields:{'pipeline':'$pipeline.label'}},
                            {$project:{files:0,dates:0}}, 
                          	{$unwind:'$services'},
                          	{$lookup:{from:'services',let:{serviceId:'$services.service'},pipeline:[{$match:{$expr:{$eq:['$_id','$$serviceId']}}},{$project:{name:1}}],as:'services.service'}},
                          	{$unwind:'$services.service'},
                          	{$addFields:{'services.service':'$services.service.name','services._id':'$services.service._id'}}                           
                        ], as: "proposals"
                    }
                },
                { $project: { sales_rep: {$concat:['$firstName',' ','$lastName']}, email: 1, companies: 1, proposals: 1 } }
            ];          

            // get the data
            this.model.aggregate(pipeline).then((rows) => {
                let rec = [];
                if (rows.length > 0) {
                    rows.forEach((row) => {                        
                        if (row.proposals.length > 0) {
                            row.proposals.forEach((p) => {
                                let data = {
                                    proposal_id: p._id,
                                    swimlane: p.pipeline,
                                    proposal_date: p._pipelineDate,
                                    proposal_name: p.name,
                                    service_id: p.services._id,
                                    service_name: p.services.service,
                                    service_price: p.services.amount || p.services.price,
                                    service_model: p.services.model,
                                    service_numOfEmp: p.services.numOfEmp,
                                    service_period: p.services.serviceDates,
                                    service_duration: p.services.duration,
                                    company_id: p.company._id,
                                    company_name: p.company.name,
                                    company_ftes: p.company.meta.fte,
                                    sales_rep: row.sales_rep,
                                    sales_rep_email: row.email
                                };
                                rec.push(data);
                            });
                        }
                        row.companies.forEach((company) => {
                            let data = {
                                proposal_id: '', swimlane: 'Target', proposal_date: '', proposal_name: '', service_id: '', service_name: '', service_price: '', service_model: '', service_numOfEmp: '', service_period: '', service_duration: '',
                                company_id: '', company_name: '', company_ftes: '', sales_rep: row.sales_rep, sales_rep_email: row.email
                            };
                            data.company_id = company._id;
                            data.company_name = company.name;
                            data.company_ftes = company.meta ? company.meta.ftes : 0;
                            rec.push(data);
                        });

                    });
                }
                const papaparse = require('papaparse');
                //console.log('Data: ', JSON.stringify(rec, undefined, 2));
                let csv = papaparse.unparse(rec);

                resolve(csv);
            }).catch((err) => {
                reject(err);
            });
        });
    }

    saveDashboardSettings(data) {
        return new Promise(async (resolve, reject) => {
            try {
                this.model.findOneAndUpdate({ _id: global._user._id }, { dashboard_settings: data }, (err, res) => {
                    if (err) { console.log(err); reject(err); }
                    resolve(res._id);
                });
            } catch (e) {
                console.log(e);
                reject(e);
            }
        });
    }
    getMainDashboard(opts = {}) {
        return new Promise(async (resolve, reject) => {
            try {
                let ids = [], lowerDate, higherDate;
            if (typeof opts.owner != 'undefined') {
                opts.owner.forEach((id) => { ids.push(this._getId(id)); });
            }
            if (typeof opts._range != 'undefined') {
                let dates = opts._range.split('-');
                if (typeof dates != 'undefined') {
                    if (typeof dates[0] != 'undefined' && typeof dates[1] != 'undefined') {
                        lowerDate = new Date(moment(dates[0].trim(), 'MM/DD/YYYY'));
                        higherDate = new Date(moment(dates[1].trim(), 'MM/DD/YYYY').add('1', 'days'));                      
                    }
                }
            }
            pipeline=[
                {"$match":{"account":global._user.account._id,"active":true, _id: { $in: ids }}},
                {$project:{firstName:1,lastName:1}},
                {$lookup:{from:'proposals',let:{'ownerId':'$_id','active':true,'lDate': lowerDate, 'hDate': higherDate},pipeline:[
                  {$match:{$expr:{$and:[
                    {$eq:['$owner','$$ownerId']},
                    {$eq:['$active','$$active']},
                    {$gte: ["$_pipelineDate", "$$lDate"] }, 
                    {$lte: ["$_pipelineDate", "$$hDate"] }
                    ]}}},
                    {'$lookup': {from: 'companies',localField: 'company',foreignField: '_id',as: 'company'}},
                    { '$unwind': '$company' },
                    {$unwind: { path: '$services', preserveNullAndEmptyArrays: true }},
                    {"$addFields":{"services.price":{"$ifNull":["$services.amount","$services.price"]}}},
                    {"$project":{
                                  "proposal":"$_id",
                                  "pipeline":"$pipeline",
                                  'company':'$company._id',
                                  "value":{$switch: {
                                    branches: [
                                    { case: { $eq: ['$services.model', 'perFte'] }, then: { $multiply: ['$services.numOfEmp', '$services.price'] } },
                                    { case: { $eq: ['$services.model', 'perPerson'] }, then: { $multiply: ['$services.numOfEmp', '$services.price'] } },
                                    { case: { $eq: ['$services.model', 'oneTime'] }, then: { $multiply: ['$services.numOfEmp', '$services.price'] } },
                                    { case: { $eq: ['$services.model', 'perMonth'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } },
                                    { case: { $eq: ['$services.model', 'perQuarter'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } },
                                    { case: { $eq: ['$services.model', 'semiAnnually'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } },
                                    { case: { $eq: ['$services.model', 'perYear'] }, then: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] } }
                                    ], default: { $multiply: ['$services.numOfEmp', '$services.price', '$services.duration'] }
                                    }}
                     }}   
                  ],as:'proposals'}}  
                ];
                this.model.aggregate(pipeline).then(async(rows) => {
                    let stages = await this.getPipelineColumns('', true);
                    let uniqueCompanies = 0;
                    rows.forEach((user) => {
                        
                    });

                }).catch((error) => {
                    console.log(error);
                    reject(error);
                });
            } catch (e) {
                console.log(e);
                reject(e);
            }
        });
    }
    includeUserInSalesManagersAccount(salesRepId, ManagersId = []) {
        return new Promise(async (resolve, reject) => {
            if (typeof salesRepId == 'undefined') { reject('Invalid sales represantative Id provided.'); }
            // check the id provided
            salesRepId = this._getId(salesRepId);
            if (!salesRepId) {
                reject('Invalid record ID provided.');
            }

            // update the record
            this.model.updateOne({ _id: { $in: ManagersId }, account: global._user.account._id }, { '$addToSet': { team: { user: salesRepId }} }, { runValidators: true }, (err) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(salesRepId);
                }
            });
        });
    }

}
module.exports = new User();

Open in new window


Thanks, Chris!
Hey Bruce,

Not sure which bit is resulting in undefined, but one thing that jumps out is this bit:

UnhandledPromiseRejectionWarning: ReferenceError: User is not defined

If you look at the catch block, you'll see this:

res.status(500).send(User.error());

Notice that User has a capital U, but you've imported user with a lowercase u, so change your catch block to:

res.status(500).send(user.error());

and that should get rid of the undefined error.
Chris, the "undefined" error is gone! So that dragon has been slayed.

However...

I'm getting this:

(node:10380) UnhandledPromiseRejectionWarning: ReferenceError: User is not defined

The error is coming from this line of code:

let checkEmail = await(user.checkPasswordHash(check.password, user.password));

I think I know what's wrong... go with me...

There's a line of code that happens just before the aforementioned problem which looks like this:

let check = await(user.getOne(global._user._id));

"user" is the thing. That's the service that's being referenced at the top of this page. It works and it's beautiful.

What's significant, at least to someone like me, who's still following his nose a lot of the times, is that "getOne" is not found in "user.js." Rather, it's found in "service.js" which is referenced at the top of "user.js..."

const Service = require('./service');

"getOne" is documented as "getOne" on that page.

As a matter of fact, most of the methods, be they located on "service.js" or "user.js" are written either "save" or "getOne." But then when I try to access the  "checkPasswordHash" method located on the "user.js" page, it's this:

  async checkPasswordHash(password, hash) {
    return new Promise((resolve, reject) => {
      bcrypt.compare(password, hash, function(err, res) {
        if (err) {
          reject(err);
        } else {
            if (res === true) { resolve(); } else { reject();}          
        }
      });
    });
  }

Does "async" matter as far as the way you would reference it?

Again, this is how I'm attempting to access it:

let checkEmail = await(user.checkPasswordHash(check.password, user.password));

It's this line of code and only this line of code that triggers the "User is not defined" error.

What to you think?
Chris, here's an update:

I tried this:

let quiz = await(encryptPassword(req.body.password));

I'm not getting an error in my console, instead I'm getting an error on the page from "user-form.js" which is this:

    $form.on('submit', function (e) {
        let teamArray = [];
        $form.find('#drag-team').find('a').each(function () {
            let uid = $(this).attr('data-user-id');
            if (uid) {teamArray.push(uid);}
        });
        teamArray = $.unique(teamArray);
        $form.find('#user-team').val(JSON.stringify(teamArray));
        $form.find('#user-avatar').val() ? $form.find('#user-avatar').val() : $form.find('#user-avatar').val('https://ui-avatars.com/api/?name=' + $form.find('#user-first-name').val() + '%20' + $form.find('#user-last-name').val()+'&background=33b5e5&size=200&color=000000');
    var $f = $(this),
        $btn = $f.find('button[type="submit"]'),
        html = $btn.html(),
        formData = $f.serialize(),
          route = (loadedUserId !== '') ? $form.attr('action') + '/' + loadedUserId : $form.attr('action');
      // include unchecked checkboxes. use filter to only include unchecked boxes.
      $.each($('.permission-section input[type=checkbox]').filter(function (idx) {
          return $(this).prop('checked') === false;
      }),
          function (idx, el) {
              // attach matched element names to the formData with a chosen value.                
              formData += '&' + $(el).attr('name') + '=' + false;
          }
      );
      

    e.preventDefault();
    e.stopPropagation();

    $f.addClass('was-validated');

    if ($f[0].checkValidity() === false) {
      addMsg('Please check the highlighted fields!');
      return false;
    }

    $btn.html('<i class="fas fa-fw fa-sync-alt fa-spin"></i> Saving...');

    // validate the form and save
    $.post(route, formData, function(resp){
      if (resp.error) {
        clearMsg();
        addMsg(resp.msg);
        return false;
      }

      // finish up
      $(window).trigger('user-form-hide');
      $(window).trigger('user-form-complete', [formData]);
    }, 'json').fail(function(resp){
      clearMsg();
      addMsg('An unexpected error was encountered while trying to process your request. Please try again.');
    }).always(function(){
      $btn.html(html);
    });
  });

Open in new window


I'm getting the "unexpected error was encountered while trying to process your request."

Is there any way I can qualify that error message so it better shows what's going on under the hood?
Hi Bruce,

I'll attempt to explain :)

First off, let's look at the Classes you're using. At the top of your profile.js file, you have this:

const user = require('../services/user');

So that's loading up the services/user.js file and storing it in a variable called user (you could just as easily store it in a variable called person or widget etc). Now if you look at the services/user.js file you'll see a few things of importance. At the top you have this:

const Service = require('./service');

so that's loading up the service.js file and storing it in a variable called Service. You then move onto your main User class, which is defined like so:

class User extends Service {

So now your User class will have all the methods and properties you define within the User class, plus any that are defined in the Service class. In Object Oriented terms, it's called inheritance. That's why your User has a getOne method - it's inherited it from a Parent class (Service).

Now, right at the end of the user.js file, you'll see this :

module.exports = new User();

What this does is instantiate (posh word for create) a new instance of the User class and export that from the User file. So in your profile.js file, when you see this:

const user = require('../services/user');

What you're actually getting is something along the lines of :

const user = new User();

and we know the User class inherits from the Service class, so your user variable has all the methods from User and Service!

Following so far ?



In your code you're calling the checkPasswordHash like so:

user.checkPasswordHash(check.password, user.password)

You shold be passing in the Users current password (stored in the DB as a hash?) along with what was entered by the User in the request, so something like this maybe:

user.checkPasswordHash(request.body.user.password, check.password);

I think that's what's throwing your Exception, but I'm a little confused as to why it's still staying User is not defined if you've already change the case on it.

Change your catch block and see if that get's you further forward:

} catch (err) {
    res.status(500).send(err);
}

Open in new window

Ahh - we cross posted.

Yeah you should be able to get some additional info. When you AJAX call fails, it passes the server response back in the resp variable, so you could console.log that and see what it says:

$.post(route, formData, function(resp){
    ...
}, 'json')
.fail(function(resp){
    console.log(resp);
    clearMsg();
    addMsg('An unexpected error was encountered while trying to process your request. Please try again.');
})
.always(function(){
    $btn.html(html);
});

Open in new window

Chris!

Nothing shows up in Git Bash...

But when I do a Google Inspect, I get this:

User generated image
Here's the code that I used...

$form.on('submit', function (e) {
        let teamArray = [];
        $form.find('#drag-team').find('a').each(function () {
            let uid = $(this).attr('data-user-id');
            if (uid) {teamArray.push(uid);}
        });
        teamArray = $.unique(teamArray);
        $form.find('#user-team').val(JSON.stringify(teamArray));
        $form.find('#user-avatar').val() ? $form.find('#user-avatar').val() : $form.find('#user-avatar').val('https://ui-avatars.com/api/?name=' + $form.find('#user-first-name').val() + '%20' + $form.find('#user-last-name').val()+'&amp;background=33b5e5&amp;size=200&amp;color=000000');
    var $f = $(this),
        $btn = $f.find('button[type="submit"]'),
        html = $btn.html(),
        formData = $f.serialize(),
          route = (loadedUserId !== '') ? $form.attr('action') + '/' + loadedUserId : $form.attr('action');
      // include unchecked checkboxes. use filter to only include unchecked boxes.
      $.each($('.permission-section input[type=checkbox]').filter(function (idx) {
          return $(this).prop('checked') === false;
      }),
          function (idx, el) {
              // attach matched element names to the formData with a chosen value.                
              formData += '&' + $(el).attr('name') + '=' + false;
          }
      );
      

    e.preventDefault();
    e.stopPropagation();

    $f.addClass('was-validated');

    if ($f[0].checkValidity() === false) {
      addMsg('Please check the highlighted fields!');
      return false;
    }

    $btn.html('<i class="fas fa-fw fa-sync-alt fa-spin"></i> Saving...');

    // validate the form and save
    $.post(route, formData, function(resp){
      if (resp.error) {
        clearMsg();
        addMsg(resp.msg);
        return false;
      }

      // finish up
      $(window).trigger('user-form-hide');
      $(window).trigger('user-form-complete', [formData]);
    }, 'json')
	.fail(function(resp){
	  console.log(resp);
      clearMsg();
      addMsg('An unexpected error was encountered while trying to process your request. Please try again');
    }).always(function(){
      $btn.html(html);
    });
  });

Open in new window


Not sure if that's giving you what you need...
OK. That doesn't really tell you a lot other than the AJAX call to 'route' failed with 500 Server Error, so you'll need to look at whatever's going on at that route and see why the script is failing.
OK, Chris!

I've been able to go back and break some things down by commenting out all of the code save what it is that I know to be producing the error.

Here's what I've been able to establish:

This works:

let check = await(user.getOne(global._user._id);
console.log(check.password)


This also works:

console.log(check.password);

let quiz = await(user.encryptPassword(req.body.current_password));
console.log(quiz);


Here's the method as it looks on "user.js"

async encryptPassword(pass) {
    return new Promise((resolve, reject) => {
      bcrypt.hash(pass, 10, function(err, hash) {
        if (err) {
          reject(err);
        } else {
          resolve(hash);
        }
      });
    });
  }


This was a big deal this morning because I wasn't confident that I was actually reaching the "user.encryptPassword" method, but I was able to see in the console two valid encrypted passwords, so that's a good day!

At this point, I thought I had this issue in the bag. Now it was only a matter of comparing the encrypted current password with the password as it exists in the database...

But they're not the same.

Even though I'm entering the current password in the current password field, when it's encrypted on the spot it doesn't match what's in the database.

Let me say that again.

I know, for example, that the password as it's documented in the database is "Drums3t!." I enter "Drums3t" in the current password field, encrypt it using the "encryptPassword" method, compare it to what's in the database and...

...it's not the same.

I'm thinking that's because (and I'm speculating) that when you encrypt something, there's a timestamp involved so a freshly encrypted password, even using the right hash, isn't going to match the same previously encrypted password as it's stored in the database (https://dev.to/nedsoft/a-simple-password-hash-implementation-3hcg).

Correct?

Now, the good news is that in "user.js" there's a "checkPasswordHash" method that looks like this:

  async checkPasswordHash(password, hash) {
    return new Promise((resolve, reject) => {
      bcrypt.compare(password, hash, function(err, res) {
        if (err) {
          reject(err);
        } else {
            if (res === true) { resolve(); } else { reject();}          
        }
      });
    });
  }


I saw it used earlier in "user.js" in the context of the "auth" method:

  async auth(email, password) {
    const isDev = (process.env.ENV == 'local' || process.env.ENV == 'dev');
    let match = {
      email: email,
      active: true
    };

    try {
        let user = await this.loadUser(match);        
      // validate the password hash
      if (!isDev) {
        await this.checkPasswordHash(password, user.password);
      }

      return this.success(user);
    } catch (err) {
      console.log('Error: ', err);
      return this.error(err);
    }
  }


Do you smell that, Chris? I'm thinking this is the method I'm looking for!

But...!

When I try to do this:

let quiz = await(user.checktPasswordHash(req.body.current_password, check.password));

I get this:

{"error":true,"data":[]}

When I look at the arguments the "checkPasswordHash" method is taking, I see "password, hash."

I can see that my passing in two passwords might throw an error, but then that seems to be the very thing the "auth" method is doing, so...

What do you think?

We're getting closer, man! Thanks for you all your help!
Chris!

Never mind! I figured it out! Here's the last thing and I'm out the door!

Here's my route:

let the_password=check.password;
let the_current_password = req.body.current_password;

let quiz = await(user.confirmPassword(the_current_password, the_password));


Here's my service:

async confirmPassword(password, hash) {
        //console.log(password);
        //console.log(hash);
        let result="";
    return new Promise((resolve, reject) => {
      bcrypt.compare(password, hash, function(err, res) {
        if (err) {
          reject(err);
        } else {
            if (res === true) {
                        result="yes";
                  }
                  else {
                        result="no";
                  }
        }
            return result;
      });
    })
  }


Here's where I'm blowing it!

The "bcrypt.compare(password, hash, function(err, res)" works. When I get to:

if(res===true)...

I have a valid piece of syntax that's correctly evaluating the inputted data with what's in the database.

My only problem is being able to send something back to my route that says, "Yeah, baby! You've got a match!"

I tried "return true" and I get nothing. When I tried what you see above, the system just spins and I don't get anything!

I just need to send the happy news that we've got a match back to the route so I can craft things accordingly. But how do I do that?
Hey Bruce,

Great effort in tracking down the code flow. Your confirmPassword method is returning "yes" or "no" (or rejecting), and that result is being assigned to the quiz variable, so you should be able to just check that;

let quiz = await(user.confirmPassword(the_current_password, the_password));

if (quiz == "yes") {
    // we're good
    ...
} else {
    // we have a problem
    ...
}

Open in new window

Regarding your hashing - there's no timestamp involved - just a salt. So if you call the hash method with the same salt, you'll get the same result - this is how you can compare passwords with hashes.
Chris!

Here's my stripped down "profile.js"

router.post('/profile/save', async (req, res) => {
  try {
    if (typeof req.body.user == 'undefined') {
      return res.send(user.error('Invalid request.'));
      }
      
	console.log("sanity");
	let the_password=check.password;
	let the_current_password = req.body.current_password;
	let quiz = await(user.confirmPassword(the_current_password, the_password));
	if(quiz=="yes") {
		console.log("book");
	}
	else {
		console.log("fizzle");
	}
  } catch (err) {
    res.status(500).send(user.error());
  }

});

Open in new window


Here's my "user.js"

    async confirmPassword(password, hash) {
	 let result="";
    return new Promise((resolve, reject) => {
      bcrypt.compare(password, hash, function(err, res) {
        if (err) {
          reject(err);
        } else {
            if (res === true) {
				//I can do a console.log("something") and get a response, so I know this works...
				result="yes";
			}
			else {
				result="no";
			}
		return result;
        }
      });
    })
  }

Open in new window


Notice the "I can do a console.log("something") and get a response, so I know this works..." comment.

It works!

But, going back to your example:

let quiz = await(user.confirmPassword(the_current_password, the_password));

if (quiz == "yes") {
    // we're good
    ...
} else {
    // we have a problem
    ...
}

Open in new window


I want to do a console.log("yes") to confirm that the user's inputted password matches what's in the database and I don't get squat! In other words, the code is working, but I can't tell from the standpoint of "quiz." I can do a console.log within "confirmPassword," but not "quiz."

Why not?
ASKER CERTIFIED SOLUTION
Avatar of Chris Stanyon
Chris Stanyon
Flag of United Kingdom of Great Britain and Northern Ireland image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
BOOM!

Thanks!
Thanks, gentlemen!
No worries Bruce - glad we got there in the end :)