For this lab, interns will be creating a clone of AirBnB - a modern booking platform for local bed and breakfasts. This competitor application will be geared towards celebrities only, in the hopes of attracting the highest revenue customers to the platform. Celebrities should be able to sign up, list any of their personal residences, and let other celebrities book trips to stay in their houses.
The focus of this lab will be on constructing a complete authentication suite that offers the ability for users to confirm the email addresses they signed up with and reset their password if they need to.
This lab will help SITE interns master:
- Integrating an external service like Twilio Sendgrid into a Node/Express application
- Sending password reset emails to users who forgot their password
- Using cryptography to generate and store password-reset tokens in the database
- How to confirm users have valid email accounts
- Managing query parameters in client-side and server-side urls
For this application, you'll need to complete the following stages:
- Stage 1 - Project Setup
- Stage 2 - Initialize database with SQL files
- Run the sql scripts to create the necessary database and tables, as well as seed the database with starter data
- Explore the backend repo code and examine the created tables with
psql
. - Create a
.env
file from the.env-template
file. Modify any appropriate environment variables there. Open up theconfig.js
file and modify any variables there as well.
- Stage 3 - Integrate Twilio Sendgrid
- We'll be using this service to send emails to users.
- Head to the Twilio SendGrid page
- Click the Try For Free button and enter in email and password.
- Handle the Captcha and agree to their terms. Then can go ahead and fill out some personal information.
- Next, click on the email api section. We'll be using their Web API.
- Select the Node.js client, give the new api key a name, and create it.
- Keep this page open, as we'll need it for later
- Update env vars and config
- Store that api key in the Express API's
.env
file under the nameSENDGRID_API_KEY
. Add anEMAIL_SERVICE_STATUS
env var as well. - Add the api key env var to the
config.js
file. Also include anEMAIL_SERVICE_ACTIVE
config variable that isfalse
whenIS_TESTING
istrue
, otherwise istrue
whenEMAIL_SERVICE_STATUS
is equal to"active"
. - Export both of them
- Store that api key in the Express API's
- Install the
nodemailer
andnodemailer-sendgrid
packages withnpm
. - Create a new directory called
services
- Add three files to it -
index.js
,email.js
,email.test.js
- Create the scaffolding code for an
EmailService
class and export it. - Write a test that ensures that an instance of the
EmailService
class has theisActive
andtransport
properties attached to it. - Modify the
EmailService
constructor:- It should accept a config object with the
SENDGRID_API_KEY
andEMAIL_SERVICE_ACTIVE
config vars. - It should create a
nodemailer
transport usingnodemailer-sendgrid
and theSENDGRID_API_KEY
, then attach it to the instance - It should attach the
EMAIL_SERVICE_ACTIVE
variable to theisActive
property on the instance. - Get the tests to pass
- It should accept a config object with the
- Export a properly configured instance of that service in the
index.js
file.
- Add three files to it -
- Stage 4 - Testing the EmailService
- Add a tests to the email service and a describe block
Is inactive when testing
- make sure that whenever we're testing that an instance of the email service has theisActive
property set tofalse
Test sendEmail
- Add two tests to this describe block
Returns 202 status code when all goes well
- ensures that when provided the proper email params, thesendEmail
method returns an object with thestatus
property set to202
, theemail
sent, and anerror
property set tonull
.Returns 400 status code when email is missing to field
- ensures that when theto
field is missing, thesendEmail
method returns an object with thestatus
property set to400
, theemail
sent, and anerror
property set a valid description of the error.
- Create a
sendEmail
method on theEmailService
- It should accept an object containing the
from
,to
,subject
andhtml
properties, which are then passed to thetransport
'ssendMail
method to send the the email. - If the
EmailService
is not active, it should NOT send the email and instead log that a fake email is being sent. It should return the right response depending on the email parameters supplied.
- It should accept an object containing the
- Make sure all the tests are passing.
- Add a tests to the email service and a describe block
- Stage 5 - Storing Password Reset Tokens with the User Model
- Create the necessary database modifications
- Modify the
kavholm.sql
file- Add the
pw_reset_token
column as text - Add the
pw_reset_token_exp
as a timestamp
- Add the
- Re-initialize the db by running
psql -f kavholm.sql
- Modify the
- Create password reset token utils in
utils/tokens.js
- Import the
crypto
module - Export a
generateCryptoToken
function that takes innumBytes
and uses thecrypto
module to return a Buffer array of pseudorandom bytes converted to a hexadecimal string - Export a
generatePasswordResetToken
function that returns an object containing a token resulting from calling thegenerateCryptoToken
method with20
num bytes and an expiration date set to an hour from the moment it was created. Convert the expiration date to anISO
formatted string.
- Import the
- Add tests to the
user.test.js
file- Import the
tokens
utils - Create a new describe block -
Test password reset
- Add a
User can store password reset token in the db
test- It should create a user, generate a password reset token for them, and then call the
savePasswordResetToken
method on theUser
class to save the users token in the db according to their email. - It should then fetch the user from the db and make sure the token and exp date are saved in the db
- It should create a user, generate a password reset token for them, and then call the
- Add a
- Import the
- Implement the
savePasswordResetToken
method on theUser
class and get all the tests passing.
- Create the necessary database modifications
- Stage 6 - Password Reset Method and Routes
- Add more tests to
user.test.js
User can reset their password when supplying the correct token
- It should follow the same protocols as the previous test, but then call the
resetPassword
method on theUser
class afterwards with the reset token and a new password. - It should then check that their password has been changed, and the token and expiration date in the database have been set to null.
- It should also ensure that the user can login with the new password
- It should follow the same protocols as the previous test, but then call the
Error is thrown when bad token is supplied
- It should follow the same protocols as the previous test
- Aftewards, it should call the
resetPassword
method on theUser
class afterwards with the wrong token and a new password inside atry...catch
block and expect it to fail with a 400 error.
- Implement the
resetPassword
method on the user class- It should hash the new password
- It should look up the user according to the password reset token and update their password to the new hashed password
- It should set the token and expiration date to null
- It should throw a 400 error when no user is found saying the token is expired or invalid
- Add tests for the
/auth/recover
and/auth/password-reset
routes in theroutes/auth.test.js
file- describe
POST /auth/recover
User can request password recovery and receive a success message
- Call the endpoint with any email and expect a 200 response with a message saying the email will be sent out if that account exists.
- Ensure the
EmailService
'ssendEmail
method was called
- describe
POST /password-reset
User with valid token can reset password
- Create a password reset token for a user (consider doing this in testing setup) and save to database
- Call the endpoint with the token and new password
- Make sure the
sendEmail
method on theEmailService
was called - Expect the users password to be changed
User with invalid token gets 400 error
- Call the endpoint with an invalid and new password
- Expect a 400 response
- describe
- Implement both routes and get all tests passing.
- Add more tests to
- Stage 7 - Testing a Mocked
EmailService
- Add empty methods on the
EmailService
forsendPasswordResetConfirmationEmail
andsendPasswordResetEmail
- In the
email.test.js
file- Create a
mockSuccessResponse
andmockFailureResponse
to simulate different responses forsendEmail
- Use
jest
to create amockSendEmail
function- It should pass a callback function to
jest.fn().mockImplementation
- That callback should accept an email object and return the
mockFailureResponse
if the object is missing theto
property. It should return themockSuccessResponse
otherwise.
- It should pass a callback function to
- Use
jest
to mocknodemailer
- Have it be an object with the
createTransport
property set tojest.fn().mockImplementation
- Attach the
mockSendEmail
function to thecreateTransport
mock
- Have it be an object with the
- Update tests that call the
sendEmail
method to first create a new instance of theEmailService
where it IS active and make sure the tests still pass - Add a describe block for
Sending password reset emails
- Test
sendPasswordResetEmail sends an email to provided user
- It should create a new link based on the token passed to it and call the
sendEmail
method to send an email to that user containing the link
- It should create a new link based on the token passed to it and call the
- Test
sendPasswordResetConfirmationEmail sends a confirmation email to provided user
- It should send an email to the user letting them know their password has been changed.
- Test
- Create a
- Update
config.js
and.env
files, and theEmailService
constructor- Create the
EMAIL_FROM_ADDRESS
,APPLICATION_NAME
, andCLIENT_URL
env vars - Add them all to config and export them
- Update the
EmailService
constructor to accept those as part of the config object and attach them to the instance - Modify all instances of
EmailService
to use the proper config
- Create the
- Add methods to
EmailService
- The
constructPasswordResetUrl
method should take in a token and contstruct the proper url using theCLIENT_URL
and token. - The
sendPasswordResetEmail
should send an email using the link from theconstructPasswordResetUrl
method, theEMAIL_FROM_ADDRESS
, and theAPPLICATION_NAME
- The
sendPasswordResetConfirmationEmail
should send an email using theEMAIL_FROM_ADDRESS
andAPPLICATION_NAME
- The
- Get all tests passing
- Add empty methods on the
- Stage 8 - Add frontend components
- Create a
Recover
component that is rendered for the/recover
route in the app.- Make sure there is a "forgot your password?" link in the
Login
component that links here - That component should have an input for
email
and make aPOST
request to/auth/recover
with the email. It should display the message returned by the server when it recieves it.
- Make sure there is a "forgot your password?" link in the
- Create a
PasswordReset
component that is rendered for the/password-reset
route in the app- It should use
react-router-dom
'suseLocation
hook to get information about the current url - It should pass the
search
params to a newURLSearchParams
and get the token in the url - It should have a custom hook called
usePasswordReset
form that accepts the token found in the url- That hook should manage two inputs -
password
andconfirmPassword
- The hook should submit the new password to the
/auth/password-reset
endpoint along with the token
- That hook should manage two inputs -
- It should render the response from the server, and redirect the user to the login page when their password has been successfully reset.
- It should use
- Add two new methods to the
ApiClient
class to handle making HTTP requests to both endpoints
- Create a
- Stage 9 - Add Info to SendGrid and Test App
- Head to SendGrid and verify a sender address by entering in your information
- Follow the protocols
SendGrid
asks for and replace theEMAIL_FROM_ADDRESS
address with the oneSendGrid
verifies - Test that password reset functionality works as expected
- Implement account verification
- Add the
account_verified
andaccount_verification_token
columns to the database - Create a
generateVerificationToken
method in theutils/tokens.js
file- Use it to generate a token for acccount verification
- When users sign up successfully, make sure to use the email service send them a confirmation email with a valid verification token and link
- Define a
verify
endpoint that extracts the users token from the route query parameters and passes it to averifyUserAccount
method on theUser
model - Define a
resendVerification
endpoint that resends a verify account email - Create a
VerifyAccount
component- Create a
useVerifyAccount
custom hook that extracts the token from query parameters and calls the appropriate endpoint when the user clicks a button to confirm
- Create a
- Add the