In a node.js express project I want to implement a switch-to-user function for admin users. The admin can either type the username or the user-id in a box. Below is the code to process this request. The problem is that if the first database call failed, I need to repeat it with another kind of query, before continuing with the login code. How can I add a conditional asynchronous call before the call to req.login?
router.route('/switchuser')
.post(function (req, res) {
mongoose.model('Users').findById(req.body.idOrName, function (err, user) {
if (!user) {
mongoose.model('Users').findOne({ username: req.body.idOrName }, function (err, user_) {
user = user_;
if (err) {
res.status(404);
res.send("User not found " + err);
}
});
}
req.login(user, function (err) {
if (err) {
res.status(404);
res.send("There was a problem switching the user: " + err);
}
else {
res.format({
html: function () {
res.redirect("/");
}
});
}
})
});
});
You are experiencing callback hell. When a user is not found by ID, you start a second callback. The problem is your login as method executes outside of this second callback which means it doesn't wait for the response. Callback hell can quickly cause a lot of duplicate code. One solution is to pull your common bits of code out into functions. Below is one example of how you might do that. Notice that loginAs may be called in two different spots depending on whether the user was found by ID, or required an additional lookup.
router.route('/switchuser')
.post(function (req, res) {
mongoose.model('Users').findById(req.body.idOrName, function (err, user) {
if (err) {
sendErrorResponse(err);
return;
}
if (user) {
loginAs(user, req, res);
return
}
mongoose.model('Users').findOne({ username: req.body.idOrName }, function (err, user) {
if (err) {
sendErrorResponse(err, req, res);
return;
}
if (!user) {
sendNotFoundResponse(req, res);
return;
}
loginAs(user, req, res);
});
});
});
function loginAs(user, req, res) {
req.login(user, function (err) {
if (err) {
res.status(404);
res.send("There was a problem switching the user: " + err);
}
else {
res.format({
html: function () {
res.redirect("/");
}
});
}
})
}
function sendErrorResponse(err, req, res) {
res.status(500);
res.send("Failed to switch user " + err);
}
function sendNotFoundResponse(req, res) {
res.status(404);
res.send("Could not find user");
}
Now depending on what version of Mongoose JS you are using, a limited version of Promises may be available already. Your code could be cleaned up even further like so.
router.route('/switchuser')
.post(function (req, res) {
mongoose.model('Users').findById(req.body.idOrName)
.then(function (user) {
// If the user was found, return it down the chain
if (user) {
return user;
}
// If the user was not found, return the promise for the next query
return mongoose.model('Users').findOne({ username: req.body.idOrName });
})
.then(function(user) {
if (!user) {
sendNotFoundResponse(req, res);
return;
}
loginAs(user, req, res);
})
.catch(function(err) {
sendErrorResponse(err, req, res);
});
});
});
function loginAs(user, req, res) {
req.login(user, function (err) {
if (err) {
res.status(404);
res.send("There was a problem switching the user: " + err);
}
else {
res.format({
html: function () {
res.redirect("/");
}
});
}
})
}
function sendErrorResponse(err, req, res) {
res.status(500);
res.send("Failed to switch user " + err);
}
function sendNotFoundResponse(req, res) {
res.status(404);
res.send("Could not find user");
}
You'll notice with promises you can lump all the errors together at the bottom in a single catch. Cleaner result with less duplicate code and methods calls. You may need to perform an exec to get access to the catch method. Just read the Mongoose docs and you should be fine.
Related
I am trying to use passport.js with mongoose. The data sent are correct but I get an error of code 401 saying unauthorized?
Here is the back end controller.
userController.login = (req, res) =>{
console.log("recieved");
console.log(req.body);
const user = new Users({
username: req.body.mail,
password: req.body.password
})
req.login(user, (err) => {
if (err){
res.status(404).send(err);
}else{
console.log("user found");
passport.authenticate("local", (err, user, info) => {
if (err) {
return res.status(401).json(err);
}
if (user) {
return res.status(200).send(user);
} else {
res.status(401).json(info);
}
})(req, res, () => {
console.log("Authenticated!");
res.status(200).send(user);
});
}
})
}
While posting I needed to rename the req.body.mail to just req.body.username because auth and req.login looks for req body directly and search for a username object.
I have multiple controllers and each controller has multiple methods. In each method I authenticate the user and use the user id returned from the authentication to get the data from database. I am trying to create reusable code for authentication since the code is repeated.
In the controller:
const authenticate = require('../utils/user-authenticate');
exports.getData = async (req, res, next) => {
const userId = await authenticate.user(req, res, next);
console.log(userId);
};
And in the authentication I have:
exports.user = (req, res, next) => passport.authenticate('jwt', async (error, result) => {
if (error) {
// Send response using res.status(401);
} else {
return result;
}
})(req, res, next);
The console.log(userId); prints undefined always. This is print before passport finishes. Looks like async/await does not work the way I want here.
It works if I use await authenticate.user(req, res, next).then() but isn't it possible to assign the result directly to userId variable?
If I use return next('1'): first time undefined but second time it prints 1.
wrapped into a promise:
exports.user = (req, res, next) => new Promise((resolve, reject) => {
passport.authenticate('jwt', async (error, result) => {
if (error) {
// reject(error)
// Send response using res.status(401);
} else {
resolve(result);
}
})(req, res, next);
})
but think about:
//app.use or something similar
addMiddleware(authJWT);
// later in the chain
useMiddleware((req, res, next)=>{
// test auth or end chain
if(!req.JWT_user) return;
req.customField = 'one for the chain'
// process next middleware
next()
});
Thanks #Estradiaz for the suggestion:
exports.user returns undefined ... Return is scoped within inner
callback - if you want to pass it outside wrap it into a promise
Reusable passport.authenticate:
exports.user = (req, res) => {
return new Promise(resolve => {
passport.authenticate('jwt', null, async (error, result) => {
if (error) {
email.sendError(res, error, null);
} else if (result) {
resolve(result);
} else {
return res.status(401).json({errors: responses['1']});
}
})(req, res);
});
};
And this is how I use it in my controller, for instance in a function:
exports.getData = async (req, res, next) => {
const userId = await authenticate.user(req, res);
};
app.post('/profile', function(req, res) {
// save file
if (req.files) {
let sampleFile = req.files.sampleFile;
sampleFile.mv('/somewhere/on/your/server/filename.jpg', function(err) {
if (err)
return res.status(500).json(err);
});
}
// do some other stuff
// .............
res.status(200).json(result);
});
I know the problem is caused by return res.status(500).json(err). I can solve the problem by moving res.status(200).json(result) after if (err) block. Since upload file is optional, the user may posting other data without any uploading files. My question is how to send status 200 with a json result after processed other stuff if the solution is
if (err)
return res.status(500).json(err);
res.status(200).json(result);
As was pointed above the problem is you are sending the success reponse outside of the callback.
The solution is to do "other stuff" within the callback.
This should fix the issue -
app.post('/profile', function(req, res) {
// save file
if (req.files) {
let sampleFile = req.files.sampleFile;
sampleFile.mv('/somewhere/on/your/server/filename.jpg', function(err) {
if (err) return res.status(500).json(err);
doOtherStuff();
res.status(200).json(result);
});
} else {
doOtherStuff();
res.status(200).json(result);
}
});
// Write a do other stuff function
function doOtherStuff() {
// do stuff
}
EDIT Adding answer with Promises to avoid code repetition.
function moveFile(file, somePlace) {
return new Promise((resolve, reject)=>{
file.mv(somePlace, function(err) {
if (err) return reject(err);
resolve();
});
});
}
app.post('/profile', function(req, res) {
// save file if present
const fileMovePromise = req.files ?
moveFile(req.files.sampleFile, '/somewhere/on/your/server/filename.jpg')
:
Promise.resolve('No file present');
fileMovePromise
.then(() => {
// do other stuff
})
.catch(err => {
res.status(500).json(err);
});
});
You could use a form of middleware to check if the post is uploading files, the act on the file upload before continuing, have a look at middleware with express here: http://expressjs.com/en/guide/using-middleware.html
With middleware you can do your check to see if the file needs uploading and then call next, otherwise just call next.
app.use('/profile', function (req, res, next) {
if (req.files) {
let sampleFile = req.files.sampleFile;
sampleFile.mv('/somewhere/on/your/server/filename.jpg', function(err, data) {
if (err) {
res.status(500).json(err);
} else {
req.data = data
next()
}
});
}
res.status(200);
}, function (req, res) {
res.status(500).json(req.data);
})
If you are using Node for express, the res param contains res.headersSent, which you can check if the headers have already been sent.
if(res.headersSent) {}
You can also find out a lot more here: https://nodejs.org/api/http.html#http_response_headerssent
I'm implementing a custom callback in PassportJS, however the additional info is not being passed through to the callback.
The documentation states:
An optional info argument will be passed, containing additional
details provided by the strategy's verify callback.
Such that if I were to pass the following to the callback:
return done(null, false, {message: 'Authentication failed.'});
with this demo code...
app.get('/login', function(req, res, next) {
passport.authenticate('basic', function(err, user, info) {
if (err) { return next(err); }
if (!user) { return res.status(401).json({message: info.message}); }
req.logIn(user, function(err) {
if (err) { return next(err); }
return res.status(200).json({message: 'success'});
});
})(req, res, next);
});
The info variable should contain a message field with the above mentioned value, however the info variable only contains the following:
"Basic realm=\"Users\""
I've looked at countless examples but I cannot figure out why the additional info is not being attached to the info variable. Any ideas?
I am using this code to add details . You may have to use flash message (connect-flash) library to send flash message and read them .
See if this fits your requirement
passport.use(
new LocalStrategy(
{},
function(username, password, done) {
var promise =authenticateService.authenticateUser(username,password);
promise.then(function(authToken)
{
if(authToken=="INVALID CREDENTIALS")
{
return done(null, false,{});
}else{
return done(null, {
username: username,authToken:authToken
},{"sucess":"sucesss"});
}
})
})
);
I am using passport for authentication and session handling. Everything works fine so far. I implemented a "Sign in" form to add new users to the app. After a user is added I would like to log him/her in automatically.
What is the best way to achieve this - should I redirect to "/login" with the user credentials or is there another/better way(call serializeUser) to do that?
So far I think I did not really understand the way the "done" function (in serializeUser and LocalStrategy) is working or what it is doing ...
Here is my code:
passport.serializeUser(function(user, done) {
done(null, user._id);
});
passport.deserializeUser(function(id, done) {
authProvider.findUserById('users', id, function (err, user) {
done(err, user);
});
});
passport.use(new LocalStrategy( function(email, password, done) {
authProvider.getUserByEmail('users', email, function(error, user){
if(error) { return done(error); }
if (!user) { return done(null, false, { message: 'Unknown user ' + email });}
if (user.password != password) { return done(null, false);}
return done(null, user);
});
}
));
app.post('/login',
passport.authenticate('local', { failureRedirect: '/login'}),
function(req, res) { res.redirect('/');});
app.post('/sign', function(req, res){
authProvider.saveUser(...do stuff), function(error, user){
if(error){
res.redirect('/sign');
} else {
res.redirect('/');
}
});
});
Does someone know how to do this?
Based on the Passport Guide req.login() is intended for this exact purpose.
This function is primarily used when users sign up, during which req.login() can be invoked to automatically log in the newly registered user.
Modifying krasu's code:
app.post('/sign', function(req, res){
authProvider.saveUser(...do stuff), function(error, user){
if ( error ){
res.redirect('/sign');
} else {
req.login(user, function (err) {
if ( ! err ){
res.redirect('/account');
} else {
//handle error
}
})
}
});
});
The potential error from the login() callback would come from your serializeUser() function.
Please use code from the #Weston answer bellow, because it's more universal and straightforward
Should look something like this
app.post('/sign', function(req, res){
authProvider.saveUser(...do stuff), function(error, user){
if(error){
res.redirect('/sign');
} else {
passport.authenticate('local')(req, res, function () {
res.redirect('/account');
})
}
});
});
I don't sure about name of strategy, but by default LocalStrategy should provide 'local' name
http://passportjs.org/guide/authenticate/
Try with:
app.post('/sign', function(req, res){
authProvider.saveUser(...do stuff), function(error, user){
passport.authenticate('local', (err, user) => {
req.logIn(user, (errLogIn) => {
if (errLogIn) {
return next(errLogIn);
}
return res.redirect('/account');
});
})(req, res, next);
});
});