Frosty CMS: Authorizing Users To Edit Content 👮

Intro

Continuing our series on Frosty CMS, where we are making a blog CMS from scratch using Node, Express, MongoDB and Passport. In this episode we will be adding user roles and authentication for specific tasks.

Currently in our application anyone who is signed in can edit any blog. We need to check whether the user is actually the person who wrote that blog before we allow them to edit or delete it.

Editing Blog Posts

Current Route

Here is our current route to render the Edit Blog form.

// edit blog form
router.get("/:id/edit", isLoggedIn, (req, res) => {
     // find blog with provided ID
    Blog.findById(req.params.id,(err, foundBlog) => {
        if(err){
            console.log("error finding blog data by ID");
        } else {
            // render single blog template with that post data
            res.render("editBlog.ejs", {blog: foundBlog});
        }
    });
});

We can see that we are currently using our middleware function isLoggedIn which is just checking to see if the request is coming from a registered user. Any user will do. We are going to replace this with a new middleware function. Here is some pseudo-code for what we are trying to accomplish.

// is user logged in?
// does user ID match user ID of author?
// if not, redirect
// if yes, continue

To check if the user is logged in we will use the same Passport method that we used for our .isLoggedIn middleware function that we wrote earlier. That method is .isAuthenticated.

Compare User ID’s

When it comes to comparing the user ID of the author and the user we must be a bit careful. You would think that we could compare the foundBlog.author.id with req.user._id, and if you console logged both of them they would be identical. But this is a special use case where req.user._id is a standard string, but foundBlog.author.id is a Mongoose Object. This is just something that you need to know, any attempt to compare these with === or == will always fail.

.equals()

To solve this Mongoose has provided us with a method just for this purpose, .equals. So instead of this

if(foundBlog.author.id === req.user._id){
// do something
}

we will do this

if(foundBlog.author.id.equals(req.user._id)){
// do something
}

And then we can chain these together into a new route like this

// edit blog form
router.get("/:id/edit", (req, res) => {
    // is user logged in?
    if(req.isAuthenticated()){
    // find blog with provided ID
        Blog.findById(req.params.id,(err, foundData) => {
            if(err){
                console.log(err);
            } else {
                // does user ID match user ID of author?
                if(foundData.author.id.equals(req.user._id)){
                    // render single blog template with that post data
                    res.render("editBlog.ejs", {blog: foundData});
                } else {
                    res.send("You are not authorized to do that.");
                }
            }
        });
    }    
});

Testing this shows that we can successfully reach the edit form for blogs that are our own, but not for others. Trying to edit blogs that were created with seed data crashes the application though, because they don’t have real user ID’s, just usernames we manually input. We can fix that later.

blog edit authorization demonstration

Middleware Function

There are many routes where we would want to include this functionality, so let’s refactor this into a new middleware function that we can include on several routes.

// check if blog author id matches user id
function checkBlogOwnership(req,res,next){
    // check if user is logged in
    if(req.isAuthenticated()){
        // find blog
        Blog.findById(req.params.id, function(err, foundBlog){
            if(err){
                res.send("Could Not Find Blog by ID");
            } else {
                // check for matching id
                if(foundBlog.author.id.equals(req.user._id)) {
                    next();
                } else {
                    res.send("You do not have permission to do that.");
                }
            }
        });
    }
}

and then we can just insert that middleware function into any blog route where we need to make sure the user is the one who created the blog. Here is the refactored edit blog form request.

// edit blog form
router.get("/:id/edit",checkBlogOwnership, (req, res) => {
    Blog.findById(req.params.id, (err, foundBlog) => {
        if(err){
            console.log(err);
        } else {
            res.render("editBlog.ejs", {blog: foundBlog});     
        }
    });
});

Hiding Edit/Delete Buttons

We’ve made it so that ownership is checked the edit form can be retrieved and submitted. But now as a quick cleanup, let’s also remove visibility of those buttons on our settings page unless you are the author.

edit button for author only

<div class="container-lg mt-5">     
    <h1 class="settings-title">Blog Posts</h1>
    <table class="table">
        <thead>
            <tr>
              <th scope="col">Image</th>
              <th scope="col">Title</th>
              <th scope="col">Author</th>
              <th scope="col">ID</th>
              <th scope="col"></th>
              <th scope="col"></th>
            </tr>
          </thead>
        <tbody>
          <!-- The Loop Starts Here -->  
          <% blogs.forEach((blog) => { %>
          <tr>
            <td><img 
                src="<%= blog.image %>" 
                alt="..."  
                class="settings-table-image">
            </th>
            <td><%= blog.title %></td>
            <td><%= blog.author.username %></td>
              <td><span class="settings-table-id"><%=blog._id%></span></td>
              <td>
              <% if(currentUser && blog.author.id.equals(currentUser._id)){ %>
                <a href="/blogs/<%=blog._id%>/edit" class="btn btn-outline-primary">Edit</a></td>
              <td>
              <form action="/blogs/<%= blog._id %>?_method=DELETE" method="POST">
                <button type="submit" class="btn btn-outline-danger">Delete</button>
              </form>
              <% } else { %>
                <td></td>
                <td></td>
              <% } %>
              </td>
          </tr>
          <% }); %>
          <!-- The Loop Ends Here -->
        </tbody>
      </table>
</div>

Note that we must && check currentUser first. If we do not we will get an error that currentUser._id is undefined.

In a future post we will introduce actual user roles, like Reader, Author, Editor and Administrator. Who will be able to control different items based on their permissions.

GitHub Repo

Ncoughlin: Frosty