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.
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.
<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​
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.