How to Async Mongoose Controller - node.js

Trying to configure a SignUp() controller that can update multiple (separate) user accounts when a referral code is provided by the user.
Basic Flow:
Verify email doesn't already exist in system
Find the driver w/ userID matching the rider's refCode (FindOneAndUpdate)
If Found: Add the userID of each user to the other users [clients] list
Only need to do a refCode match if isRider
If any of those fail... Return the specific error to the client/user
This does not work. But essentially, this is what I'm trying to accomplish...
// POST `/signup` (Create a new local user)
export function signUp(req, res, next) {
const newUser = new User({
email: req.body.email,
password: req.body.password,
profile: {
userID: req.body.userID,
refCode: req.body.refCode,
isRider: req.body.isRider
}
});
User.findOne({ email: req.body.email }, (findErr, foundUser) => {
if (foundUser) {
return res.status(409).send('This e-mail address already exists');
}
// riders must link to a driver
if (req.body.isRider) {
// find driver + add rider ID to clients
return User.findOneAndUpdate({ 'profile.userID': req.body.refCode }, { $push: { clients: newUser.profile.userID }}).exec()
.then((err, foundDriver) => {
if (err) {
return res.status(409).send('Error searching for driver');
} else if (!foundDriver) {
return res.status(409).send(`We can't find your driver (${req.body.refCode})!`);
}
// add driver ID to rider's clients
newUser.clients = [req.body.refCode];
return newUser.save((saveErr) => {
if (saveErr) return next(saveErr);
return req.logIn(newUser, (loginErr) => {
if (loginErr) return res.sendStatus(401);
return res.json(newUser.profile);
});
});
});
}
return newUser.save((saveErr) => {
if (saveErr) return next(saveErr);
return req.logIn(newUser, (loginErr) => {
if (loginErr) return res.sendStatus(401);
return res.json(newUser.profile);
});
});
});
}
Tried to configure it as a pure promise but no luck. Most of the examples out there all seem different to me... Also could not figure out how to handle/throw specific errors using the mongoose docs.
Greatly appreciated if anyone can lend a hand, Thx!
UPDATE:
Ippi's answer helped a ton - Thx!
This does the trick. Remember to return null from .then() after the req.login stuff to avoid warnings - Any tips on how to improve this are appreciated - Thx!
const createUser = (foundUser) => {
if (foundUser) { throw new Error('This e-mail address already exist.'); }
if (!req.body.isRider) { return newUser.save(); }
return User.findOneAndUpdate({ 'profile.userID': req.body.refCode.toLowerCase() }, { $push: { clients: newUser.profile.userID }}).exec()
.then((driver) => {
if (!driver) { throw new Error('We can\'t find your driver.'); }
newUser.clients = [req.body.refCode];
return newUser.save();
})
.catch(() => { throw new Error('There was a database error.'); });
};
User.findOne({ email: req.body.email }).exec()
.then(createUser)
.then((user) => {
if (user.profile) {
req.logIn(user, (loginErr) => {
if (loginErr) return res.sendStatus(401);
return res.status(200).send({ profile: user.profile, clients: user.clients });
});
} else { res.status(409); }
return null;
})
.catch((err) => { return res.status(409).send(err.message); });

function signUp(req, res, next) {
return new Promise((resolve, reject) => {
const newUser = new User({
email: req.body.email,
password: req.body.password,
profile: {
userID: req.body.userID,
refCode: req.body.refCode,
isRider: req.body.isRider
}
});
User.findOne({ email: req.body.email }, (findErr, foundUser) => {
if (foundUser) {
// return res.status(409).send('This e-mail address already exists');
reject('This e-mail address already exists');
}
// riders must link to a driver
if (req.body.isRider) {
// find driver + add rider ID clients
return User.findOneAndUpdate({ 'profile.userID': req.body.refCode }, { $push: { clients: newUser.profile.userID } }).exec()
.then((err, foundDriver) => {
if (err) {
// return res.status(409).send('Error searching for driver');
reject('Error searching for driver');
} else if (!foundDriver) {
// return res.status(409).send(`We can't find your driver (${req.body.refCode})!`);
reject(`We can't find your driver (${req.body.refCode})!`);
}
// add driver ID to rider's clients
newUser.clients = [req.body.refCode];
newUser.save((saveErr) => {
if (saveErr)
// next(saveErr);
reject(saveErr);
req.logIn(newUser, (loginErr) => {
if (loginErr)
// return res.sendStatus(401);
reject('401');
// return res.json(newUser.profile);
resolve(newUser.profile);
});
});
});
}
newUser.save((saveErr) => {
if (saveErr)
// return next(saveErr);
reject(saveErr);
req.logIn(newUser, (loginErr) => {
if (loginErr)
// return res.sendStatus(401);
reject(loginErr);
// return res.json(newUser.profile);
resolve(newUser.profile);
});
});
});
});}

This is how I would do it. I couldn't be bothered to try with express or the login (you need to replace console.log with res.status().json()) and I might have done some other blunder in the logic with the driver. But other than that I tested it with local mongo and it probably works and if nothing else it's a little bit more concise.
let updateUser = user => {
if (user){ throw new Error("USER_EXIST"); }
if (!req.body.isRider) { return newUser.save() }
return User.findOneAndUpdate({ 'profile.userID': req.body.refCode },{ $push: { clients: newUser.profile.userID }}).exec()
.then(driver => {
if (!driver) { throw new Error("NO_DRIVER");}
newUser.clients.push(req.body.refCode);
return newUser.save();
});
}
User.findOne({ email: req.body.email }).exec()
.then(updateUser)
.then(req.logIn) // newUser.save() response is passed in as is (I have not tested this line.)
.then( ()=>{ return console.log('profile', newUser.profile); })
.catch( Error, err => {
if (err.message == "USER_EXISTS") return console.log ("This e-mail address already exist." );
if (err.message == "NO_DRIVER") return console.log ("We can't find your driver." );
throw err;
});
Something worth remembering:
Callback calls or res.send should always go in the last then / catch. Calling res.send in middle of chains leads to trouble.

Related

Cannot set headers after they are sent to the client error is showing

Error is showing if a user is already exiting. The error is not showing if the user does not exist in the database
Plz tell me if there is any better way to check the username thank you in advance
exports.updateUser = async(req, res) => {
if (req.body.userId === req.params.id) {
if (req.body.userName || req.body.email) {
await User.findOne({
$or: [{
email: req.body.email
}, {
username: req.body.userName
}]
}).exec((err, user) => {
if (err) {
return res.status(500).json(err);
} else if (user) {
if (user.username === req.body.userName) {
return res.status(400).json({ error: "User Name already exists" });
//err = "User Name already exists";
} else {
return res.status(400).json({ error: "Email already exists" });
}
}
})
}
if (req.body.password) {
const salt = await bcrypt.genSalt(10);
req.body.password = await bcrypt.hash(req.body.password, salt);
}
await User.findByIdAndUpdate(
req.params.id, { $set: req.body }, { new: true }
).exec((err, user) => {
if (err) {
return res.status(500).json(err);
}
if (user) {
return res.status(200).json("User Updated successfully!"); // here error is showing.
}
});
} else {
res.status(401).json("You can update only your account!");
}
};
Don't send response inside exec, because if after sending response it'll execute rest of the code and try to send a response but response is already sent.
exports.updateUser = async (req, res) => {
try {
if (req.body.userId === req.params.id) {
if (req.body.userName || req.body.email) {
await User.findOne({
$or: [
{
email: req.body.email,
},
{
username: req.body.userName,
},
],
}).exec((err, user) => {
if (err) {
throw {
code: 400,
error: err,
};
} else if (user) {
if (user.username === req.body.userName) {
throw {
code: 400,
error: { error: "User Name already exists" },
};
}
throw {
code: 400,
error: "Email already exists",
};
}
});
}
if (req.body.password) {
const salt = await bcrypt.genSalt(10);
req.body.password = await bcrypt.hash(req.body.password, salt);
}
await User.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true }
).exec((err, user) => {
if (err) {
throw {
code: 500,
error: err,
};
}
if (user) {
return res.status(200).json("User Updated successfully!"); // here error is showing.
}
});
} else {
throw {
code: 401,
error: "You can update only your account!",
};
}
} catch (e) {
res.status(e.code).json(e.error);
}
};

vue.js | TypeError: Cannot read property 'then' of undefined

I'm trying to make email verification in my vue.js/express app.
I can create the user and send emails. But showing a message like "verification mail sent" won't work.
The error occurs when executing the code in the then() callback after the execution in DataService.
When registering the following functions are executed:
vuex
const actions = {
registerUser({
commit
}, user) {
commit('registerRequest', user)
return DataService.registerUser(JSON.stringify(user))
// HERE'S THE ERROR
.then(response => {
commit('confirmation', response.message)
setTimeout(() => {
state.status = {
confirmHere: ''
}
}, 4000);
})
.catch(...)
confirmation:
confirmation: (state, msg) => {
state.status = {
confirmHere: msg
}
},
DataService
registerUser(user) {
// Send email for registration
apiClient.post('/user/register/sendMail', user)
.then(res => {
return apiClient.post(`/user/register`, user)
})
.catch(err => {
throw err;
})
},
The sendmail function is using nodemailer to send an email and returns
res.status(200).json({
message: "success"
});
The register function in express is:
router.post('/register', async (req, res) => {
try {
if (req.body.username !== undefined && req.body.password !== undefined) {
let password = await bcrypt.hashSync(req.body.password, saltRounds);
let compareUser = await db.getObject({}, User, 'SELECT * FROM app_users WHERE username=? LIMIT 1', [req.body.username]);
if (compareUser !== undefined) {
res.status(409).json('User already exists');
return;
}
const tmp = {
username: req.body.username,
password: password
};
await db.query('INSERT INTO app_users SET ?', [tmp]);
let user = await db.getObject({}, User, 'SELECT * FROM app_users WHERE username=? LIMIT 1', [req.body.username]);
if (user === undefined)
res.status(500).json('Internal server error');
res.status(201).json({
"message": "Bestätigungs-Email gesendet."
});
} else {
res.sendStatus(400);
}
} catch (error) {
res.sendStatus(500);
}
});
You forgot to return the response from DataService.registerUser
// DataService.js
registerUser(user) {
// Send email for registration
return apiClient.post('/user/register/sendMail', user)
.then(res => {
return apiClient.post(`/user/register`, user)
})
.catch(err => {
throw err;
})
The issue is that your registerUser function doesn't return anything whereas you're expecting it to return a promise.
Change your registerUser to:
registerUser(user) {
// Send email for registration
return apiClient.post('/user/register/sendMail', user)
.then(res => {
return apiClient.post(`/user/register`, user)
})
}
(FYI in the example, I left the .throw out because it already gets handled by the Promise you return ;)

Correct way to figure out what rejection a promise had?

I have an API / express router:
router.post("/signup", async function (req, res) {
try {
var user = await controllers.user.register(req.body.username, req.body.password);
req.session.user = user;
res.json(user);
} catch (e) {
res.status(500).json("DB Error");
}
});
Currently, on error, it returns 500 DB error. This is my controller:
function register(username, password) {
return new Promise((resolve, reject) => {
User.findOne({ username: username }).lean().exec((e, doc) => {
if (e) reject(e);
if (doc) {
reject("Username already exists.");
} else {
var user = new User({ username, password: hash(password) });
user.save((e) => {
if (e) reject(e);
else {
delete user.password;
resolve(user);
}
});
}
});
});
}
What's the right way to return a 400 if username already exists, and a 500 if it was a database error?
Mongoose already uses promises, the use of new Promise is promise construction antipattern.
Express doesn't have the concept of controllers, there are only route handlers and middlewares. Since register should be very aware of the way it will be used in a response, there may be no need for another level of abstraction above route handler. There will be no problem when a function has access to handler parameters and can form a response in-place.
It can be:
router.post("/signup", async function (req, res) {
try {
const { body, password } = req.body;
const user = await User.findOne({ username: username }).lean();
if (user) {
res.status(400).json("Username already exists");
} else {
...
res.json(user);
}
} catch (e) {
res.status(500).json("DB Error");
}
});
In case route handler needs to be reused in multiple places with some variations, it could be refactored to higher-order function or some other helper that is aware of original req and res parameters.
You can change the way you are rejecting the Promise. I'd suggest something like:
function register(username, password) {
return new Promise((resolve, reject) => {
User.findOne({ username: username }).lean().exec((e, doc) => {
if (e) reject(500);
if (doc) {
reject(400);
} else {
var user = new User({ username, password: hash(password) });
user.save((e) => {
if (e) reject(500);
else {
delete user.password;
resolve(user);
}
});
}
});
});
}
And in the route:
router.post("/signup", async function (req, res) {
try {
var user = await controllers.user.register(req.body.username, req.body.password);
req.session.user = user;
res.json(user);
} catch (e) {
res.status(e).json(e == 400 ? "Username already exists." : "DB Error");
}
});

Express-validation not producing the correct errors

So, this worked before, and all the sudden decided to stop working, and I have no idea why.
EDIT: Updated the code to show what I currently got now
router.post('/register', async (req, res) => {
// let query
let query;
// Start email checks
req.check('email', 'Email is not valid.')
.isEmail()
.custom(async value => {
query = {email: value};
User.findOne(query).then(user => {
if (user) return false;
});
}).withMessage('Email is in use.');
// Start username checks
req.check('username', 'Username is required.')
.notEmpty()
.isLength({ min: 5, max: 15}).withMessage('Username requires 5-15 alphanumberic characters.')
.isAlphanumeric().withMessage('Username must be alphanumeric only.')
.custom(async value => {
query = {username: value}
User.findOne(query).then(user => {
if (user) return false;
});
}).withMessage('Username is in use.');
// Start password checks
req.check('password', 'Password is required.')
.notEmpty()
.isLength({min: 5}).withMessage('Password must be atleast 5 characters long.');
req.check('confirmPassword', 'Confirm Password is required.')
.notEmpty()
.custom(value => value === req.body.password).withMessage('Password must match');
const errors = await req.getValidationResult();
//console.log(errors);
if (!errors.isEmpty()) {
res.render('index', {
errors: errors.mapped()
});
} else {
let newUser = new User({
email: req.body.email,
username: req.body.username,
password: req.body.password,
});
let hash = bcrypt.hashSync(req.body.password, 10);
newUser.password = hash;
newUser.save(err => {
if (err) {
console.log(err);
} else {
res.render('index', {
success: 'Registration Successful'
});
}
});
}
});
So its pretty clear its something with my custom checks, and I do not know why.
EDIT:
It seems there is confusion. The checks are working correctly, what I'm having issues with is it populating the errors when I want it to. If i try to register with the same email, it will pull up the user and will go through my if statements. If I use Promise.reject() it doesn't work. If I use false, it doesn't work. Again, the checks itself work, the error handling seems like it isn't.
EDIT TWO:
So I have tried this method (all the other code is still the same)
// Start email checks
req.checkBody('email', 'Email is not valid.')
.isEmail()
.custom(value => {
query = {email: value}
User.findOne(query).then(user => {
if (user) console.log('Email Exists'); return false;
});
}).withMessage('Email in use.');
// Start username checks
req.check('username', 'Username is required.')
.notEmpty()
.isLength({ min: 5, max: 15}).withMessage('Username requires 5-15 alphanumberic characters.')
.isAlphanumeric().withMessage('Username must be alphanumeric only.')
.custom(value => {
query = {username: value}
User.findOne(query).then(user => {
if (user) console.log('Username Exists'); return false;
});
}).withMessage('Username in use.');
This should work. Since node.js is non render blocking,the db query may not complete before it proceeds to next step. You could use the format I have posted below or try the async library in which case await keyword should be placed before User.findOne
User.findOne(query).then(user=>{
if(user) return false
}).catch(err => console.log(err))
Finally found an answer, which incidentally makes it a little easier to read as well. I ended up making my own custom validators:
customValidators: {
emailExists: (email) => {
let query = {email: email};
return new Promise((resolve, reject) => {
User.findOne(query, (err, results) => {
if (results === null) {
resolve(err);
}
reject(results);
});
});
},
userNameExists: (username) => {
let query = {username: username};
return new Promise((resolve, reject) => {
User.findOne(query, (err, results) => {
if (results === null) {
resolve(err);
}
reject(results);
});
});
}
},
Then:
req.check('email', 'This email is in use.').emailExists();
req.check('username', 'Username is in use.').userNameExists();
req.asyncValidationErrors().then(() => {
console.log('No errors');
let newUser = new User({
email: req.body.email,
username: req.body.username,
password: req.body.password,
});
let hash = bcrypt.hashSync(req.body.password, 10);
newUser.password = hash;
newUser.save(err => {
if (err) {
console.log(err);
} else {
res.render('index', {
success: 'Registration Successful'
});
}
});
}).catch(errors => {
res.render('index', {
errors: errors
});
});

MongoDB find always returning true when something doesnt exist (even on an empty database)

app.post('/sign-up', function (req, res) {
let emailValid = validator.validate(req.body.email);
let consent = req.body.consent ? true:false
if(emailValid && consent) {
const user = new UserModel({
name: req.body.firstName,
surname: req.body.surname,
email: req.body.email
})
UserModel.find({'email': req.body.email}, function(notFound, found) {
if(notFound) {
user.save().then(item => {
console.log('Saved successfully!');
res.render('submitSuccess', {data: req.body});
}).catch(err => {
res.status(400).render('404');
})
} else if(found) {
console.log('Exists');
res.status(404).render('submitSuccess', {data:req.body});
}
else {
res.status(404).render('404');
}
});
}
});
The intended functionality here is that if someone submits an email to the database that already exists, it does not then save a duplicate. However, it seems that found is returning true everytime, therefore nothing is getting saved.
Run this code:
app.post('/sign-up', function (req, res) {
let emailValid = validator.validate(req.body.email);
let consent = req.body.consent ? true : false
if (emailValid && consent) {
const user = new UserModel({
name: req.body.firstName,
surname: req.body.surname,
email: req.body.email
})
UserModel.find({ 'email': req.body.email }, function (err, found) {
if (err) {
console.error(err);
res.status(500).end();
return;
}
if (found.length == 0) {
user.save().then(item => {
console.log('Saved successfully!');
res.render('submitSuccess', { data: req.body });
}).catch(err => {
res.status(400).render('404');
})
} else {
console.log('Exists');
res.status(404).render('submitSuccess', { data: req.body });
}
});
}
});
What's in the err?

Resources