Frosty CMS: Edit and Destroy Comments 💬

Nested Routes

Nested routes can get tricky. They require multiple unique Express variables in the route. Check out this PDF to get help on how to write nested routes.

📄 expressRouteBreakDown.pdf

The nested routes that require two different ID’s are really the only part of this that require any new knowledge. So that being said, we need to make the actual edit comment template, create a link to edit comments that follows our REST formula, create a route that handles the new edit comment REST route, and then another route that handles the saving of the comment.

REST Route

If we follow our proper convention the route will look like this:

/blogs/:id/comments/:id/edit

Keeping in mind that we cannot actually use :id twice. So it will actually look like this:

/blogs/:id/comments/:comment_id/edit

And then we can write a route that looks like this.

// edit comment form
// /blogs/:id/comments/...
router.get("/:comment_id/edit",isLoggedIn, (req, res) => {
    Comment.findById(req.params.comment_id, (err,foundComment)=>{
        if(err){
            console.log(err);
        } else {
            res.render('editComment.ejs', {blog_id: req.params.id, comment: foundComment});
        }
    });
});

This exact example is followed in the document above, so download that and follow the colors if you are confused on the variables passing between layers.

Edit Comment Form

That route will take us to our edit comment form.

<div class="container-lg mt-5">
    <h1 class="settings-title">Edit Comment</h1>
    <h4> Author: <strong><%= comment.author.username %></strong></h4> 
        <!--input for title, image, content -->
        <form action="/blogs/<%= blog_id %>/comments/<%= comment._id %>?_method=PUT" method="POST">
            <div class="form-group">
                <label for="contentInput">
                    <p><strong>Your Thoughts...</strong></p>
                </label>
            <textarea class="form-control" id="contentInput" rows="3" name="comment[content]"><%= comment.content %></textarea>
            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
</div>

Save Updated Comment

Which then submits to this route.

// save updated comment
router.put('/:comment_id', isLoggedIn, (req,res)=>{
    Comment.findByIdAndUpdate(req.params.comment_id, req.body.comment, (err, updatedComment)=>{
        if(err){
            console.log(err);
            res.redirect('back');
        } else {
            res.redirect('/blogs/' + req.params.id);
        }
    });
});

Delete Comments

There is nothing new here, but here is the delete route. I’ll skip the front end stuff.

// delete comments
router.delete("/:comment_id",isLoggedIn,(req, res) => {
    Comment.findByIdAndRemove(req.params.comment_id,(err) => {
        if(err){
          console.log("failed to .findByIdAndRemove Comment object");  
        } else {
            console.log("Comment with ID:" + req.params.comment_id + " has been deleted");
            res.redirect('/blogs/' + req.params.id);
        }
    });
});

Restrict Access

Of course we don’t want anybody to be able to edit any comment. So once again we go through our process to restrict a users ability to edit this based on ownership and role.

Our comments are available in two places. Directly on the blog post pages, and also in settings>comments. So we can add an Async function to the individual blog post route that checks a bunch of things for us. We want to check if the user is logged in for starters. If not they definitely can’t edit and we can skip all other checks. Then we can check the users role. If the user qualifies to edit we create a variable called editPermission that evaluates to true, and then we pass that variable in on the route along with the blog data.

router.get("/blog/:id",(req, res) => {
    
    // evaluate if the user should be able to edit
    function editorCheck(){
        return new Promise((resolve, reject)=>{
            // First check if user is logged in
            if(!req.isAuthenticated()){
                resolve(false);
            // If user role = Editor || Admin
            //  let editAllow = true;    
            } else if (req.user.role === "Administrator" || req.user.role === "Editor"){
                resolve(true);
            //  if the user is any other role    
            } else {
                resolve(false);
            }
        });
    }    
    
    // check if user has blanket permission to edit a comment before loading page.
    async function checkForEditOrAdmin(){
        try {
            const editPermission = await editorCheck();
            console.log("User is Admin or Editor: " + editPermission);
            // Find Blog by ID and populate comments
            Blog.findById(req.params.id).
            // populate comments
            populate("comments").
            exec((err, dbData) => {
                if(err){
                    console.log("error finding blog data by ID");
                } else {
                    // render single post template with that post data
                    res.render("singleBlog.ejs", {blog: dbData, editPermission: editPermission});
                    console.log("Article: " + dbData.title + " has loaded.");
                }
            });
        } catch(err) {
            console.log(err);
        }
    }
    checkForEditOrAdmin();
});

And then we just wrap our buttons on our template in some logic that checks for authorship and our editPermission variable.

<!-- Only author, admin or editor can modify content -->
<% if(currentUser && comment.author.id.equals(currentUser._id) || editPermission === true){ %>

Note 7/7/20: Looking back on this this code doesn’t actually protect the route, it just hides/displays the edit button based on the user role and author. That is a good thing to do, but I believe that after I wrote this I went back and actually protected the routes themselves properly. Check repo for samples.