Last week, we set out to build a Reddit clone from the database's perspective. We ended up with a RedditAPI
with createPost
, getAllPosts
, createUser
and other data-related methods.
In this next part of the project, we are going to take the functionality that we already built, and make a website out of it! To do this, we are going to use many of the technologies we have already learned about, but we will also be adding a bit more on top.
At the end of this project, we should have at a bare minimum a Reddit clone with the following functionality:
- Non logged in users will be able to view a list of posts ordered by one of hot, top, newest.
- Non logged in users will be able to signup and/or login to the site.
- Logged in users will be able to cast either an up or down vote for each post they are looking at.
- Logged in users will be able to add new posts to the site. A post is a URL and a title, and it belongs to a subreddit and a user.
Clone this project to your Cloud9 workspace, under your home directory. Then, copy the reddit.js
file from the reddit-nodejs-api
directory to the current project.
Before starting to write any code, let's figure out the different pages we will need:
-
Homepage:
The homepage lists 25 posts, as provided by the
getAllPosts
method of theRedditAPI
. -
Signup page:
The signup page will be a simple page with an HTML
<form>
. The form will have username and password fields, as well as a "signup" submit button. More on that later. -
Login page:
The login page will be a simple page with an HTML
<form>
. The form will have username and password fields, as well as a "login" submit button. More on that later. -
Create post page
The create post page will also be a simple page with an HTML
<form>
. The form will have title and URL text fields, a dropdown for choosing the subreddit, and a "create" submit button. -
Subreddits pages
They will work similarly to the homepage, except that the posts will be filtered for one subreddit only.
In your Express code, you will have one
app.get('/r/:subreddit')
and use thereq.params.subreddit
to make a request to your Reddit API. Then, you can use the same rendering code as the homepage to print the posts for that subreddit :)
Rendering HTML by writing code like this:
var output = "<ul>";
contents.forEach(function(item) {
output = output + "<li><a href='" + item.url + "'>" + item.title + "</a>";
});
output = output + "</ul>";
can quickly get out of hand, especially as you have a more complex page.
As seen in class, Express offers the Pug templating engine, which was previously known as Jade. The syntax will look a bit weird at first, but it translates to HTML and allows you to avoid the whole string concatenation headache
Here's a full example:
In the server code:
// At the top (already added for you)
app.set('view engine', 'pug');
app.get('/', function(request, response) {
// This is only an example!
redditAPI.getAllPosts().then(function(posts) {
// Response.render will call the Pug template engine with the `post-list.pug` file.
response.render('post-list', {
posts: posts,
});
});
});
In your post-list.pug
:
h1 Welcome to Reddit Clone!!!
ul.post-list
each post in posts
li.post
h2
a(href=post.url)= post.title
p Submitted by #{post.username}
p Score: #{post.score} Upvotes: #{post.upVotes} Downvotes: #{post.downVotes}
Which will return this HTML:
<h1>Welcome to Reddit Clone!!!</h1>
<ul class="post-list">
<li class="post">
<h2>
<a href="http://blabla">This is the first post</a>
</h2>
<p>Submitted by 514FOREVER</p>
<p>Score: 100 Upvotes: 500 Downvotes: 400</p>
</li>
<li class="post">
<h2>
<a href="http://blabla123">This is the second post</a>
</h2>
<p>Submitted by Other_User</p>
<p>Score: 123 Upvotes: 523 Downvotes: 400</p>
</li>
</ul>
In general, we want to return a full page of HTML , not only a snippet. For this, Pug gives us template inheritance. We can create a layout which has the general structure of our page, and have a placeholder for the content.
Here would be an example of layout.pug
file. By using so-called "blocks", we can create placeholders for content.
doctype html
html
head
meta(charset="utf-8")
block title
title The Default Title
body
block content
Then, in our post-list.pug
file, we can extend this layout, and provide a block for each block in the layout:
extends layout.pug
block title
title Welcome to Reddit Clone!!!
block content
h1 Welcome to Reddit Clone!!!
ul.post-list
each post in posts
li.post
h2
a(href=post.url)= post.title
p Submitted by #{post.username}
p Score: #{post.score} Upvotes: #{post.upVotes} Downvotes: #{post.downVotes}
Check out the full documentation for Pug to learn more. Here are some of the important sections:
We will have at least three form submissions to handle: login, signup and create post. Each form should be sent using a POST request to the server. Sending a POST request is an indication that we want to create new data on the target system. Therefore it's very important to not submit such data more than once.
Browsers are good at helping with this: notice that if you submit a form through POST, and try to refresh the resulting web page, the browser will warn that you are about to re-submit a form.
We can avoid this though: a good practice is to always redirect the user after a POST (read this Wikipedia article). If you redirect the user to another page using an HTTP 303 See Other
status code then the browser will load that other page with a GET request and all will be well. The user will not even be able to re-submit the same form data!
For the signup form, we could redirect the user to the login page after they're done. For the login form we can redirect users to the homepage. For the create post form, we could also redirect the user to the homepage. If we had a page per post (with comments for example) then we could also redirect the user to the new post page that they created, like they do over at Reddit.
To redirect users, we can use the Express res.redirect
function.
One of the most sensitive aspects of a website is its security. As we've seen in the past years, even some of the largest sites out there are not immune to hacks. As web developers, it's our job to make sure that a site we build is as secure as possible. This will reduce the chances of compromising our customers' personal data and/or putting us out of business.
One field where this is super important is the user signup and login process. During this process, we are asking the user to provide us with a username and password combination that will be used to identify them. Some of our users will re-use that same password for all their accounts. It would be pretty bad if we stored their password in plain text and our database got compromised.
WARNING: The signup/login method described below is not meant to be 100% secure. It's only meant to give you a bit of insight into how complicated this process can be. In fact, many companies make a business out of providing user management functionalities to other businesses. This lets us concentrate on what makes our product different!
For this reason and many others, we will never store our customers' passwords in plain text in our database. When creating a new user, we will instead store a hashed version of their password.
Hashing is a function that takes a string as an input (like a password for example), and uses an irreversible but consistent transformation of that string to generate its output.
Let’s imagine your password was a number. My hashing function could be:
- take the password (number) and divide it by 100 using integer division
- return the remainder of the division as the hash.
So if your password is 1234
I would store it as 34
(1234 % 100 = 34
). While I cannot recover your password, if you give me an input password I can check that it has the same hash. This would work well as long as there are no collisions. If you tell me your password is 134
or 2234
, they will all hash to 34
and you will be able to login!
For these reasons, n the real world we will be using hashing functions that have little chance for collision. An example of such a hashing function is SHA-1. If I pass the string Hunter2
through the SHA-1 function I will get back a8a00adebf1411b8baf07bdc688ce3889e8f7cb2
. Simply changing the string to hunter2
(note the lack of capital H) then the SHA-1 will be f3bbbd66a63d4bf1747940578ec3d0103530e21d
. While this is not a demonstration of any security feature, you can see that even a slight change in the input string will result in a completely different hash.
We can compute the number of possible combinations of SHA-1 outputs: if we see the output as a set of 40 hexadecimal digits, then the number of combinations would be 16^40
which is a huuuuge number. However big that number may be, the number of possible password strings is infinite! This means that our hashing function will definitely have collisions, meaning that two passwords will hash to the same string. However up until 2016, there has still not been a practical way to create a collision with this hashing function.
Moreover, we will not simply be storing the password has a hash of the input string. That would still be too easy to crack! For example, the Hunter2
password above is a "popular string": it comes from an old internet joke that you may lookup in your own time. There exists a few websites out there that can "reverse" SHA-1 outputs of popular strings. There's no magic involved: they simply have a large database of SHA-1 input/output combinations.
For all these reasons, we will be using a library called bcrypt to take care of our password hashing. When signing up a new user, we will use bcrypt's hash
function to generate a hashed version of the password.
If you look at the Reddit API we built last week, the createUser
function uses bcrypt
to hash a password. In this case the output will look like this:
$2a$10$26OFMwEvtb4.6nWuYOPg6OJYlyl.uh7barqO5wfKrI9J9wJOZFIei
Eventually we'll have to build a login function. In there, we will receive again a username and a password. This time, we will go to our database to find a user with the same username.
If we don't find a user, then we can respond with "username or password incorrect". This will prevent attackers from knowing whether or not the username exists.
If we do find a user, we can use bcrypt's compare
function to compare the found user's hashed password with the password we received from the login process. It would go a bit like this:
- User loads the
/login
page - Browser displays an HTML form with a username field and a password field
- User fills in both fields and clicks on the LOGIN button
- Browser constructs a query string like
username=john&password=Hunter2
- Browser looks at the
action
andmethod
of the<form>
and sends an HTTP request -- usually aPOST
- Server receives the request and parses it into appropriate object under
request.body
- In our web server, we use the
request.body.username
andrequest.body.password
to call theRedditAPI.checkUserLogin
function - The
RedditAPI.checkUserLogin
function: - Takes a username and password
- Does an SQL query to our database
SELECT * FROM users WHERE username = ?
- After retrieving the SQL result, uses the
bcrypt.compare
function to check if the hashed password matches the input password - If the passwords match, the promise resolves with the full user object minus the hashed password
- If the passwords don't match, the promise throws an error "username or password invalid".
- If we have a password match from the
RedditAPI.checkUserLogin
function: - Create a session by calling
RedditAPI.createSession
, passing it the full user object from previous step. - When the promise resolves, retrieve the sessionId
- Use [
response.cookie
] to set a cookie with the nameSESSION
and the value being thesessionId
from the promise - Use [
response.redirect
] to send the user back to the homepage, but now they'll be logged in.
Cool. We now have set a random, "unguessable" SESSION
token in the user's browser cookies. Next time they do an HTTP request to our server, their browser will send the SESSION token. We can then check in our database if it exists and what userId it's linked to.
A middleware was already created for you that does this. Its code is in lib/check-login-token.js
and will be explained in detail later. Here's the gist of what it does:
- Check the request cookies for a cookie called
SESSION
- If it does not exist, move on
- If the cookie exists, do a database query to see if the session token belongs to a user:
- if it doesn't exist, move on
- if it does exist, then we update the
request
to contain the logged in user's info.
This project already contains many files and directories. In this section we go over what each one does in detail.
This file sits at the root of your project, and is the main file that will execute your web server. It contains a bit more logic than is normally desirable, but we tried to split up some of that logic in other modules where possible. Here's what this file does:
- Load
express
and create a new web server - Load all the Express middlewares we will use and adds them to the pipeline with
app.use
- Loads the RedditAPI, created a database connection and sets up the API
- Delegates anything under
/static
to the static middleware - Delegates anything under
/auth
to a custom Express Router - Sets up a few other request handlers for the homepage, subreddits, creating posts and voting. These functionalities could be further split up in their own modules
- Finally, makes the web server listen on
process.env.PORT
, which is set to8080
on Cloud9.
Contains the CREATE TABLE
SQL statements for the whole database
Contains a data dump to give your Reddit Clone some initial data
This directory contains static files like CSS and logo images. The files are served by Express' static middleware.
This file contains a custom Express Router. Here, we export a function that receives the RedditAPI
instance, and returns the Router. An Express Router is like a tiny sub-application that takes care of its own paths. Notice that the .get
s and .post
s in there will say /login
and /signup
, but in the index.js
file, we mount the router under /auth
. This means that the final URLs will be /auth/login
and /auth/signup
.
This file contains the RedditAPI
class. It's a correct version of the last project you worked on.
This file exports a function that takes a RedditAPI
instance and returns an Express middleware that will check if the current request has a SESSION
token. If it does, the middleware will try to find the user that corresponds to that session, and add the user object under the request.loggedInUser
property. This same user object will also be added under request.locals.loggedInUser
. request.locals
is an object and its properties will be made available to the HTML template engine.
This file exports a simple middleware that will force a user to be logged in. If a request comes from a non-logged in user, the middleware will not call next()
and instead return a 401 Unauthorized
response. This middleware is not meant to be used on every request. Look in index.js
for how this middleware is used.
This file contains the main layout for the website. It outputs the main <html>
structure, and uses Pug's inheritance system. The part that says block content
will be replaced with the content output by any template that extends layout.pug
. Check homepage.pug
for an example of extending the layout.
This file creates a Pug mixin which is the equivalent of a function in that it can take arguments and be re-used. This mixin is used in views/homepage.pug
and will be useful for you to build other views.
This file extends the layout.pug
file and defines a block called "content". This block in turn uses the postList
mixin to output a list of posts after outputting a generic title.
This file can be used anytime you have access to an error object. It is useful to output the error in a nice way to the browser.
This section details the work that you have to do on this project, as well as suggestions to improve it further.
The first thing you'll have to do is complete the signup and login features of the site.
-
Signup In
controllers/auth.js
, make theapp.get('/signup')
render an HTML signup form. To do this, add a fileviews/signup-form.pug
and make it output the following form:<h1>Signup</h1> <form action="/auth/signup" method="POST"> <p>Username: <input type="text" name="username"></p> <p>Password: <input type="password" name="password"></p> <p><button type="submit">Signup!</button></p> </form>
Make sure that your pug
file extends the layout.pug
so that your signup form gets output with all the surrounding HTML.
Then, implement the code of authController.post('/signup')
. This code will receive the form data under request.body
. There, you have to call myReddit.createUser
and pass it the necessary info. Once the createUser
promise is resolved, use response.redirect
to send the user to /auth/login
.
-
Login In
controllers/auth.js
, make theapp.get('/login')
render an HTML login form. To do this, add a fileviews/login-form.pug
and make it output the following form:<h1>Login</h1> <form action="/auth/login" method="POST"> <p>Username: <input type="text" name="username"></p> <p>Password: <input type="password" name="password"></p> <p><button type="submit">Login!</button></p> </form>
This form is super similar to the signup form, except for the action
. Make sure that your pug
file extends the layout.pug
so that your signup form gets output with all the surrounding HTML.
Then, implement the code of authController.post('/login')
. To do this, you'll need to complete some code in lib/reddit.js
:
- In
lib/reddit.js
, complete the code of thecheckUserLogin
function following the instructions in comments. - In
lib/reddit.js
, complete the code of thecreateUserSession
function following the instructions in comments.
When these two functions are done, start working on the POST
handler for /login
:
-
Use the
checkUserLogin
function, passing it therequest.body
username and password -
If the login check is unsuccessful, send a
401 Unauthorized
status to the browser, else move to step 3 -
Since login is successful, use the
checkUserLogin
response to find the user's ID, and pass it to thecreateUserSession
function -
When that function is done, you'll get back a random session ID. Use Express
response.cookie
to set a cookie with nameSESSION
and value being that session id -
Use
response.redirect
to send the user back to the home page. -
Checking if user is actually logged in The code in
lib/check-login-token.js
gets executed on every request to check if the request contains aSESSION
cookie. Even though the code was written for you, it relies on a function calledgetUserFromSession
in the RedditAPI. Implement that function by doing aJOIN
query between the sessions and users tables, and return the full user object for the given session ID. Once you do that, refresh the home page and you should see a message at the top saying "Welcome YOUR USER".
That's it! You have fully implemented the signup, login, and cookie consumption process. Your Pug templates have access to a variable called loggedInUser
. It will be false
if there is no user, and will contain a user object otherwise. Check the code of views/layout.pug
to see an example of using that variable.
In index.js
, there is an app.get('/r/:subreddit')
that is currently not returning anything. We'd like to make it output a list of posts just like on the front page, but only for the requested subreddit. To do this, you'll have to make a few changes:
- First, we have to go from subreddit name to subreddit ID. Create a
RedditAPI
function calledgetSubredditByName(name)
. This should make a query to the database, and return a subreddit object that matches the given name. If no subreddit was found, the promise should resolve withnull
. - Call
getSubredditByName
from theapp.get
handler, and pass it therequest.params.subreddit
. If you get back null, send a 404 response. Otherwise move to the next step. - Modify the
RedditAPI.getAllPosts
function to accept asubredditId
optional parameter. If this parameter is passed, theSELECT
query should be changed to add aWHERE p.subredditId = ?
, and return only posts for that subreddit. - Call
getAllPosts
from yourapp.get
handler, passing it the subreddit ID from step 2. Then, render the resulting list of posts using thepost-list.pug
template. Since this is a subreddit, the rendering should include the name of the subreddit as well as its description before the post list. You can use Pug conditionals inpost-list.pug
to make this happen.
In index.js
, there is an app.get('/sort/:method') that is currently not returning anything. We'd like to make it output a list of posts just like on the front page, but sorted by something other than
createdAt DESC`.
To do this, you'll first have to make some changes to the RedditAPI.getAllPosts
function. Make it accept an optional sortingMethod
parameter that can be hot
or top
. If the sorting method is set to top
, then the posts should be ordered by voteScore DESC
. If the sorting method is set to hot
, then the posts should be ordered by voteScore / NOW() - p.createdAt DESC
. This formula will take the score, but divide it by the number of seconds the post has been online. This will make newer posts appear higher if they have the same number of votes as an older post.
In the app.get
handler, check if request.params.method
is either hot
or top
. If not, then return a 404 error. If it is, call the getAllPosts
and then render a list of posts just like on the home page.
Finally, make sure that you have <a href="/sort/hot">
and <a href="/sort/top">
links somewhere on the page, so that the user can change sorting methods by clicking.
In index.js
, there is a GET
and POST
handlers for /createPost
. Let's implement them! Make the GET
handler return an HTML form like the following one by creating a create-post-form.pug
file:
<h1>Share a new link!</h1>
<form method="POST" action="/createPost">
<p>
Subreddit:
<select name="subredditId">
<option value="1">FirstSubreddit</option>
... one option tag for each subreddit
</select>
</p>
<p>URL: <input type="text" name="url"></p>
<p>Title: <input type="text" name="title"></p>
</form>
The <select>
element is a dropdown list. The names between <option>
tags will be shown to the user, but the value="XX"
part will be sent to the server. To output this <select>
box, you'll have to use the RedditAPI.getAllSubreddits
function before rendering the template.
Then, implement the POST
handler: notice that the code uses the onlyLoggedIn
middleware to make sure that this will only be called when there is a logged in user. Here you will call RedditAPI.createPost
and you'll need to pass it the information from the form. You also need to provide a userId
, but that will be coming from request.user
instead of request.body
.
Once the post is created successfully, the only thing you can do is redirect the user. Use the newly created posts' ID to redirect them to /post/<postId>
which you will implement next.
In index.js
there is a GET
handler for /post/:postId
. This should use the RedditAPI.getSinglePost
function to get the post by its ID. If the post does not exist, return a 404. If it does, then create a new Pug template that will output that post as well as its comments. To do this, you'll not only need to call getSinglePost
, but also getCommentsForPost
. Make sure to use Promise.all
to do this, since the two requests are independent.
How will the user cast a vote for a post eventually? Their browser will have to make a POST request to/vote
. Next to each post, when outputing the <li>
for that post, you have to add the following form:
<form action="/vote" method="post">
<input type="hidden" name="postId" value="THE ID OF THE CURRENT POST">
<button type="submit" name="vote" value="1">upvote this</button>
<button type="submit" name="vote" value="-1">downvote this</button>
</form>
This code looks weird because we have two submit buttons. The way it works is that the submit buttons are each tied to a -1
and 1
value for the vote
property. Clicking on one of the buttons will submit its value
as the vote
value in the form.
Then, you have to implement the POST
handler for /vote
in index.js
. Make it call RedditAPI.createVote
and pass the necessary information. The postId
will come from the hidden
input. Hidden inputs are useful because they allow us to pass information to the server without any user input.
This concludes the minimal part of the project. The following section gives you some suggestions on features you can add to make your Reddit Clone more unique.
The following are suggestions for features you can add to your Reddit Clone. If you have an idea for a feature that's not listed here, don't hesitate to ask us what we think about it. Each feature is rated from one ⭐ up to three ⭐ depending on its difficulty level. It's up to you and your group to decide which features you'd like to implement.
In all post listings (post-list.pug
), if the post URL looks like it leads to an image -- ends in .gif
, .png
or .jpg
-- then include a 40x40 image thumbnail along with the rest of the information for that post.
<img>
tags with images from other domains and sometimes those domains will block you from doing so. If we wanted to implement this feature in a real application, we would have to produce the thumbnails on our own server.
When listing posts, the user who created the post is linked as /u/:username
. In index.js
add a GET
handler for a new /u/:username
endpoint. This endpoint should serve list of all the posts made by that user.
Create a new RedditAPI
method getAllPostsForUsername
to retrieve all the posts made by a given username
. Re-use the post-list
mixin to render the post list for that user.
Make post titles and comments text emojifiable so that if a word like :rocket:
or :metal:
appears in the text, they will be replaced with 🚀 or 🤘.
Look at the node-emoji
package on NPM and try to incorporate it in your project. The best place to do this is in the RedditAPI
functions concerned by this change: getAllPosts
, getSinglePost
and getCommentsForPost
.
Markdown is a text format that can be automatically converted to HTML but is easier to write and read for humans. Learn more about Markdown. It's a great format for writing technical documentation because it allows for fixed width
text as well as code blocks with syntax highlighting. For example, this README.md
is written with Markdown.
For this feature, you can use the marked
package to transform a string of markdown to HTML. When outputting that string of HTML with Pug, you'll have a surprise. Pug will do the safe thing and escape your HTML, effectively replacing characters like <
with their HTML entity counterparts like <
. Read the Pug interpolation documentation and find out how to tell Pug to not escape this bit of HTML.
Earlier we created a Single Post View for the endpoint /post/:postId
. Extend the pug template of this page to add a comment form which will POST its data to a new endpoint /createComment
. Then, add a POST
handler for /createComment
and make use of the RedditAPI.createComment
function to add a comment. When the comment is created, redirect the user back to the post page from the POST
handler.
Currently comments are being displayed by createdAt
date. We will build this feature the same way as the post votes feature. The steps are roughly:
- Create a
commentVotes
table, similar to thevotes
table for posts. - Add a
createCommentVote
method to theRedditAPI
- Add an
app.post('/commentVote')
handler similar to the post vote handler - Add an HTML
<form>
to each comment output, similar to the post vote form - Test everything
Next week we will look at CSS together. Working on this feature will allow you to get a head start, and make your Reddit Clone more unique.
Add basic style to the main elements of your Reddit clone. Style the header, the main navigation, the main content, the sidebar and the footer. Try to make it look nice. If you need help to pick a colorway, you can try Adobe Color CC for inspiration.
In addition to sharing links, give users the ability to share their thoughts through self posts. Here is an example of self post on Reddit. To accomplish this feature, you'll have to implement the following steps:
- Add column
postText TEXT
to theposts
table, and set appropriate values for the already existing posts. - Update the
createPost
function so that it accepts apostText
in thepost
object. A post should have one and only one ofurl
orpostText
. - Update the create a post form to accept self posts. You'll have to add a
<textarea name="postText"></textarea>
element where the user will be able to type their self post. - Update the
app.post('/createPost')
handler to accept and pass through the value of the text area. - Optional Next week we will look at how to make a web page dynamic with browser-side JavaScript. If you want to take a head start, make the form dynamic by allowing the user to toggle between a self-post form and a link sharing form.
Add a feature that will designate a moderator for a subreddit. A moderator is someone who will have admin power that will allow him or her to delete the posts in this subreddit. In order to achieve this, you will need to:
- Add a new column called
moderatorId
in yoursubreddits
table. When creating a new subreddit, insert theuserId
of the creator as themoderatorId
. - When the moderator of a subreddit visits the subreddit, he should have a new button on every post that allows him or her to delete a post. ATTENTION: You will have to make sure only the moderator of this subreddit can delete a post.
- Clicking the button should submit a form that makes a
POST
to/deletePost
with theid
of the post. Make sure to only allow the moderator to delete a post! - Bonus: You can also add this delete button on the single post page.
This feature depends on the "subreddit moderator" feature.
Allow the moderator of a subreddit to change the appearance of it. In order to do this, you will need to add a new page to allow the style customization at /r/:subreddit/admin
. On this page, the moderator should be presented with a list of styles they can modify. Here is an example of what it could look like:
To do this, you will need to:
- Create a new table in your database called
subredditStyle
. This table should have the following columns:id
,subredditId
,styleName
,styleValue
. There should be a unique key constraint on the (subredditId
,styleValue
) pair. - When saving the custom style page, it should insert any modified entry in your
subredditStyle
table. Every style element has its own row. Use theON DUPLICATE KEY UPDATE
in yourINSERT
query, like for thevotes
table. - When on a subreddit page, grab all the custom styles and inject them into the page using a
<style></style>
tag in the<head>
of the output.
This feature only makes sense if users provide an email address. To implement the feature you'll need to cover the following points:
- Add an
email VARCHAR(100)
column to theusers
table and make sure there is a unique constraint on that column. Emails should be optional. - At signup, allow the user to provide an email address and make it optional. Modify the signup form and the
POST
handler as well as thecreateUser
function accordingly. - Add a
/auth/recover
page through thecontrollers/auth.js
Router with a form that asks for the email address. Make itPOST
to/auth/createResetToken
. - Add a
POST
handler for/auth/createResetToken
in thecontrollers/auth.js
Router. It will receive arequest.body.email
. If the email address is found in the database, we will let the user reset their password by using a random token similar to the session id token.- Create a new table
passwordResetTokens
with columnsuserId INT
andtoken VARCHAR(100)
, making sure that the token is unique. - Add a
createPasswordResetToken(userId)
method to theRedditAPI
. In this method, generate a random string and insert it along with the user ID in thepasswordResetTokens
table. - Send an email to the user with a link to your website at
/auth/resetPassword?token=XXXX
replacingXXXX
with the random string that is in the database- Signup for an account at Mailgun, a web service for sending emails
- Install the NPM package
mailgun-js
and read its documentation. - Go to https://app.mailgun.com/app/domains and click on the sandbox domain to find your domain name and API key
- Use the
mailgun-js
module to send an email to your user with the link to reset their password/auth/resetPassword?token=XXXX
- Create a new table
- Add a
GET
handler for/auth/resetPassword
that will output a<form>
with a "new password" field. When the form should also have a hidden input that will be whatever is in thetoken
param of the query string. The form willPOST
to/resetPassword
with thetoken
and thenewPassword
. - Add a
resetPassword(token, newPassword)
method to theRedditAPI
. In it, find if thetoken
corresponds to a real token and whichuserId
it corresponds to. Then, reset their password by hashing thenewPassword
with bcrypt and making anUPDATE
to the database. Make sure to delete the password reset token from the database so that it cannot be reused! - Add a
POST
handler for/auth/resetPassword
that will callRedditAPI.resetPassword
and pass it the necessary info. Once the password is updated, redirect the user to/auth/login
so they can re-login with their new password. - Test everything!