Learn JS: FSFS: Section 6 – User-Created Posts


Courses

Updated May 3rd, 2022

Section 6 Table of Contents

Go to the complex app summary

Chapter Details

Chapter 70 – User Created Post

Create new template named create-post.ejs and grab code from github repo.

Update form action in template to be “/create post” and and .get() route under the comment “post related routes.” Point to the function postController.viewCreateScreen

In postController.js file create exports.viewCreateScreen function that has line res.render(‘create-post’).

Chapter 70 at 6m: Create userController.mustBeLoggedIn() and add it to router.js file. In the function pass req, res, and next. If logged in (req.session.user), next(), or else get an error and redirect to the home page. This is a reusable function you can use to protect other routes as well.

Chapter 70 at 12m: Split the header and footer into their own template files and save to a new “includes” folder put in the “views” folder. This is not needed if you don’t have or want a lot of pages.

<%- include('includes/header')%>

Chapter 71 – Pass Session Data

Create a middleware function to pass the same current user session data into multiple page templates by putting it into the header.

app.use(function(req, res, next) {
res.locals.user = req.session.user
next()
})

Note that res.locals is an object available within EJS templates

Have home-guest.ejs template pull in shared header.ejs include file. In the header.ejs file conditionally show code depending on whether you’re logged in or not. If (user) then show this else show that.

<% if (user) { %>
  html code if logged in
<% } else { %>
  html code if not logged in
<% } %>

Implement a shared reusable footer.ejs template in the views/includes folder using the same technique for the header.

Chapter 72 – Save Post Into Database: You have a form with an action that has method=POST to “/create-post”. In the router.js set a route for that .post url and run userController.mustBeLoggedIn and then postController.viewCreateScreen

Create a Post.js model file and require this into the postController.js file. Add a exports.viewCreateScreen function and and an exports.create function to the postController.js file.

exports.viewCreateScreen = function(req, res) {
res.render('create-post')
}
exports.create = function(req, res) {
let post = new Post(req.body)
post.create().then(function() {
res.send("New post created.")
}).catch(function(err) {
res.send(err)
})
}

In Post.js model’s main constructor function add this.data=data property and this.errors= empty array property with a .then() and .catch()

// in Post.js model file
let Post = function(data) {
  this.data = data
  this.errors = []
}
module.exports = Post

Create prototype methods named .cleanup .validate and .create

Post.prototype.cleanup – makes sure values must be strings using typeOf and makes sure no extra bogus properties by setting the properties you want (title, body, createdDate, etc.). Run trim on title and body properties to ignore any empty spaces.

Post.prototype.validate – make sure neither fields are blank using if statement that throws an error if left blank by pushing message onto the errors array that was set in the constructor function.

Post.prototype.create – rSet this function up to return a new Promise that runs this.cleanup() method and then this.validate() method and makes sure if there’s no errors in the errors array then actually stores document in database else reject(this.errors).

Chapter 72 at 15m: Require in database by creating a postsCollection constant variable and setting equal to require(‘../db’).db().collection(‘posts’).

Leverage this by saving post to the database in the create function using .insertOne() mongoDB method with .then() and .catch() since the .insertOne() method returns a promise.

Note that .then().catch() may be preferred to async/await when you need to do one thing then do one more thing.

Chapter 73 – Post Model (Part 2): At this point we are missing an author id value for any posts in the post collection. We don’t want to just set the author id to the user’s username because in most applications that can be changed. What we will do instead is utilize the ObjectID that is automatically-generated for each document in the users collection. This is what we’ll set as the author id for any document in the posts collection.

When a user logs in we’ll need to store this user ObjectID value into the current sessions data, and then pass that into the newly created post object when we create a post.

In the userController.js file, within the login and register functions, add to what you are storing in the session data another field of _id: user.data._id

In postController.js leverage the _id property that lives in the session data.

let post = new Post(req.body, req.session.user._id)

In our Post.js model in the constructor function add a second parameter named userid and then add a property called this.userid = userid. Down in the cleanup function where we are setting the object’s properties add a new property called author and set it to this.userid. This saves it as a simple string of text but it’s a best practice to go one step further and utilize mongodb’s ObjectID() object type. This is a special way that mongoDB treats id values and we want to honor it. At the top of this file set a const ObjectID equal to require(‘mongodb’).ObjectID. Now set the author property to ObjectID(this.userid)

Chapter 74 – Viewing a Post (Part 1)

Viewing an existing individual post on a separate route and show template “single-post-screen.ejs.” Include header and footer. Route should be router.get(‘/post/:id’) and then run a function called postController.viewSingle

Note: Not running mustBeLoggedIn function so public can view.

Chapter 75 – Viewing a Post (Part 2)

In the “postController.js” file we want to create and export a new “viewSingle” function that that looks up posts from the database using a function, findSingleByID, and then renders a newly created “single-post-screen.ejs” template, passing it the post data and rendering a 404 template if the attempt fails.

In “postController.js” file, create and exports the “viewSingle” function as an “async” function to generate the “post/:id” route. Note we are not using the object oriented approach here. We are just leveraging the Post model’s namespace to create and later call a function. In a try block have “let post = await Post.findSingleById() and pass this function req.params.id.” In the request object there will be a “params” object and inside of that we can look for the “id.” Pass this into the “single-post-screen” template with “res.render(‘single-post-screen’, {post: post}).” In the catch block have a function that renders a 404 template (if user types in something crazy or typo).

Go into Post.js model file to create the “findSingleById” function.

You can leverage Post as a constructor or to just call a simple function (which is what we are doing here) called Post.findSingleByID and set it equal to a function. This function should use return a new Promise that is an async function and if the (typeof(id) != “string” || !ObjectId.isValid(id)) then reject() and return, else, let post equal await postsCollection.findOne({_id: new ObjectID(id)}) to find a post id and if there’s a post then render data of {post.post} in the ejs template named “single-post-screen”

Post.findSingleById = function(id) {
  return new Promise(async function(resolve, reject) {
    if (typeof(id) != "string" || !ObjectID.isValid(id)) {
      return reject()
    }
    let posts await postsCollection.aggregate([
      {$match: {_id: newObjectID(id)}},
      {$lookup: {from: "users", localField: "author", foreignField: "_id", as: "authorDocument"}},
      {$project: {
        title: 1,
        body: 1,
        createdDate: 1,
        author: {$arrayElemAt: ["authorDocument"], 0}
      }}
    ]).toArray()
  })
}

Note: that mongoose, which is an object model for MongoDB, uses the same style of leveraging the object’s namespace to create a function.

Pull in dynamic title, date, body, and author in EJS template as well. The author requires a second-step database lookup described in the next chapter.

<%= post.createdDate.getMonth() + 1%>/<%= post.createdDate.getDate()%>/<%= post.createdDate.getFullYear()%>
// remember month's are zero based

Chapter 76: At the beginning of this chapter you create a “404.ejs” template.

Chapter 76 at 3:40 (until 3:00 mark in next movie, this might be the most important content in the entire course):

In trying to pull in author info to display in the “viewSingle.ejs” template you must do a two-step or second-layer database search.

This lookup is required because in a post document we’re only storing the ID of the author and the actual name lives in the user document (and not the post document). So we pull in data from the postsCollection and call it “authorDocument” and replace this with the value for author.

Instead of .findOne use .aggregate() to perform multiple db operations and .toArray() to put this in the correct format and return a promise. Let posts equal if posts.length. You pass aggregate an array of configuration objects like $match $lookup $project. A good idea to re-watch video over and over.

let posts = await postsCollection.aggregate([
{$match: {_id: new ObjectId(id)}},
{$lookup: {from: "users", locaField: "author", foreignField: "_id", as: "authorDocument"}},
{$project: {
title: 1,
body: 1,
createdDate: 1,
author: {#arrayElemAt: ["$authorDocument", 0]}
}}
]).toArray()

Chapter 77 – Performing a Lookup in MongoDB (Part 2)

Continue previous chapter by cleaning up the author document property returned in each post object before passing to the controller.

// clean up author property in each post object
posts = posts.map(function(post){
post.author = {
  username: post.author.username,
  avatar: new User(post.author, true)
}
return post
})

Add a second argument named getAvatar to main User Object. Minutes 3-7 are all about the gravatar if you’re going that route. Render author in template.

Chapter 78 – User Profile Screen

Incredibly important chapter!

Create “profile.ejs” template in the “views” folder, create by copy and pasting from repo “profile-posts.html” between header and footer. Set up route as:

router.get("/profile/:username", userController.ifUserExists, userController.profilePostsScreen)

Create an “ifUserExists” function in the “userController.js” file create the shell of these new functions:

exports.ifUserExists = function(req, res, next) {
  next()
}
exports.profilePostsScreen = function(req, res) {
  res.render("profile")
}

Then build out the “ifUserExists”

exports.ifUserExists = function(req, res, next) {
  User.findByUsername(req.params.username).then(function(userDocument) {
  req.profileUser = userDocument
  next()
  }).catch(function() {
   res.render("404")
  })
}

Now in the “User.js” model file, create a static “findByUsername” function.

User.findByUsername = function(username) {
  return new Promise(function(resolve, reject) {
    if (typeof(username) != "string") {
      reject()
      return
    }

    usersCollection.findOne({username: username}).then(function(userDoc) {
      if (userDoc) {
        resolve()
      } else {
        reject()
      }
    }).catch(function() {
      reject()
    })
  })
}

We could resolve the entire “userDoc” but that contains password, and even though the password is hashed, you should really not leak out or expose data that is not necessary. So clean it up a bit before returning.

User.findByUsername = function(username) {
  return new Promise(function(resolve, reject) {
    if (typeof(username) != "string") {
      reject()
      return
    }

    usersCollection.findOne({username: username}).then(function(userDoc) {
      if (userDoc) {
        userDoc = new User(userDoc, true)
        userDoc = {
          _id: userDoc.data._id,
          username: userDoc.data.username,
          avatar: userDoc.avatar
        }
        resolve(userDoc)
      } else {
        reject()
      }
    }).catch(function() {
        reject()
    })
  })
}

Back in the “userConroller.js” file, when rendering the profile template pass it the relevant data (username and avatar).

In the “profile.ejs” template file, leverage this username and avatar data by displaying it on the page.

Chapter 79 – View Posts by Author: On the “profile.ejs” template you want to show a list of posts. In the “userController.js” file, in the “profilePostsScreen” function, use the “Post.js” model to get posts for a given author ID.

exports.profilePostsScreen = function(req, res) {
  // ask post model for posts by a certain author id
  Post.findByAuthorId(req.profileUser._id).then(function(posts) {
    res.render("profile", {
      posts: posts,
      profileUsername: req.profileUser.username,
      profileAvatar: req.profileUser.avatar
    })
  }).catch(function() {
    res.render("404")
  })
}

In “Post.js” create a static “findByAuthorId” method that takes in an “authorId” parameter.

Instead of borrowing heavily or copying from the “Post.findSingleById” static method, creating a lot of duplicate code, we will instead create a new function they can both leverage.

So instead of finding documents that match a given “post id” we want to find all documents that match a given “author id.”

Here is the existing “findSingleById” function for reference.

Post.findSingleById = function(id) {
  return new Promise(async function(resolve, reject) {
    if (typeof(id) != "string" || !ObjectID.isValid(id)) {
      return reject()
    }
    let posts await postsCollection.aggregate([
      {$match: {_id: newObjectID(id)}},
      {$lookup: {from: "users", localField: "author", foreignField: "_id", as: "authorDocument"}},
      {$project: {
        title: 1,
        body: 1,
        createdDate: 1,
        author: {$arrayElemAt: ["authorDocument"], 0}
      }}
    ]).toArray()

    // clean up author property in each post object
    posts = posts.map(function(post) {
      post.author = {
        username: post.author.username,
        avatar: new User(post.author, true).avatar
      }

      return post
    })

    if (posts.length) {
      resolve(posts[0])
    } else {
      reject()
    }
  })
}

Chapter 79: Duplicate Post.findSingleById and rename it Post.reusablePostQuery. Rework the function to have an aggOperations variable that starts with a parameter uniqueOperations and concatenates on the rest of the shared operations.

Post.reusablePostQuery = function(uniqueOperations) {
  return new Promise(async function(resolve, reject) {
    let aggOperations = uniqueOperations.concat(
      [
{$lookup: {from: "users", localField: "author", foreignField: "_id", as: "authorDocument"}},
      {$project: {
        title: 1,
        body: 1,
        createdDate: 1,
        author: {$arrayElemAt: ["authorDocument"], 0}
      }}
    ])
    let posts = await postsCollection.aggregate(aggOperations).toArray()

    // clean up author property in each post object
    posts = posts.map(function() {
      post.author = {
        username: post.author.username,
        avatar: new User(post.author, true).avatar
      }
      return post
    })
      resolve(posts)
  })
}

Now re-work the Post.findSingleById function to leverage the Post.reusablePostQuery function.

Post.findSingleById = function(id) {
  return new Promise(async function(resolve, reject) {
    if (typeof(id) != "string" || !ObjectID.isValid(id)) {
      return reject()
    }
    
    let posts = await Post.reusablePostQuery([
      {$match: {_id: new ObjectID(id)}}
    ])

    if (posts.length) {
      resolve(posts[0])
    } else {
      reject()
    }
  })
}

Build out the “findByAuthorId” function to using the “reusablePostQuery” function.

Post.findByAuthorId = function(authorId) {
  return Post.reusablePostQuery([
    {$match: {author: authorId}},
    {$sort: {createdDate: -1}} // DESC order
  ])
}

In the “profile.ejs” file leverage the data it is receiving to display a list of posts with a map function

<% posts.forEach(function(posts) {
  // Link tag for each post
  // use EJS tags with equals sign for href, src, title, date
}) %>

Update and Delete Posts Starts Here

Chapter 80 – Is the Current Visitor the Owner of the Post?

Need to set things up so only the author of a post sees the edit and delete icons. In the main “app.js” file, set up a piece of middleware that runs for every request to make the current “userID” available on the request object. Do this by adding some code to an existing middleware function

app.use(function(req, res, next) {

  // make current user ID available on the request object

  if (req.session.user) {
    req.visitorId = req.session.user._id
  } else {
    req.visitorId = 0
   }
  
  // make user-session data available from within view templates

  res.locals.user = req.session.user
  next()
})

In “postController.js” find the “findSingleById” function and pass it a second argument “req.visitorID.” In the post model in “findSingleById,” add a second parameter to add “visitorId” and in the body of this function also pass “reusablePostQuery” function a second argument of “visitorId.”

Go to the “reusablePostQuery” function and receive the argument by creating a second parameter, “visitorId” and in this function body, in the map function, just above “post.author= {},” add a line of code that reads “post.isVisitorOwner={post.authorId.equals(visitorId)}.”

Note that equals() is a mongoDB method for checking mongoDB objectIds that returns true or false.

In order for this to work we need to go higher in the code in the “$project” object and create a new line that reads “authorId: “$author”

If you include a dollar sign within quotes, mongoDB knows you are talking about that actual field, and not a string of text.

Ch 80 11m: In “single-post-screen.ejs” view template, use an if statement to say only “if isVisitorOwner” then show the edit and delete buttons else don’t show.

Chapter 81 – Edit Screen for a Post

Update the href attribute in the html to be dynamic that looks for post you are interested in.

In router file set up a get request for post/:id/edit and have it run postController.viewEditScreen function. In the postController.js file create this exports.viewEditScreen function that asks the “Post.js” model file for relevant data and then renders an edit screen template. Create an “edit-post.ejs” template using the “create-post.ejs” template as a base.

Ch 81 at 4m: Pre-populate fields by sending data to the template and then use it in the template in the value attribute of the input.

exports.viewEditScreen = async function(req, res) {
try {
let post = await Post.findSingleById(req.params.id)
res.render("edit-post", {post: post})}
catch { res.render("404")}
}

Now in view template, in the value attributes on the input tags, you can use ejs tags to dynamically pull in data using post.title, post.body

Chapter 82 – Update Posts in the Database After Editing

Update the form action with “/post/:id/edit” in the edit-post template and use ejs tags to make the :id portion dynamic using post._id. Add a route Router.post(“post/:id/edit”) that runs a postController.edit function.

In the”postController.js” file, create this edit function that initializes a new post variable and sets it equal to (instantiate) a “new Post(req.body, req.visitorId, req.params.id)” object and then runs a “post.update()” function that we will create and will be set up to return a promise. So to “post.update()” add .then() and .catch() and give both of these arrow functions.

In the .catch() show a flash message, manually save the session data and redirect to the homepage. In the .then() function have a parameter of status and inside the function block have an if/else statement. For the “if condition” check to see if (status equals “success”) then add a flash message, manually save session data and redirect back to this same edit page. If not a “success” then run a “post.errors.forEach()” with a function as an argument that has a parameter of error and in the block adds each error as a flash message. Then manually save session data and redirects back to this same edit page.

Chapter 83 – Continue Updating Database After Editing Post

In “Post.js” model file create a “Post.prototype.update” function that returns a promise and in the promise function has a try and catch block. In the try block create variable of post with let that is set equal to Post.findSingleById(a, b) where a is post id you want to look up (“this.requestedPostId”) and b is “this.userid”.

In order for “this.requestedPostId” to work as a paremeter in the “update” function above you need to go to your Post constructor function and add a third parameter of “requestedPostId” and a property of “this.requestedPostId set equal to requestPostId”.

need to await Post.findSingleByid and convert Promise function to async.

Under the let post = awaitPost.findSingleById line, have an if/else block that checks if post.isVisitorIsOwner updates the database by calling a separate function (“await this.actuallyUpdate()” and then resolves. In the else block have the promise reject. In the catch block have promise reject.

The reason we create a standalone “actuallyUpdate” function is because the post.prototype.update function is already several layers deep and this will help keep our code more readable.

Create the “Post.prototype.actuallyUpdate” method just below the “update” method and have it set it up to return a promise. In the Promise() have an async arrow function. In the function block run the cleanup and validate functions and then have an if/else block that says only if there’s no errors, run the “findOneAndUpdate” mongoDB method on the postsCollection to update the database and then resolves with a value of “success”. If there are errors then resolve with a failure of “Failure”.

await postsCollection.findOneAndUpdate({_id: new ObjectId(this.requestPostId)}, {$set: {title: this.data.title, body: this.data.body}})

Tweak the “update” function to catch create a status variable that store the resolve message.

if(post.isVisitorOwner) {
let status = await this.actuallyUpdate()
resolve(status)
}

Chapter 83 at 11:00 – 17:00 Aside for Flash Messages

Why? In relation to the “update” promise rejecting, the controller is making extensive use of the flash messages to show relevant messages to the user. We also want to send green messages in addition to the red messages.

Implement flash messages in the “edit-post.ejs” template by borrowing from “home-guest.ejs” template. Paste in twice and tweak second bit of code to be “success.forEach()” instead of “errors.forEach()” to be able to have success messages in green (and not just red errors messages). Update the corresponding bootstrap class to alert-success.

Actually create a standalone “flash.ejs” template in the includes folder and include this new “flash.ejs” template within the “edit-post.ejs” template. Do this for all other view templates including “create-post.ejs” and “single-post-screen.ejs” and “home-dashboard.ejs” and “home-guest.ejs.”

Instead of manually passing flash data into the template every single time we can create a piece of middleware instead in the “app.js”file. To see why should do this go to the “userController.js” file and search for the “exports.home” function and the line where we render the “home-guest.js” template. We pass the “home-guest.ejs” template some data an object {errors: req.flash(‘errors’) and regErrors: req.flash(‘regErrors’). The “errors” portion is data all templates should have whereas “regErrors” is specific to just the “home-guest.ejs” template. We don’t to pass “errors” manually like this.

To create the middleware go to the “app.js” file and add to an existing custom middleware function.

app.use(function(req, res, next) {
// make current user id avail...
// code here
// make user session data avail...
// code here
// make all errors and success flash msgs available to all template
res.locals.errors = req.flash("errors")
res.locals.success = req.flash("success")
})

Updating the database should no be functioning properly.

Chapter 84 – Miscellaneous Improvements

Lock down the “edit” URL for those who are not the owner. In “postController.js” file and look for “exports.viewEditScreen” function and cut the res.render line. Create an if/else and if (post.authorId == req.visitorId) paste back in “res.render” line else hit with flash message, manually save session data and redirect back to the homepage.

Change “edit-post.ejs” to add a link that sends back to post-permalink. Add a link with dynamic href.

Chapter 84 at 5m: When you create a new post and hit submit you are redirected back to the view-post for the new url with a green success flash message. Update “create-post.ejs” template from simple res.send to send flash message, amanully save session data and “res.redirect” back to dyamic url. In the catch block, if the “post.create” promise fails, use a .forEach() to push errors as flash message using an arrow function, manually save session data and redirect.

Need to set up “post.create” function that returns a promise to resolve() with a value that is the new post’s id so we can redirect to the correct path. In Post.js find the Post.prototype.create method and the line where we have insertOne(). InsertOne() is a mongoDB method that will return a promise and when that promise resolves it will resolve with a bunch of information about the database action that just took place. So we can add a parameter of “info” to the .then() and we can now resolve with a value of “info.ops[0]._id” and now back in the “create” function, in the “postController.js” file, in the function in the .then(), pass a parameter of newId that and use this as part of a dynamic redirect url.

Dig into the mongoDB docs to learn more about ops and the rest of the “bunch of information about the database action that just took place.”

Chapter 84 at 12m: Protect edit routes with the “mustBeLoggedIn” function for “router.get/post/:id/edit” and “router.post/post/:id/edit” routes by adding a userController.mustBeLoggedIn function to these routes in the router file. There is no need to waste a trip to the database to see if they are the owner because we know they are not the owner if they are not even logged in.

Chapter 84 at 14 and Chapter 85: Skipping because this is where we bring in the markdown package to allow non-malicious user-generated line breaks and strong tags and in a post by preventing EJS template engine from escaping.

Note that escaping user-generated content is 100% necessary and great for security reasons to prevent being vulnerable from a cross-site-scripting attack.

Chapter 86: In the “postController.js” file update the “exports.viewEditScreen” function replace what we currently have with the snippet below:

exports.viewEditScreen = async function(req, res) {
  try {
    let post = await Post.findSingleById(req.params.id, req.visitorId)
    if (post.isVisitorOwner) {
      res.render("edit-post", {post: post})
    } else {
      req.flash("errors", "You do not have permission to perform that action.")
      req.session.save(() => res.redirect("/"))
    }
  } catch {
    res.render("404")
  }
}

Essentially, the course author forgot that if we pass our Post model the current user ID it can figure out if the current request is the owner of the post or not. So then for the if statement condition, I’m using that isVisitorOwner property instead.

I strongly encourage you to copy and paste and use the code above as this will avoid frustration in further lessons; we eventually remove authorId from returned Post objects entirely; and the above code will work flawlessly with that architecture while the code from the previous video lesson will not.

Chapter 87 – Let the User Delete their Post

Update the form action in “single-post-screen.ejs” to be “/post/:id/delete.” Keep in mind the “post._id” portion of this needs to be wrapped in EJS tags since it’s dynamic. Set up a route in the “router.js” file that runs the “userController.mustBeLoggedIn” function and then call a “postController.delete” function.

Set up a “exports.delete” function in the “postController.js” file that calls Post.delete() which will be set up to return a promise so add a .then() and .catch() and arrow functions inside of each of those. Pass Post.delete() an argument of “req.params.id” that represents the id for the post you want to delete. Also pass the id of the current user that is trying to complete the delete action “req.visitorId.” If the promise is successful, if it resolves, in the .then() function block, have a success flash message show, manually save the session data, and redirect to the “/profile/${req.session.user.username}” (swap quotes for backticks). If the promise rejects then in the .catch(), in the function block, have an error flash message show, manually save the session data and redirect to the home page.

Go to the “Post.js” model and create a “Post.delete” function that returns a new promise. Inside the Promise parentheses provide it a new async arrow function with parameters of resolve and reject and try and catch blocks. In the try block let post be equal to await Post.findSingleById().

Pass two arguments to the “Post.delete” function, postIdToDelete and currentUserId. When you call Post.findSingleById() pass itpostIdToDelete and currentUserId.

Below the let post = await Post.findSingleById() line of code have an if/else statement. If (post.isVisitorOwner) then delete in the database using deleteOne() else reject(). Also add resolve() to the catch block. Within the if block:

await postsCollection.deleteOne({_id: new ObjectID(postIdToDelete)})
resolve()

Confirm the “flash.ejs” template is included at the top of the “profile.ejs” template. Test deleting a post that is yours. Also test the permission logic by trying to delete a post that is not your own.