Promises vs Async with Jsonwebtokens
Asked Answered
B

4

13

I completed a Node app tutorial and went back to rewrite the code with async/await to better learn how it's done. However I have a route handler the I can't get right without using promises:

getProfile: function(id){
    return new Promise(function(resolve, reject){
        Profile.findById(id, function(err, profile){
            if (err){
                reject(err)
                return
            }

            resolve(profile.summary())
        })
    })
}

which I rewrote as:

getProfile: async (req, res, next) => {
    const profileId = req.params.id;
    const profile = await Profile.findById(profileId);
    res.status(200).json(profile)
}

EDIT 2: OK i also realized I rewrote:

create: function(params){
    return new Promise(function(resolve, reject){

        var password = params.password
        params['password'] = bcrypt.hashSync(password, 10)

        Profile.create(params, function(err, profile){
            if (err){
                reject(err)
                return
            }

            resolve(profile.summary())
        })
    })
}

as

newProfile: async (params, res, next) => {
    const newProfile = new Profile(params);
    const password = params.password
    params['password'] = bcrypt.hashSync(password, 10)
    const profile = await newProfile.save();
    return profile.summary()
},

which could very well be causing an issue with the jsonwebtokens :<

The API endpoint I am having trouble with uses jsonwebtokens:

var token = req.session.token
    utils.JWT.verify(token, process.env.TOKEN_SECRET)
    .then(function(decode){
        return controllers.profile.getProfile(decode.id)
    })
    .then(function(profile){
        res.json({
            confirmation: 'success',
            profile: profile
        })
    })
    .catch(function(err){
        res.json({
            confirmation: 'fail',
            message: 'Invalid Token'
        })
    })
}

The async code works for both the GET and POST requests to /profile route but keeps getting the 'Invalid Token' message in the API catch block. I am new to both promises and async code so Im sure theres a lot I'm not understanding right now.

So my questions is how could I have rewritten the promise to pass off the profile object in the correct format?

Full Files:

controllers/ProfileController.js

var Profile = require('../models/Profile')
var Album = require('../models/Album')
var Promise = require('bluebird')
var bcrypt = require('bcryptjs')

module.exports = {
    index: async (req, res, next) => {
        const profiles = await Profile.find({});
        const summaries = []
        profiles.forEach(function(profile){
            summaries.push(profile.summary())
        })
        res.status(200).json(summaries)
    },

    newProfile: async (params, res, next) => {
        const newProfile = new Profile(params);
        const password = params.password
        params['password'] = bcrypt.hashSync(password, 10)
        const profile = await newProfile.save();
        return profile.summary()
    },

    getProfile: function(id){
        return new Promise(function(resolve, reject){
            Profile.findById(id, function(err, profile){
                if (err){
                    reject(err)
                    return
                }

                resolve(profile.summary())
            })
        })
    },

    updateProfile: async (req, res, next) => {
        const { profileId } = req.params;
        const newProfile = req.body;
        const result = await Profile.findByIdAndUpdate(profileId, newProfile);
        res.status(200).json({success: true})
    },

    getProfileAlbums: async (req, res, next) => {
        const profileId = req.params.id;
        const profile = await Profile.findById(profileId);
    },

    newProfileAlbum: async (req, res, next) => {
        const newAlbum = new Album(req.body);
        console.log('newAlbum', newAlbum)
    }

}

routes/profile.js:

var express = require('express');
const router = require('express-promise-router')();

const ProfileController = require('../controllers/ProfileController')

router.route('/')
    .get(ProfileController.index)
    .post(ProfileController.newProfile);

router.route('/:id')
    .get(ProfileController.getProfile)
    .patch(ProfileController.updateProfile);

router.route('/:id/album')
    .get(ProfileController.getProfileAlbums)
    .post(ProfileController.newProfileAlbum);

module.exports = router;

routes/account.js:

var express = require('express')
var router = express.Router()
var controllers = require('../controllers')
var bcrypt = require('bcryptjs')
var utils = require('../utils')

router.get('/:action', function(req, res, next){
    var action = req.params.action

    if (action == 'logout'){
        req.session.reset()
        res.json({
            confirmation: 'success'
        })
    }

    if (action == 'currentuser'){
        if (req.session == null) {
            res.json({
                confirmation: 'success',
                message: 'user not logged in'
            })

            return
        }

        if (req.session.token == null) {
            res.json({
                confirmation: 'success',
                message: 'user not logged in'
            })

            return
        }

        var token = req.session.token
        utils.JWT.verify(token, process.env.TOKEN_SECRET)
        .then(function(decode){
            return controllers.profile.getProfile(decode.id)
        })
        .then(function(profile){
            res.json({
                confirmation: 'success',
                profile: profile
            })
        })
        .catch(function(err){
            res.json({
                confirmation: 'fail',
                message: 'Invalid Token'
            })
        })
    }
})

router.post('/register', function(req, res, next){
    var credentials = req.body

    controllers.profile
    .newProfile(credentials)
    .then(function(profile){
        var token = utils.JWT.sign({id: profile.id}, process.env.TOKEN_SECRET)
        req.session.token = token
        res.json({
            confirmation: 'success',
            profile: profile,
            token: token
        })
    })
    .catch(function(err){
        res.json({
            confirmation: 'fail',
            message: err.message || err
        })
    })
})

router.post('/login', function(req, res, next){

    var credentials = req.body
    controllers.profile
    .find({userName: credentials.userName}, true)
    .then(function(profiles){
        if (profiles.length == 0){
            res.json({
                confirmation: 'fail',
                message: 'Profile not found'
            })
            return
        }
        var profile = profiles[0]

        var passwordCorrect = bcrypt.compareSync(credentials.password, profile.password)
        if (passwordCorrect == false){
            res.json({
                confirmation: 'fail',
                message: 'Incorrect password'
            })

            return
        }

        var token = utils.JWT.sign({id: profile._id}, process.env.TOKEN_SECRET)
        req.session.token = token

        res.json({
            confirmation: 'success',
            profile: profile.summary(),
            token: token
        })
    })
    .catch(function(err){
        res.json({
            confirmation: 'fail',
            message: err
        })
    })
})

module.exports = router

utils/JWT.js:

var jwt = require('jsonwebtoken')
var Promise = require('bluebird')

module.exports = {

    sign: function(obj, secret){
        return jwt.sign(obj, secret)
    },

    verify: function(token, secret){

        return new Promise(function(resolve, reject){
            jwt.verify(token, secret, function(err, decode){
                if (err){
                    reject(err)
                    return
                }

                resolve(decode)
            })
        })
    }
}
Broads answered 8/9, 2017 at 3:37 Comment(11)
you're not returning anything in your rewriteIchthyo
The original getProfile without async should have been written as return Profile.findById(id).then(profile => profile.summary()), FWIW.Horta
You will want to console.error(err) the error instead of failing with an indeterminate message, so that you know what the actual problem is.Cystolith
Your rewrite does a completely different thing?! It's taking different parameters now, does something different with the result, and assumes that Profile.findById already returns a promise instead of taking a callback.Cystolith
@Cystolith sorry I mistyped my rewrite code originally. It is supposed to return profile.summary() which I was hoping would be the same result as resolve(profile.summary())Broads
But they take different arguments as well. The first version takes an id, the second version looks like a route that takes a request, response, and next argument. If you had the rewrite call the original instead of calling Profile.findById directly it would work.Beggar
@Beggar correct, I don't really know what I'm doing :/ . It did kind of work as is however ...Broads
@NolanDavis Can you post more of the code... specifically the names of the files that each snippet is in?Beggar
@Beggar yes i added it above. thanks for helpingBroads
If you go into the models/Profile file, and change the function to not use a callback, but rather return a promise, your change should work. If you want to post the models/Profile file I can edit it to show you what I mean.Beggar
@Beggar Im thinking now the error is instead when i create a profile and am not passing of the correct object (or what should probably be a callback function) to the Jsonwebtoken Account api route. That would cause the error when i check the token against the current accountBroads
P
6
validate.js:

async verifyToken(jwt,token,key){
   if(!token) return {};
   return new Promise((resolve,reject) =>
      jwt.verify(token,key,(err,decoded) => err ? reject({}) : 
                                                  resolve(decoded))
   );
}


auth.js:

const jwt = require('jsonwebtoken');
const {verifyToken} = require('../functions/validate');
const TOKEN_KEY = "abrakadabra";

const token = await verifyToken(
     jwt, req.session.token, TOKEN_KEY
).catch(err => {}) || {};
// || {} - need if decoded is undefined

console.log(token.id);
Palaestra answered 13/1, 2021 at 16:52 Comment(2)
Answers should usually be accompanied by an explanation so people also learn from your code snippetSlovakia
Sorry man , but my english is not very good, but the code is short and clear, only copy and try.Palaestra
C
3

You should not have rewritten it to an async function. This is totally fine for promisification of the Profile.findById method:

getProfile: function(id) {
    return new Promise(function(resolve, reject){
        Profile.findById(id, function(err, profile) {
            if (err) reject(err);
            else resolve(profile);
        })
    }).then(profile => profile.summary());
}

You can rewrite the code of your API endpoint to use async/await syntax however:

async function(req, res, next) {
    try {
        const token = req.session.token
        const decode = await utils.JWT.verify(token, process.env.TOKEN_SECRET);
        const profile = await controllers.profile.getProfile(decode.id);
        res.json({
            confirmation: 'success',
            profile: profile
        });
    } catch(err) {
        console.error(err);
        // maybe also call next(err)?
        res.json({
            confirmation: 'fail',
            message: 'Invalid Token'
        });
    }
}
Cystolith answered 8/9, 2017 at 4:42 Comment(0)
M
1

Instead of this:

jwt.verify(token, config.secret, function (err, decoded) {
    if (err) 
      return res.status(500).send({ 
          auth: false,
          message: 'Failed to authenticate token.' 
      });
   res.status(200).send(decoded);
});

using this in async-await form:

let isVerified = await jwt.verify(token, config.secret)

here decoded and isVerified have the same data body

Myrtlemyrvyn answered 26/10, 2020 at 10:29 Comment(0)
B
0

Profile.findById doesn't return a promise. When it works originally you're using a callback. Wrap Profile.findById in a promise then use the await keyword on the wrapped function.

Beggar answered 8/9, 2017 at 4:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.