sailsjs error handling when creating two models - javascript

I'm pretty new two nodejs and sails. I'm trying to create two models inside one action. My question is how to handle the errors which can occur in both queries.
Current Code:
new: function (req, res) {
var errorArray = [];
var data = req.allParams();
User.create({
username: data.username,
password: data.password,
device_id: data.device_id,
own_number: data.own_number,
mobile_numbers: data.mobile_numbers
}).exec(function (err, user) {
if(err){
errorArray.push(err);
}
});
Mobile_number.create({
number: data.own_number,
number_id: this.hash(data.own_number)
}).exec(function(err, mobile_number){
sails.log(err);
if(err){
errorArray.push(err);
}
});
if(errorArray.length == 0){
return res.ok('success');
}else {
return res.send(errorArray.toString());
}
}
The problem with this code is that the if at the end is handled before the queries finish. What would be the right way for to wait for the queries?
Bruno

First of all your code will not work because node.js is asynchronous. So you check if there are errors before functions are executed. Below are 2 solutions:
You can use async.series. If you use that it will stop executing if first method fails and it will return it's error. It it succeed it will go to second method.
async.series([
function(callback) {
User.create({
username: data.username,
password: data.password,
device_id: data.device_id,
own_number: data.own_number,
mobile_numbers: data.mobile_numbers
}).exec(callback);
},
function(callback) {
Mobile_number.create({
number: data.own_number,
number_id: this.hash(data.own_number)
}).exec(callback);
}
],
function(error, results) {
// error first finished
if(error)
res.send('error');
else
res.send('success');
}
);
Or you can do it traditional way with callbacks.
new: function(req, res) {
var errorArray = [];
var data = req.allParams();
var mobileCreateCallback = function(err, mobile_number, user) {
sails.log(err);
if (err) {
errorArray.push(err);
}
if (errorArray.length === 0) {
sails.log(user, mobile_number);
return res.ok('success');
} else {
return res.send(errorArray.toString());
}
};
var userCreateCallback = function(err, user) {
if (err) {
errorArray.push(err);
}
Mobile_number.create({
number: data.own_number,
number_id: this.hash(data.own_number)
}).exec(function(error, mobile_number) {
mobileCreateCallback(error, mobile_number, user);
});
};
User.create({
username: data.username,
password: data.password,
device_id: data.device_id,
own_number: data.own_number,
mobile_numbers: data.mobile_numbers
}).exec(userCreateCallback);
}
You should reed about callbacks: callbackhell and asynchronous functions in node.js/sails.js.

Related

Race condition with encryption timing of bcrypt and adding new mongo db document

I have a register function on my site to basically create a new document with the users credentials. Here is my implementation, with data being literally the input fields of username / password stored in an object:
let users = db.collection('users');
let query = sanitize(data);
users.findOne({username: query.username}).then(res=>{
if (res){
socket.emit('usercreated', {
msg: `User: ${query.username} already exists.`
});
return;
}
const h = query.username + query.password;
bcrypt.hash(h, 13, (err, hash)=>{
users.insert({username: query.username, password: hash}, (err, user)=>{
if (err){
socket.emit('usercreated', {
msg: `DB is having issues. Please contact admin.`
});
return;
}
socket.emit('usercreated', {
msg: `User ${query.username} has been created.`
});
});
});
})
The problem is that if the user spams submit for username / password, the res isn't seeing that the user already exists yet because the bcrypt.hash function literally takes a second to resolve.
I have tried also this method too to check res after bcrypt has done its work, but this isn't working either:
let users = db.collection('users');
let query = sanitize(data);
users.findOne({username: query.username}).then(res=>{
const h = query.username + query.password;
bcrypt.hash(h, 13, (err, hash)=>{
if (res){
socket.emit('usercreated', {
msg: `User: ${query.username} already exists.`
});
return;
}
users.insert({username: query.username, password: hash}, (err, user)=>{
if (err){
socket.emit('usercreated', {
msg: `DB is having issues. Please contact admin.`
});
return;
}
socket.emit('usercreated', {
msg: `User ${query.username} has been created.`
});
});
});
})
What is a good way to check if the user already exists properly before making the insertion occur?
The issue is not that bcrypt.hash takes 1 sec to resolve but the way you are handling things.
In case of spamming, this is a classic readers-writters problem, though there are many ways but IMHO simple modification of mutex locks will work just fine.
class NamedLocks {
constructor() {
this._pid = {};
}
acquire(pid) {
if (this._pid[pid]) {
// process is locked
// handle it
return Promise.reject();
}
this._pid[pid] = true;
return Promise.resolve();
}
release(pid) {
this._pid[pid] = false;
}
}
let users = db.collection('users');
let query = sanitize(data);
const userLocks = new NamedLocks();
userLocks.acquire(query.username).then(() => {
users.findOne({
username: query.username
}).then(res => {
const h = query.username + query.password;
bcrypt.hash(h, 13, (err, hash) => {
if (res) {
socket.emit('usercreated', {
msg: `User: ${query.username} already exists.`
});
return;
}
users.insert({
username: query.username,
password: hash
}, (err, user) => {
if (err) {
socket.emit('usercreated', {
msg: `DB is having issues. Please contact admin.`
});
return;
}
socket.emit('usercreated', {
msg: `User ${query.username} has been created.`
});
userLocks.release(query.username);
});
});
})
}).catch((e) => {
// handle spamming
})
Sounds like you need to have throttling on the function that gets called from hitting submit on the page. Function throttling makes it so the function can only be called once in a set time period. So, say you specify a timeout of 1 second, if a user spams the button only the first call will go through and all subsequent calls for the next second will be ignored. Underscore.js has a _.throttle function you can use or you can look at their implementation and make a version for yourself.
I found answer. I must encrypt the password first, then check after that if the user exists.
let users = db.collection('users');
let query = sanitize(data);
const h = query.username + query.password;
bcrypt.hash(h, 13, (err, hash)=>{
users.findOne({username: query.username}).then(res=>{
if (res){
socket.emit('usercreated', {
msg: `User: ${query.username} already exists.`
});
return;
}
users.insert({username: query.username, password: hash}, (err, user)=>{
if (err){
socket.emit('usercreated', {
msg: `DB is having issues. Please contact admin.`
});
return;
}
socket.emit('usercreated', {
msg: `User ${query.username} has been created.`
});
});
});
});

Able to create multiple users with same email despite email set as unique in Mongoose

I am writing an API using Express, MongoDB, and Mongoose. I am somehow able to create multiple users with the same email. However, I shouldn't be able to create another user with the same email address. I have email unique: true in my user schema, but this isn't working as expected.
Here's my user schema:
var UserSchema = new Schema({
fullName: { type: String, required: [true, 'Full name is required.'] },
emailAddress: {
type: String, required: true, unique: true,
validate: {
validator: function (value) {
// check for correct email format
return /^[a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+#[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(value)
},
message: `Please enter a valid email address!`
}
},
password: { type: String, required: true }
});
My user authenticate method:
UserSchema.statics.authenticate = function (email, password, callback) {
User.findOne({ emailAddress: email })
.exec(function (err, user) {
if (err) {
return callback(err);
} else if (!user) {
var error = new Error('User not found');
error.status = 401;
return callback(error);
}
bcrypt.compare(password, user.password, function (error, user) {
if (user) {
return callback(null, user);
} else {
return callback();
}
});
});
}
My pre-save hook to hash the password:
UserSchema.pre('save', function (next) {
var user = this;
bcrypt.hash(user.password, 10, function (err, hash) {
if (err) {
return next(err);
}
user.password = hash;
next();
});
});
And finally, my user create route:
router.post('/', function (req, res, next) {
if (req.body.fullName &&
req.body.emailAddress &&
req.body.password &&
req.body.confirmPassword) {
if (req.body.password != req.body.confirmPassword) {
var err = new Error('Passwords do not match!');
err.status = 400;
return next(err);
}
// object with form input
var userData = {
fullName: req.body.fullName,
emailAddress: req.body.emailAddress,
password: req.body.password
};
// schema's 'create' method to insert document into Mongo
User.create(userData, function (error, user) {
if (error) {
var err = new Error('Please enter a valid email.');
err.status = 400;
return next(err);
} else {
// set location header to '/', return no content
res.status(201);
res.location('/');
req.session.userId = user._id;
return res.json(user);
}
});
} else {
var err = new Error('All fields required.');
err.status = 400;
return next(err);
}
});
MongoDB does not create unique index to a field if the field value has duplicates already stored in it.
In your case emailAddress must have duplicates already stored in the database.
You can check it by runing the code
mongoose
.model(#modelName)
.collection.createIndex( { "inviteCode": 1 });
After running this code you should be able to see a error message in the console.
Or You can check by running the below code if you have duplicate. The below will fetch documents if have duplicates:
mongoose
.model(#modelName).aggregate([{
"$group": {
"_id": "$loginStatus",
count: { $sum: 1 },
ids: { $push: "$_id" }
}
},{ $match: {
count: { $gt : 1 }
}}]);
If you emailAddress which are already duplicates you cant create unique: true on it. You would have to run the second query and find out the duplicate email address. You can find the documents with duplicate email in the ids array.

Cannot read property "rid" of undefined

[TypeError: Cannot read property 'rid' of undefined]
Is the error that I get when I try to execute this controller on my post route.
I've tested it out with Postman.
I've tried to console.log(result) but I get undefined.
My query gets executed and my row is inserted into my table. I've checked it. Password is also hashed.
The problem is that I don't get any out binds that should be returned.
Problematic code (IMO) is
...
.then(function(result) {
console.log(result);
cb(null, {
id: result.outBinds.rid[0],
email: result.outBinds.remail[0],
role: result.outBinds.rrole[0]
});
})
...
oracle-NodeDB Wrapper
var oracledb = require('oracledb');
module.exports.OBJECT = oracledb.OBJECT;
function executeSQL(config ,sql, bindParams , options) {
return new Promise(function(resolve, reject) {
oracledb.getConnection(
config,
function(err, connection) {
if (err) {
return reject(err);
}
connection.execute(
sql,
bindParams,
options,
function(err, result) {
if (err) {
doRelease(connection);
return reject(err);
}
resolve(result);
doRelease(connection);
});
});
});
}
function doRelease(connection) {
connection.release(
function(err) {
if (err) {
console.log(err.message);
}
}
);
}
module.exports.executeSQL = executeSQL;
Controller
var database = require('../database/oracledbWrapper');
var dbconfig = require('../database/dbconfig').dbconfig;
var jwt = require('jsonwebtoken');
var bcrypt = require('bcrypt');
exports.createUser = function(req, res, next) {
var user = {
email: req.body.email
};
var unhashedPassword = req.body.password;
bcrypt.genSalt(10, function(err, salt) {
if (err) {
return next(err);
}
bcrypt.hash(unhashedPassword, salt, function(err, hash) {
if (err) {
return next(err);
}
user.hashedPassword = hash;
insertUser(user, function(err, user) {
var payload;
if (err) {
return next(err);
}
payload = {
sub: user.email,
role: user.role
};
res.status(200).json({
user: user,
token: jwt.sign(payload, config.jwtSecretKey, {expiresInMinutes: 60})
});
});
});
});
}
function insertUser(user, cb) {
var bindParams = {
email: user.email.toLowerCase(),
password: user.hashedPassword,
rid: {
type: database.NUMBER,
dir: database.BIND_OUT
},
remail: {
type: database.STRING,
dir: database.BIND_OUT
},
rrole: {
type: database.STRING,
dir: database.BIND_OUT
}
};
database.executeSQL(
dbconfig,
'insert into express_users (email, password, role ) values ( :email, :password, \'BASE\' ) returning id, email, role into :rid , :remail, :rrole',
bindParams,
{}
)
.then(function(result) {
console.log(result);
cb(null, {
id: result.outBinds.rid[0],
email: result.outBinds.remail[0],
role: result.outBinds.rrole[0]
});
})
.catch(function(err) {
console.log(err);
next(err);
});
}
Route
var RESTfulAPICon = require('../controllers/RESTfulAPI');
var indexCon = require('../controllers/index');
var views = require('express').Router();
views.route('/users').post(RESTfulAPICon.createUser);
exports.views = views;
The problem was in my wrapper , mainly here
module.exports.OBJECT = oracledb.OBJECT;
I export only the OBJECT property , but I try to access BIND_OUT properties later on. And they are non existent.
If I do the full export like this
module.exports.OBJECT = oracledb;
Then I can access BIND_OUT properties.

Async waterfall equivalent with Q

I've got a single page which is an account settings page. In it, I allow my users to update their avatar (if they've attached an image), change their email (if it has been changed from the original), and change their name and password.
Right now, I'm using async's waterfall method, but am swapping out async for Q since I prefer the syntax (and api). I'm wondering if this is the way that I should be using Q in replacement of async's waterfall.
I'm doing something like this:
exports.settingsAccountPOST = function(req, res) {
var doesEmailExist = function() {
var deferred = Q.defer();
User.findByEmail({
email: req.body.email
}, function(err, user) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve(user);
}
});
return deferred.promise;
};
var updateEmail = function(email) {
var deferred = Q.defer();
User.updateEmail({
userId : req.session.user.id,
email : req.body.email
}, function(err, updated) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve(updated);
}
});
return deferred.promise;
};
var updateName = function() {
var deferred = Q.defer();
if (req.body.name) {
User.updateName({
userId: req.session.user.id,
name: req.body.name
}, function(err, updated) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve(updated);
}
});
return deferred.promise;
}
};
doesEmailExist().then(function(email) {
if (!email) {
return(updateEmail(email));
}
}).then(function() {
return(updateName())
}).then(function() {
res.redirect('/account')
});
};
Say that there is an error with the email address being used. Is there a way to "pass" it to the final call? Use case: Updated password properly, but email update didn't work, so I want to show a session flash to the user telling them they updated their password properly, but there was an issue with updating their email.
I was looking in the docs and it seems I may need to use:
.fin(function () {
});
Is this correct? If so, what should I be passing into that? Just push to an object the error that occurred within the chain and then loop through all errors and display them to the user? Or just return immediately and display the error?
If you are using Q.defer you are generally doing something wrong.
var findByEmail = Q.nbind(User.findByEmail, User);
var updateEmail = Q.nbind(User.updateEmail, User);
var updateName = Q.nbind(User.updateName, User);
//later on...
exports.settingsAccountPOST = function (req, res) {
findByEmail({
email: req.body.email
})
.then(function (user) {
if (!user) {
return updateEmail({
userId: req.session.user.id,
email: req.body.email
});
}
})
.then(function () {
return updateName({
userId: req.session.user.id,
name: req.body.name
})
})
.then(function () {
res.redirect("/account");
})
.catch(function(e){
//Handle any error
});
};

nodejs prototype and nested callbacks with mysql

I have two problems with the code below. It is the beginning of a node.js module for registering users that are stored in a mysql database.
The first problem is that in the callback from the first query the variable this.connection is undefined. The second problem is that the variable user is also undefined in the same callback. Hence the code crashes on the following line:
this.connection.query('INSERT INTO users{{values values}}', {
values: {
user_name: user.username,
user_email: user.email,
user_password: user.password
}
})
Please let me know how to solve this in a propper way.
Many thanks!
Full code is below:
module.exports = usermodel;
//connection is a mysql-wrapper connection
function usermodel(connection)
{
this.connection = connection;
}
playermodel.prototype = {
registerByEmail: function (user, callback) {
//check firts if the user already exists
this.connection.query('SELECT user_id FROM users {{where data}}', {
data: {
user_email: user.email
}
},
function (err, result) {
if (err) {
...
} else if (result.length > 0) {
...
} else {
// this.connection is undefined ????????????
this.connection.query('INSERT INTO users {{values values}}', {
values: {
user_name: user.username,
user_email: user.email,
user_password: user.password
}
}),
function (err, result) {
...
}
}
});
}
}
this.connection is undefined because this.connection is on the instance of playermodel the callback function passed to the first query does not know about the context of the playermodel instance.
user should be defined though. You can get around the this.connection being undefined by either binding the function or setting a reference to this in a variable the the callback will have access to.
//Bind
playermodel.prototype = {
registerByEmail: function (user, callback) {
//check firts if the user already exists
this.connection.query('SELECT user_id FROM users {{where data}}', {
data: {
user_email: user.email
}
},
function (err, result) {
if (err) {
...
} else if (result.length > 0) {
...
} else {
// this.connection is undefined ????????????
this.connection.query('INSERT INTO users {{values values}}', {
values: {
user_name: user.username,
user_email: user.email,
user_password: user.password
}
}),
function (err, result) {
...
}
}
//HERE
}.bind(this));
}
}
or
//me reference to this
playermodel.prototype = {
registerByEmail: function (user, callback) {
//HERE
var me = this;
//check firts if the user already exists
this.connection.query('SELECT user_id FROM users {{where data}}', {
data: {
user_email: user.email
}
},
function (err, result) {
if (err) {
...
} else if (result.length > 0) {
...
} else {
//HERE
me.connection.query('INSERT INTO users {{values values}}', {
values: {
user_name: user.username,
user_email: user.email,
user_password: user.password
}
}),
function (err, result) {
...
}
}
});
}
}

Categories