Coding Journal

Jordan Vidrine

Daily Coding Journal

An experienced business owner + musician with a demonstrated history of management in the Energy sector and an insatiable appetite for learning new and complex skills. Transitioning from an ownership role & seeking employment in Web Development as a Jr Full Stack Developer.

July 24th 2019

Today I will be going back to the NodeJS course as I feel like I have been able to implement everything I’ve learned thus far on my own with the Drawing App.

Logging Out

Below we setup a route to log a user out. This simply authenticates the user using an auth middleware we wrote previously. Once the user is authenticated, it checks the users list of active tokens and removes the one currently saved to the req method, which was saved there during their authentication.

router.post('/users/logout', auth, async (req,res) => {
  try {
    req.user.tokens = req.user.tokens.filter((token) => {
      return token.token !== req.token
    })

    await req.user.save()

    res.send()
  } catch (e) {
    res.status(500).send()
  }
})

To ‘Log Out from All Locations’ we simply remove ALL of the tokens from the token array stored in the User object, rather than the only ‘active’ one. We can do this by setting the user’s tokens to an empty array like so. req.user.tokens = []

Hiding Private Data

In a real life situation, when a user logs in, we dont want to send to them ALL of their data. It could lead to exposure of their hashed password as well as other sensitive information. Express makes this easy for us.

We need to add a function to the method .toJSON. When we do this, any time Express SENDS information to a user, it will be first passed through the function stored at .toJSON of the User Model. For instance, anytime we want User data returned, we want to remove certain elements from that data. Here is how we did that: (you also need to make sure you are using express.json() like so ```app.use(express.json()))

userSchema.methods.toJSON = function () {
  const user = this;
  const userObject = user.toObject();

  delete userObject.password;
  delete userObject.tokens;

  return userObject
}

Adding Authentication to more user routes

We want to make sure we user authentication for all tasks. This reminds me that I need to implement this into my drawing app for myself as an admin. Right now, I have an admin panel that I need to be authenticated for, which works great. However, for the delete and approve functions of my Drawing app, I dont use authentication once I am logged into that admin panel. Since my tokens are set to expire after an hour, I can theoretically still use that section of the site as long as I dont refresh my browser. If I however, add authentication to the delete and approve routes, I will need to be authenticated any time I try and perform any manipulation on that data.

Deleting a User & Updating a User

To delete a user is fairly simple for this application. Once a user is logged in he/she can delete their account at this route.

// Delete user
router.delete('/users/me', auth, async (req,res) => {
    try {
      await req.user.remove()
      res.send(user)
    } catch (e) {
      res.status(400).send(e)
    }
})

The update user route needed some slight adjustment but it was simple as well.

// Update User
router.patch('/users/me', auth, async (req,res) => {
    // this will store an array of the keys the user is trying to update
    const updates = Object.keys(req.body)
    const allowedUpdates = ['name','email','password','age']
    const isValidOperation = updates.every((update) => allowedUpdates.includes(update))
    if (!isValidOperation) {
        return res.status(400).send({error: 'Invalid Updates!'})
    }
    try {
      // for each update, stored in the updates array, save the new info to the user object we have access to
      updates.forEach((updateField) => {
        req.user[updateField] = req.body[updateField]
      })
      await req.user.save()
      res.send(req.user)
    } catch (e) {
        res.status(400).send(e)
    }
})

Working with task routes & User Relationships

Now we will move onto the task routes. I am excited about this section because I will be able to directly use and implement it into the drawing prompt app I am working on. This will allow me to have users log in, submit prompts, and have prompts tied to the user who created it. We are going to learn how to tie tasks to the user who created it.

We will do this by tying the user ID of the creator into the task/prompt created.

First we will add the following to the Task Schema.

owner:
  {
    _id: {
      type: mongoose.Schema.Types.ObjectId,
      required: true
    },
    name: {
      type: String,
      required: true
    }
  }

Next, on the task creation route, we will add auth middleware and change how we save the task to the DB.

// add new task
router.post('/tasks', auth, async (req,res) => {
  // const task = new Task(req.body)
  const task = new Task({
    ...req.body,
    owner: {
      _id: req.user._id,
      name: req.user.name
    }
  })
  try {
    await task.save()
    res.status(201).send(task)
  } catch (e) {
    res.status(400).send(e)
  }
})

Of course, as with every well thought out tutorial, there is an easier way to tie in the User to the Task. Mongoose supplies the ability to do this. Inside of our task model, lets change the above to this:

owner: {
  type: mongoose.Schema.Types.ObjectId,
  required: true,
  ref: 'User' // make sure that it is typed and spelled the same as the Model you are tying it to
}

Next we will use Mongoose’s ability to ‘populate’ a field with info from the reference model. Since we made ‘User’ a ref for Task.owner, this is now possible like so:

const test = async () => {
  const task = await Task.findById('5d387e41b7d972185c99e962');

  await task.populate('owner').execPopulate()

  console.log(task.owner)
}
test();

// User Object for the user who created the task will be logged to the console.

This will convert the owner object of the Task into the entire profile of the user who created it. This only takes place inside the function. In the DB owner is still only the ID.

To do this in the opposite direction, IE. Get all tasks created by a certain user, we will implement something called a Virtual Property. This is a relationship between two entities. The user and the task. We can do this by adding the following to the User model.

userSchema.virtual('tasks', {
  ref: 'Task',
  // This is the local area where task is related to, which is the user ID (created by mongoose)
  localField: '_id',
  // the foreignField is the place where the data relationship is stored on the 'Task' model
  foreignField: 'owner'
})

The above code tells mongoose to create a virtual field called tasks. This will not be stored on the database, but will be used by mongoose to serve information to users. We specify the ref to be the Task model, the localField to be the _id of the User model, and the foreignField to be the owner field of the Task model.

To test this, lets use this code:

const main = async () => {
  const user = await User.findById('5d3879499ad93a1e7ca15126');
  await user.populate('tasks').execPopulate()
  console.log(user.tasks)
}

main();

// This logs to the console ALL tasks created by the User with the ID passed.

Very cool!

Authenticating Users to get tasks

The following code is refactored to take the logged in user into account when performing CRUD operations. One thing I noticed is that findByIdAndDelete will delete any task with the id passed in, regardless of any other querys passed to it. It is best to use findOneAndDelete which allows for multiple queries to be passed in.

// update task
router.patch('/tasks/:id', auth, async (req,res) => {
  const allowedUpdates = ['completed','description']
  const updates = Object.keys(req.body);
  const updatesAreAllowed = updates.every((update) => allowedUpdates.includes(update))

  if (!updatesAreAllowed) {
      return res.status(400).send({error: 'Invalid Updates'})
  }

  try {
    const task = await Task.findOne({_id: req.params.id, owner: req.user._id})

    if (!task) {
        return res.status(404).send()
    }

    updates.forEach((updateField) => {
      task[updateField] = req.body[updateField]
    })
    await task.save();
    res.send(task);
  } catch (e) {
    res.status(500).send(e)
  }
})

// delete task by id
router.delete('/tasks/:id', auth, async (req,res) => {
  try {
      const task = await Task.findByOneAndDelete({_id: req.params.id, owner: req.user._id})
      if (!task) {
          return res.status(404).send()
      }
      res.send(task)
  } catch (e) {
      res.status(500).send(e)
  }
})

Deleting all tasks for deleted User account

If a user were to delete his/her account, we want the tasks they have created to also be deleted. (In my drawing app this will not be the case.) We could add some code to the DELETE user route, to also delete the tasks they have created, but our approach will be different. We will use middleware we will add to the User model to do so. This was a super easy fix.

// Delete user tasks when user is removed.
userSchema.pre('remove', async function (next) {
  const user = this
  await Task.deleteMany({owner: user._id})
  next();
})

Thats it as far as my node learning goes for the day. For the second half of my day I will be diving back into a littl bit of the SICP lectures and book, Eloquent Javascrtipt, and an Algorithms course by Colt Steele.

Back to blog...