Data is the new oil - Clive Humbly
Everywhere around us is data waiting to be collected and utilized. In recent years, we've seen the rise of applications and services that exist to quantify concepts that were previously hard to capture. FitBit, Apple Health, and Woop are all $1 billion dollar services to offer tracking statistics about how we live our lives. The LifeTracker app you'll be building will do exactly that - track your life by quantifying your activity.
By the end of this project you will be able to...
- Develop a full-fledged authentication system using PostgreSQL and
bcrypt
- Provide users with an Express API they can interact with to store user-related activity
- Construct multiple models that implement the core business logic associated with tracking users' lives
- Write SQL queries that aggregate user statistics and provide summary overviews about their activity
- Design a React frontend that interacts with the API using an API service class
- Build multiple pages and forms that communicate with the server using HTTP requests
- Employ
useEffect
anduseState
hooks to manage application state on the frontend - Store user-authenticated JWT tokens in the browser's local storage for persisted authentication
- The Landing Page: Display a large hero image and a brief blurb on what this application is about. Note: This is the only page that unauthenticated users should be able to view.
- Registration Page: A form that allows the user to sign up with their email, password, username, first name, and last name.
- Login Page: A form that allows users to login with email and password.
- When a user first authenticates, they should be redirected to an authenticated view (i.e., the detailed activity page). When they sign out, all frontend data should be reset.
- The Nav Bar: Implement customized views for users who are logged in vs not logged in.
- If the user is logged in, it should display a Sign Out button.
- If no user is logged in, it should display Login and Register buttons.
- Display a logo on the far left side, and contain links to the individual detailed activity pages.
- Users should have the ability to track at least one type of activity (i.e., nutrition, exercise, sleep, etc.). Each activity should be tracked on separate pages.
- Detailed Activity Page: Display and enter activities.
- Display a feed of all previously tracked activities.
- A form to enter relevant information (i.e., if tracking nutrition, the user can enter calories, timestamp, image, category, etc.).
- Each activity tracked is given a unique ID for easy lookup.
- Deploy your website with Render. Check out our Render Deployment Guide for detailed instructions.
Implement any of the following features to improve the application:
- Users have access to an overview Activity page that shows one summary statistic about each of the three types of activity tracked (i.e., total number of minutes exercised, average calories consumed, max hours of sleep in one night, etc.). These summary statistics should be created using the
AVG
,SUM
,COUNT
,MIN
,MAX
, functions in SQL queries and served from a dedicated API endpoint. Note: Summary statistics should not be calculated on the frontend. - Each model (i.e
nutrition
,exercise
, andsleep
) should also implement afetchById
method that queries the database for a record by its id and only serves it to users who own that resource.- You should also create a new dynamic route on the frontend that displays detail about a single record. For instance,
nutrition/detail/:id
should show a page with all the information about a single nutrition item.
- You should also create a new dynamic route on the frontend that displays detail about a single record. For instance,
- Provide a dropdown that allows users to filter activity based on a certain attribute of any activity item. Example: filter exercise or nutrition by category, or filter sleep by the week/month it was recorded.
- Calculate aggregate statistics based on time periods - such as daily, weekly, monthly aggregates.
- Create a page that shows all other users that use the LifeTracker application and allow users to follow each other. You'll want to create a new table to store this data.
- Implement
security
middleware on the API that allows only authenticated users to access resources and allows users to only access resources about themselves.
- Build the
App
component to:- Be wrapped by an element with the class name of
app
- Contain the routes for the app
- Render the
Navbar
component on every route - Render a
BrowserRouter
component that contains aRoutes
component with the following routes:-
/
- Render theLanding
component -
/login
- Render theLoginPage
component -
/register
- Render theRegistrationPage
component -
/activity
- Render theActivityPage
component only if the user is logged in, otherwise it renders theAccessForbidden
component -
/nutrition/*
- Render theNutritionPage
component only if the user is logged in, otherwise it renders theAccessForbidden
component -
*
- Anything else renders theNotFound
component
-
- Be wrapped by an element with the class name of
- Create a
constants.js
file at the root of the project that exports the following variables:-
PRODUCTION_API_BASE_URL
- set to whatever URL the production API is deployed at -
DEVELOPMENT_API_BASE_URL
- set to"http://localhost:3001"
for development -
API_BASE_URL
- Ifprocess.env.NODE_ENV
isproduction
, set this toPRODUCTION_API_BASE_URL
, otherwise set it toDEVELOPMENT_API_BASE_URL
-
- Create a
services
directory at the root of the project. - Inside the
services
directory, create anapiClient.js
file - In the
apiClient.js
file, import theaxios
package and theAPI_BASE_URL
constant from theconstants.js
file. - Define a new class in that file called
ApiClient
.- Give it a constructor function that accepts a single parameter -
remoteHostUrl
. The constructor should attach theremoteHostUrl
parameter to a new instance withthis.remoteHostUrl = remoteHostUrl
. It should also setthis.token = null
. - Export default a new instance of the
ApiClient
class. - Add an additional method called
setToken
that accepts a single parameter -token
and attaches it to the instance. - Create a utility method called
request
that usesaxios
to issue HTTP requests - Add a
login
method that uses therequest
method to send an HTTP request to theauth/login
endpoint - Add a
signup
method that uses therequest
method to send an HTTP request to theauth/register
endpoint - Add a
fetchUserFromToken
method that uses therequest
method to send an HTTP request to theauth/me
endpoint - Add as many other methods as needed when making API requests.
- Give it a constructor function that accepts a single parameter -
Update the App
component to manage authentication state:
- Create a state variable called
appState
with a function calledsetAppState
to update that state.- Initialize
appState
with an object containing properties likeuser
,isAuthenticated
,nutrition
,sleep
, andexercise
.
- Initialize
- Implement a
useEffect
hook to fetch the user data.- Define an asynchronous function named
fetchUser
to fetch the user data.- Inside the
fetchUser
function, retrieve a token fromlocalStorage
usinglocalStorage.getItem("lifetracker_token")
- Call the
setToken
function from theapiClient.js
file. - Make an API call to fetch user data using the
fetchUser
function from theapiClient.js
file and extract thedata
from the response. - If
data
is not null and not undefined, update the component's state using thesetAppState
function. Pass a callback tosetAppState
that takes the previous state and returns a new state object. - In the callback, use the spread operator (
...
) to copy the previous state's properties to the new state object. - Assign the following properties from the
data
object to the new state object:-
user
-
token
-
- Assign at least one of the following properties from the
data
object to the new state object:-
nutrition
-
exercise
-
sleep
-
- Call the
setAppState
with a new state object to update the component's state. - Outside the
fetchUser
function, callfetchUser
to trigger the initial data fetch when the component mounts. - The effect should be triggered whenever the value of
appState.isAuthenticated
changes.
- Inside the
- Define an asynchronous function named
- Build the
Loading
component to:- Render JSX that is wrapped by an element with the class name of
loading
- Render an element with the class name of
loading-message
that contains the text"Loading"
- Render JSX that is wrapped by an element with the class name of
- Build the
Navbar
component to:- Render JSX that is wrapped by a
nav
element with the class name ofnavbar
- Render the app's logo as an element with the class name of
logo
.- Inside that element should be a
Link
component fromreact-router-dom
that navigates the user to the/
route when clicked. - Inside that
Link
component should be the application's logo (text or image).
- Inside that element should be a
- Render the
NavLinks.jsx
component with links to each of the resources and the/activity
route.
- Render JSX that is wrapped by a
- Build the
NavLinks
component to:- Render JSX that is wrapped by an element with the class name of
nav-links
- Render a
Link
element fromreact-router-dom
for:- The
/activity
route with a label ofActivity
. - The
/nutrition
route with a label ofNutrition
. - A route for any other resource page
- The
- If a valid user is logged in, it should render an element with the class name of
logout-button
that calls thelogoutUser
function when clicked.- The
logoutUser
function should remove thelifetracker_token
from local storage and refresh the page so that all user data is reset.
- The
- If no valid user is logged in:
- Render a
Link
element that redirects to the/login
route with the labelLogin
- Render a
Link
element that redirects to the/register
route with the labelSign Up
- Render a
- Render JSX that is wrapped by an element with the class name of
- Build the
LoginForm
component to:- Render JSX that is wrapped by an element with the class name of
login-form
- Render an input element for the following fields:
-
email
-
password
-
- Each
input
element in the form should have a class name ofform-input
and should have the following props set:-
name
- thename
of theinput
field being rendered (email
,password
) -
type
- the type of theinput
element (text
,email
,number
, etc.) -
value
- the current value of theinput
element -
onChange
- theonChange
handler function
-
- Validate the
email
field. If the user has entered text into theemail
field and it doesn't contain an@
symbol, then an error message should be displayed in an element with the class name oferror
indicating that the entry is not a valid email. - Gracefully handle errors:
- If the user has attempted to login and gotten a
401
error, then an error message should be displayed in an element with the class name oferror
indicating that theemail
andpassword
combination is incorrect. - If the user has attempted to login and gotten a
400
or422
error, then an error message should be displayed in an element with the class name oferror
indicating what went wrong.
- If the user has attempted to login and gotten a
- There should be a
button
element with the class name ofsubmit-login
:- It should contain the text
"Login"
- When clicked, it should call the
loginUser
function
- It should contain the text
- Render JSX that is wrapped by an element with the class name of
- Build the
LoginPage
component to:- Render JSX that is wrapped by an element with the class name of
login-page
- Using either a custom hook, context, or manually set state, check to see if a user is already logged in
- If the user is already logged in, redirect them to the
/activity
page. - If no user is authenticated, render the
LoginForm
component and pass it any props it needs.
- If the user is already logged in, redirect them to the
- Render JSX that is wrapped by an element with the class name of
- Build the
RegistrationForm
component to:- Render JSX that is wrapped by an element with the class name of
registration-form
- Should render an input element for the following fields:
-
email
-
username
-
firstName
-
lastName
-
password
-
passwordConfirm
-
- Each
input
element in the form should have a class name ofform-input
and should have the following props set:-
name
- thename
of theinput
field being rendered (email
,username
,firstName
,lastName
,password
,passwordConfirm
) -
type
- the type of theinput
element (text
,email
,number
, etc.) -
value
- the current value of theinput
element -
onChange
- theonChange
handler function
-
- Validate the
email
field: If the user has entered text into theemail
field and it doesn't contain an@
symbol, then an error message should be displayed in an element with the class name oferror
indicating that the entry is not a valid email. - Validate the
password
andpasswordConfirm
fields: If the user has entered text into thepassword
andpasswordConfirm
fields and they don't match, then a message should be displayed in an element with theclassName
oferror
with a message that contains the text:passwords don't match
- Gracefully handle errors:
- If the user has attempted to login and gotten a
401
error, then theerrors
object should contain aform
property that contains a message indicating that theemail
andpassword
combination is incorrect. - If the user has attempted to login and gotten a
400
or422
error, then theerrors
object should contain aform
property that contains a message indicating what went wrong.
- If the user has attempted to login and gotten a
- There should be a
button
element with theclassName
ofsubmit-registration
:- It should contain the text
"Create Account"
- When clicked, it should call the
signupUser
function
- It should contain the text
- Render JSX that is wrapped by an element with the class name of
- Build the
RegistrationPage
component to:- Render JSX that is wrapped by an element with the class name of
registration-page
- Using either a custom hook, context, or manually handled state, check to see if a user is already logged in
- If the user is already logged in, it should redirect them to the
/activity
page - If no user is authenticated, it should render the
RegistrationForm
component and pass it any props it needs
- If the user is already logged in, it should redirect them to the
- Render JSX that is wrapped by an element with the class name of
- Build the
LandingPage
component to:- Render JSX that is wrapped by an element with the class name of
landing-page
- Render an element with the class name of
hero
- Inside it, display a large hero image using an
img
element with the class name ofhero-img
- Render a brief blurb on what this application is about inside an element with the class name of
cta
- Inside it, display a large hero image using an
- Allow unauthenticated access
- Render JSX that is wrapped by an element with the class name of
- Build the
ActivityPage
component to:- Render JSX that is wrapped by an element with the class name of
activity-page
- Take the
appState
andsetAppState
as props and extract all the necessary data from it. - If the
isProcessing
flag istrue
, it should render theLoading
component. - If the
isProcessing
flag isfalse
, it should render theActivityFeed
component and pass it the appropriate props.
- Render JSX that is wrapped by an element with the class name of
- Build the
ActivityFeed
component to:- Render JSX that is wrapped by an element with the class name of
activity-feed
- Accept at least the following props:
-
totalCaloriesPerDay
- an array of items containing summary data about the total calories consumed per day -
avgCaloriesPerCategory
- an array of items containing summary data about the average calories consumed per category - Any other props as needed
-
- Inside an element with the class name of
per-category
, it should:- Render the text:
"Average Calories Per Category
inside anh4
element - Take the first
6
or less items in theavgCaloriesPerCategory
array and render aSummaryStat
component for each item.- Pass the calories rounded down to one decimal place as the
stat
prop - Pass the string of
calories
as thelabel
prop - Pass the
category
as thesubstat
prop
- Pass the calories rounded down to one decimal place as the
- Render the text:
- Inside an element with the class name of
per-day
, it should:- Render the text:
"Total Calories Per Day
inside anh4
element - For each item in the
totalCaloriesPerDay
array, render aSummaryStat
component.- Pass the calories rounded down to the nearest whole number as the
stat
prop - Pass the string of
calories
as thelabel
prop - Pass the
date
in the formatdd/mm/yyyy
- example:07/02/2022
- as thesubstat
prop
- Pass the calories rounded down to the nearest whole number as the
- Render the text:
- Render JSX that is wrapped by an element with the class name of
- Build the
SummaryStat
component to:- Render JSX that is wrapped by an element with the class name of
summary-stat
- Accept at least the following props:
-
stat
- the primary statistic to display -
label
- the unit label assigned to the statistic -
substat
- a secondary statistic related to the primary statistic
-
- Render the
stat
prop inside an element with the class name ofprimary-statistic
- Render the
label
prop inside an element with the class name ofstat-label
- Render the
substat
prop inside an element with the class name ofsecondary-statistic
- Render JSX that is wrapped by an element with the class name of
- Build the
NutritionPage
component to:- Render JSX that is wrapped by an element with the class name of
nutrition-page
- Take the
appState
andsetAppState
as props and extract all the necessary data from it. - Render a nested
Routes
component fromreact-router-dom
.- There should be multiple
Route
components:- The
/nutrition
route should render theNutritionOverview
component - The
/nutrition/create
route should render theNutritionNew
component - The
/nutrition/id/:nutritionId
should render theNutritionDetail
component - Any other route should render the
NotFound
component
- The
- There should be multiple
- Render JSX that is wrapped by an element with the class name of
- Build the
NutritionOverview
component to:- Render JSX that is wrapped by an element with the class name of
nutrition-overview
- Take the
appState
andsetAppState
as props and extract all the necessary data from it.- If the
error
state variable has a valid string in it, it should render theerror
message inside an element with the class name oferror
- If
isLoading
istrue
, it should render theLoading
component - If
isLoading
isfalse
, it should render theNutritionFeed
component and pass it the appropriate props
- If the
- Near the top of the component, it should render a
Link
component that directs to the/nutrition/create
route and contains the text:"Record Nutrition"
- Render JSX that is wrapped by an element with the class name of
- Build the
NutritionFeed
component to:- Render JSX that is wrapped by an element with the class name of
nutrition-feed
- Receive at least the following props:
-
nutritions
- an array ofnutrition
items
-
- If the
nutritions
array has no items in it, render an empty message that saysNothing here yet
inside an element with the class name ofempty-message
- If the
nutritions
array does have items in it:- For each item in the
nutritions
array, it should render aNutritionCard
component
- For each item in the
- Render JSX that is wrapped by an element with the class name of
- Build the
NutritionNew
component to:- Render JSX that is wrapped by an element with the class name of
nutrition-new
- Render the
NutritionForm
component and pass it the appropriate props
- Render JSX that is wrapped by an element with the class name of
- Build the
NutritionForm
component to:- Render JSX that is wrapped by an element with the class name of
nutrition-form
- Render an input element for the following fields:
-
name
- name of the nutrition item (defaults to an empty string) -
calories
- number of calories in the nutrition item (defaults to 1) -
imageUrl
- theurl
of an image to show for this nutrition item (defaults to an empty string) -
category
- the category that this nutrition item belongs to, like fruit, meat, soda, snack, nuts, etc. (defaults to an empty string)
-
- Each
input
element in the form should have a class name ofform-input
and should have the following props set:-
name
- thename
of theinput
field being rendered (name
,calories
,imageUrl
,category
) -
type
- the type of theinput
element (text
,email
,number
, etc.) -
value
- the current value of theinput
element -
onChange
- theonChange
handler function
-
- Gracefully handle errors:
- If any of the required fields are left blank, there should be an error message inside of an element with the class name of
error
indicating which fields are required. - If the user has attempted to create a nutrition entry and gotten a
400
or422
error, then that message should be displayed inside an element with the class name oferror
- If any of the required fields are left blank, there should be an error message inside of an element with the class name of
- There should be a
button
element with the class name ofsubmit-nutrition
:- Contain the text
"Save"
- When clicked, it should call a function that creates a new nutrition entry
- Contain the text
- After the form has been successfully submitted:
- Ensure that the new nutrition entry is stored in the
nutrition
context'snutritions
array and is displayed in theNutritionFeed
component - Fetch the
activity
data again so that new summary stats will be calculated
- Ensure that the new nutrition entry is stored in the
- Render JSX that is wrapped by an element with the class name of
- Build the
NutritionDetail.jsx
component to:- Render JSX that is wrapped by an element with the class name of
nutrition-detail
- Leverage the
useParams
hook fromreact-router-dom
to extract thenutritionId
param from the URL - When the component is mounted to the screen...
- It should make a
GET
request to the/nutrition/:nutritionId
endpoint with theaxios.get
method. - The
:nutritionId
part of the request should be replaced with thenutritionId
pulled from the URL. - When the initial request is loading, it should render an
h1
element with the class name ofloading
and contain the text"Loading..."
- Store the
nutrition
received by the request in state and then render aNutritionCard
component for that nutrition. - If no
nutrition
is found with thatid
, it should render theNotFound
component
- It should make a
- Render JSX that is wrapped by an element with the class name of
- Build the
NutritionCard
component to:-
Render JSX that is wrapped by an element with the class name of
nutrition-card
-
Accept at least the following props:
-
nutrition
- should be a nutrition entry object containing the following attributes:-
imageUrl
- (not required) -
name
- (required) -
calories
- (required) -
category
- (required) -
createdAt
- (required)
-
-
-
Render the
name
of thenutrition
entry inside an element with the class name ofnutrition-name
-
If the
nutrition
entry has a validimageUrl
attribute, render animg
element with the class name ofnutrition-image
and use thatimageUrl
as itssrc
-
Render the
calories
attribute of thenutrition
entry inside an element with the class name ofnutrition-calories
-
Render the
category
attribute of thenutrition
entry inside an element with the class name ofnutrition-category
-
Render the
createdAt
attribute of thenutrition
entry in the formatdd/mm/yyyy
- example:07/02/2022
- inside an element with the class name ofnutrition-date
. -
DO THE SAME FOR ANY OTHER RESOURCE THAT IS IN THE APPLICATION
- Choose whatever resources you want!
-
- Build the
ProtectedRoute
component to:- Take the
appState
andsetAppState
as props and extract all the necessary data from it. - Accept a component as the
element
prop and render that component. - If the application isn't currently loading and no user is found, render the
LoginPage
component instead of rendering the route the user intended to go to. This way, we can ensure that only authenticated users can access the provided component. - Any unauthenticated user should be shown the
LoginPage
component with a message indicating that they need to authenticate first - Update the
LoginPage
component so that it accepts amessage
prop that is displayed in the login form - if it exists. - Make sure to protect the entire
ActivityPage
component route and theNutritionPage
component route (along with any other private resource pages). Don't protect theLandingPage
component or theLoginPage
andRegistrationPage
components, as they should be public.
- Take the
Here are the pieces of functionality that should be built out for the backend:
- Project setup
- First things first, bootstrap the Express application with some essential files and starter code
- Create a
.gitignore
file, anapp.js
file, anapp.test.js
file, and aserver.js
file - Make sure
node_modules
are added to the.gitignore
file. - Add dependencies for
express@next
,morgan
,cors
, andnodemon
- Install new dependencies for
bcrypt
,jsonwebtoken
,colors
,dotenv
,pg
- Commit all work to
git
- Add a
.env
file to the root of the repo and include the following environment variables-
PORT
(default to3001
) -
SECRET_KEY
(set to a long random string) -
BCRYPT_WORK_FACTOR
(set to13
) -
DATABASE_USER
-
DATABASE_PASS
-
DATABASE_HOST
-
DATABASE_PORT
-
DATABASE_NAME
- (set tolifetracker
) -
DATABASE_TEST_NAME
- (set tolifetracker_test
)
-
- Add a
config.test.js
file- Write tests that check to make sure that:
-
process.env.NODE_ENV
is set totest
when the test suite is run - There is an
IS_TESTING
variable that is exported, which should only be true ifprocess.env.NODE_ENV
is set totest
-
- Write tests to ensure that certain environment variables are exported from the
config.js
file and can be imported:-
PORT
-
SECRET_KEY
-
BCRYPT_WORK_FACTOR
-
IS_TESTING
-
- Write tests to ensure that a
getDatabaseUri
function is exported from theconfig.js
file- The
getDatabaseUri
function should:- Check to see if a valid
process.env.DATABASE_URL
environment variable exists, and return that if it does. - When
IS_TESTING
istrue
, thegetDatabaseUri
function should use the test database - Otherwise, it should combine the proper database environment variables into a database connection string if no
process.env.DATABASE_URL
environment variable exists
- Check to see if a valid
- The
- Write tests that check to make sure that:
- Add a
config.js
file- Use the
dotenv
package to parse the environment variables from the.env
file. - Export each of the environment variables from the
config.js
file until the tests pass - Write a
getDatabaseUri
function so that all the tests pass
- Use the
- Commit all work to
git
- The project should now be ready to go!
- PostgreSQL database
- Time bring in a PostgreSQL database client as the application's persistence layer
- Make sure the PostgreSQL server is running
- Create two files at the root of the project:
-
lifetracker-schema.sql
- This script should:
- Create a
users
table with the following columns:-
id
-
username
-
password
-
first_name
-
last_name
-
email
-
created_at
-
updated_at
-
- Create a
nutrition
table with the following columns:-
id
-
name
-
category
-
calories
-
image_url
-
user_id
-
created_at
-
- Any other tables that the application might depend on
- Create a
- This script should:
-
lifetracker.sql
- This script should:
- 1. Let the user know that they're about to delete the
lifetracker
database and prompt them to confirm that is what they want. - 2. Drop the
lifetracker
database and then create a newlifetracker
database, before connecting to thelifetracker
database. - 3. It should then run the
lifetracker-schema.sql
file. - Follow the exact same steps for
1
,2
, and3
, but with thelifetracker_test
database.
- 1. Let the user know that they're about to delete the
- This script should:
-
- Setup the database by running
psql -f lifetracker.sql
- Create a new file at the root of the project called
db.js
. In that file:- Import the
getDatabaseUri
function from theconfig.js
file. - Initialize a new PostgreSQL client with the
pg
package and connect to PostgreSQL using any necessary config variables. - Connect to PostgreSQL and log a message to the terminal on success or failure.
- Export the connected database client
- Import the
- Commit all work to
git
- A database client is now ready to be used!
- Server
- Build out a bare-bones Express server with a health check route and an adequate middleware pipeline.
- Create a
utils
directory- In the
utils
directory, create anerrors.js
file. - Create error classes inside the file that will be used throughout the app.
- In the
- In the
app.test.js
file, write tests that:- Ensure that the Express application responds to
GET
requests to the/
route with a JSON object of{ "ping": "pong" }
- Check that middleware like
morgan
andcors
exist, along with the JSONbody-parser
middleware fromexpress
- Include an
afterAll
hook that callsawait db.end()
so that any open database connections close when all the tests are finished.
- Ensure that the Express application responds to
- Add code to the
app.js
andserver.js
file to get a simple server running along with responding toGET
requests to the/
route - Create error classes inside the
utils/errors.js
file. - Add
404
and generic error handler middleware to theapp.js
file. - In the
server.js
file:- Import the Express app and the
config.js
file - Have the
app
listen on the port specified byconfig.PORT
.
- Import the Express app and the
- Commit all work to
git
- Test out the fancy new Express server by starting it up in a new terminal window!
- Common Test Configuration
- It would probably be helpful to create some common test functions that can be used throughout the application's testing suite.
- Create a new directory called
tests
- Now, touch a new file at
tests/common.js
- In that file:
- Import the
db
client - Create and export four functions:
-
commonBeforeAll
- Actions that should happen before any tests in a particular file run.
- This should include things like executing queries that delete all items from any tables in the test database that might have been added during testing
-
commonBeforeEach
- Actions that should happen before any single test in a particular file runs.
- This should include things like starting a database transaction
-
commonAfterEach
- Actions that should happen after any single test in a particular file runs.
- This should include things like rolling back any database actions before they're committed
-
commonAfterAll
- Actions that should occur after all tests in a particular file run.
- This should include things like ending any open database client connections
-
- Import the
- In that file:
- Commit all work to
git
- Authentication
- Go ahead and build out a full-fledged authentication flow using PostgreSQL,
bcrypt
, and JSON Web Tokens. For it all to work, we'll need aUser
model, asecurity
middleware, sometokens
utility functions, and the appropriateauth
routes. - Add new directories for
models
,routes
, andmiddleware
- The User model
- In the
models
directory, create two new files:models/user.js
andmodels/user.test.js
- The
User
model should have at least the following static methods:-
login
-
register
-
fetchUserByEmail
-
- The
- In the
models/user.test.js
file:- Test the
login
method. Write test cases for:- User can login successfully with proper credentials
- Unknown email throws
UnauthorizedError
- Invalid credentials throws
UnauthorizedError
- Test the
register
method. Write test cases for:- User can successfully register with proper credentials
- Registering with duplicate email throws
BadRequestError
- Registering with duplicate username throws
BadRequestError
- Registering with invalid email throws
BadRequestError
- Test the
fetchUserByEmail
method:. Write test cases for:- A valid email returns a user from the database
- Invalid emails are handled correctly
- It will probably be important to use the
beforeAll
,afterAll
,beforeEach
, andafterEach
hooks to add and delete users from the database before running the tests
- Test the
- In the
models/user.js
file:- Import the
bcrypt
package, thedb
client, and the appconfig
. - Implement the features outlined in the tests until they're all passing.
- Import the
- In the
- Commit all work to
git
- The tokens utility functions
- In the
utils
directory, create two new files:utils/tokens.js
andutils/tokens.test.js
- At the bare minimum, two functions will be needed:
- One that accepts a JSON payload as an argument and converts it into a JWT
- One that accepts a JWT as an argument, validates it, and returns the JSON payload encoded within - if it's valid
- At the bare minimum, two functions will be needed:
- In the
utils/tokens.test.js
file:- Write test cases for:
- Can create valid JWT tokens for user payloads
- Can extract a payload from a valid JWT with the correct secret
- No payload gets returned when invalid tokens are parsed
- Write test cases for:
- In the
utils/tokens.js
file:- Implement the features outlined in the tests until they're all passing
- In the
- Commit all work to
git
- The security middleware
- In the
middleware
directory, create two new files:middleware/security.js
andmiddleware/security.test.js
- One middleware will be responsible for extracting a user from a valid JWT in the request:
- Checking the
Authentication
header of each request for the existence of a JWT. - If one exists, it should extract the token, validate it, extract the encoded JSON payload, and attach it to the response's
locals
property
- Checking the
- One middleware will be responsible for ensuring that an authenticated user exists:
- Checking that a valid user exists on the response's
locals
property - If one does, the middleware should simply call next
- If no valid user exists, it should throw an
UnauthorizedError
- Checking that a valid user exists on the response's
- One middleware will be responsible for extracting a user from a valid JWT in the request:
- In the
middleware/security.test.js
file:- Test the
Authentication
header parsing middleware- Write test cases for:
- Extracts user from valid JWT in
Authentication
header - No user is stored when no valid JWT exists in the
Authentication
header - No user is stored when an invalid JWT is in the
Authentication
header
- Extracts user from valid JWT in
- Write test cases for:
- Test the middleware that ensures an authenticated user exists
- Write test cases for:
- Doesn't throw an error when a valid user is present
- Throws an
UnauthorizedError
when no valid user is present
- Write test cases for:
- Test the
- In the
middleware/security.js
file:- Implement the features outlined in the tests until they're all passing
- In the
app.js
file, add theAuthentication
header parsing middleware to the Express app's middleware pipeline
- In the
- Commit all work to
git
- The /auth routes
- In the
routes
directory, create two new files:routes/auth.js
androutes/auth.test.js
- A new Express router should be created. It should handle:
- A
GET
request to the/me
endpoint- It should send a JSON response back to the client with the user info like so:
{ "user": { "email": "[email protected]", ... } }
- It should send a JSON response back to the client with the user info like so:
- A
POST
request to the/login
endpoint- It should accept a request body with
email
andpassword
keys - It should send a JSON response back to the client with a new JWT and user info like so:
{ "token": "e2c2...", "user": { "email": "[email protected]", ... } }
- It should accept a request body with
- A
POST
request to the/register
endpoint- It should accept a request body with
email
,username
,firstName
,lastName
, andpassword
keys - It should send a JSON response back to the client with a
201
status code, along with a new JWT and user info like so:{ "token": "e2c2...", "user": { "email": "[email protected]", ... } }
- It should accept a request body with
- A
- It should be mounted at the
/auth
endpoint in theapp.js
file
- A new Express router should be created. It should handle:
- In the
routes/auth.test.js
file:- Test the
POST /auth/login
endpoint- Write test cases for:
- Allows user to register with valid credentials and responds with JSON containing a valid token and user in the "token" and "user" fields
- Throws
UnauthorizedError
when user doesn't exist in database - Throws
UnauthorizedError
when user provides wrong password - Throws
BadRequestError
when user doesn't provide password - Throws
BadRequestError
when user doesn't provide email
- Write test cases for:
- Test the
POST /auth/register
endpoint- Write test cases for:
- Allows user to login successfully with valid credentials and responds with a
201
status code, along with JSON containing a valid token and user in the "token" and "user" fields - Throws
BadRequestError
when user doesn't provide one of the required fields - Throws
BadRequestError
when user provides email that already exists - Throws
BadRequestError
when user provides username that already exists
- Allows user to login successfully with valid credentials and responds with a
- Write test cases for:
- Test the
POST /auth/me
endpoint- Write test cases for:
- Provides the user with their user info when a valid JWT is present in the
Authentication
header of the request - Throws an
UnauthorizedError
when no valid user is logged in
- Provides the user with their user info when a valid JWT is present in the
- Write test cases for:
- Test the
- In the
routes/auth.js
file:- Create a new Express router
- Implement the features outlined in the tests until they're all passing
- In the
app.js
file: - Mount the router at the
/auth
endpoint
- In the
- Commit all work to
git
- There should now be a full-fledged authentication system in place!
- Go ahead and build out a full-fledged authentication flow using PostgreSQL,
- Resources and Permissions
- Next, implement the functionality to allow users to save instances of things they've drank/eaten, so that they can track their own nutrition data! Also make sure users can only access the data that they themselves have created. No other user should be able to see any data owned by another user!
- The Nutrition model
- In the
models
directory, create two new files:models/nutrition.js
andmodels/nutrition.test.js
- The
Nutrition
model should have at least the following static methods:-
createNutrition
- Should insert a new nutrition instance into the database when values are supplied for all of the required fields:
"name"
,"category"
,"calories"
, and"image_url"
. Thequantity
field should default to1
. - The new nutrition instance should have its
user_id
field set to theid
of the authenticated user - Should throw a
BadRequestError
(400
status code) orUnprocessableEntityError
(422
status code) when any of those values are not supplied.
- Should insert a new nutrition instance into the database when values are supplied for all of the required fields:
-
fetchNutritionById
- When supplied with a valid
id
, fetches the a nutrition instance from the database that matches thatid
. - If no nutrition instance matches that
id
, throws aNotFoundError
(404
status code)
- When supplied with a valid
-
listNutritionForUser
- Should list all nutrition instances in the database that are owned by a particular user
-
- The
- In the
models/nutrition.test.js
file:- Test the
createNutrition
method. Write test cases for:- A user can create a nutrition instance when they supply the appropriate values
- The appropriate error is thrown when any of the provided errors are invalid
- The user that creates the nutrition instance now owns that nutrition instance
- Test the
fetchNutritionById
method. Write test cases for:- Fetches the nutrition instance that matches the supplied
id
- Throws a
NotFoundError
when no nutrition instances matches the suppliedid
- Fetches the nutrition instance that matches the supplied
- Test the
listNutritionForUser
method. Write test cases for:- Fetches all nutrition instances belonging to a particular user
- Doesn't include any nutrition instances belonging to a different user
- Returns an empty array if no nutrition instances are found in the database that belong to that user
- Test the
- In the
models/nutrition.js
file:- Implement the features outlined in the tests until they're all passing
- Commit all work to
git
- In the
- The permissions middleware
- In the
middleware
directory, create two new files:middleware/permissions.js
andmiddleware/permissions.test.js
- Though more functions will need to be added here as the number of resources grows, for now only 1 function needs to be created.
- The
authedUserOwnsNutrition
middleware function should:- Probably be called after the
requireAuthenticatedUser
security middleware in any route's middleware pipeline - Extract a parameter from the request endpoint that corresponds to the
id
of the nutrition instance - Query the database for that nutrition instance
- Check that it is owned by the authenticated user
- If it doesn't, it should throw a
ForbiddenError
(403
status code) - If the nutrition instance does belong to the authenticated user, it should attach it to the
locals
property of theresponse
as itsnutrition
property so that it doesn't need to be fetched again by the database (this isn't required, but is probably a good idea).
- If it doesn't, it should throw a
- Probably be called after the
- In the
middleware/permissions.test.js
file:- Test the
authedUserOwnsNutrition
middleware function- Write test cases for:
- Throws error if authenticated user doesn't own nutrition
- Throws
NotFoundError
ifid
of nutrition isn't found in database - Doesn't throw error if authenticated user is nutrition owner
- (OPTIONAL) Attaches the
nutrition
to thelocals
property of the response when the user owns the nutrition instance
- Write test cases for:
- Test the
- In the
middleware/permissions.js
file:- Implement the features outlined in the tests until they're all passing
- Commit all work to
git
- In the
- The /nutrition routes
- In the
routes
directory, create two new files:routes/nutrition.js
androutes/nutrition.test.js
- A new Express router should be created that will be mounted at the
/nutrition
endpoint. It should handle:-
GET
requests to the/
endpoint- It should send a JSON response back to the client with all of the user-owned nutrition instances in an array like so:
{ "nutritions": [...] }
- It should send a JSON response back to the client with all of the user-owned nutrition instances in an array like so:
-
POST
requests to the/
endpoint- It should accept a request body with one
nutrition
key containing an object with all the attributes of thenutrition
entry - It should send a JSON response back to the client with a
201
status code, and the newly created nutrition instance like so:{ "nutrition": { ... } }
- It should accept a request body with one
-
GET
requests to the/:nutritionId
endpoint- It should send a JSON response back to the client with the nutrition instance that matches the
:nutritionId
parameter like so:{ "nutrition": { ... } }
- It should send a JSON response back to the client with the nutrition instance that matches the
-
- A new Express router should be created that will be mounted at the
- In the
routes/nutrition.test.js
file:- Test the
GET /nutrition
endpoint- Write test cases for:
- Returns an array of all
nutrition
entries belonging to the user - Other user's entries aren't included in the
nutritions
array - Throws
UnauthorizedError
if no valid user is logged in
- Returns an array of all
- Write test cases for:
- Test the
POST /nutrition
endpoint- Write test cases for:
- Authenticated users can create a new
nutrition
entry when providing values for all the required fields - The new
nutrition
entry belongs to the user that created it - Throws a
BadRequestError
if any of the required fields are missing - Throws an
UnauthorizedError
if no valid user is logged in
- Authenticated users can create a new
- Write test cases for:
- Test the
GET /nutrition/:nutritionId
endpoint- Write test cases for:
- Nutrition owner can fetch a
nutrition
entry when providing a validid
- Throws a
403 ForbiddenError
if a user tries to access anutrition
instance that does not belong to them - Throws a
404 NotFoundError
when thenutritionId
does not match any nutrition in the database - Throws a
401 UnauthorizedError
if no valid user is logged in
- Nutrition owner can fetch a
- Write test cases for:
- Test the
- In the
routes/nutrition.js
file:- Implement the features outlined in the tests until they're all passing
- In the
- Commit all work to
git
- Additional Resources
- Create model and routes files for 1-2 additional resources that your app will track (sleep, exercise, steps, floors climbed, meditation, mood, heartrate, music practice, etc.)
- Commit all work to
git
- Summary Statistics
- One of the last features of the API will be a model that calculates summary statistic on the different resources that users are tracking. This includes statistics like average calories per day, or max calories per category. To do that, we'll create a new
Activity
model and anactivity
route that will be used to populate the frontend. - The Activity model
- In the
models
directory, create two new files:models/Activity.js
andmodels/Activity.test.js
- The
Activity
model should have at least the following static methods:-
calculateDailyCaloriesSummaryStats
- Should execute a SQL query that calculates at least the total calories consumed per day (aliased as
totalCaloriesPerDay
), along with the day (aliased asdate
). - The query should return a row for each day containing the total calories consumed per day, and the average caloric content per nutrition entry.
- For instance, here's a set of 7 simplified nutrition item entries (actual data will look different):
-
{ id: 1, user_id: 1, calories: 100, category: "candy", created_at: "12-22-2022" }
-
{ id: 2, user_id: 1, calories: 200, category: "drink", created_at: "12-22-2022" }
-
{ id: 3, user_id: 1, calories: 200, category: "fruit", created_at: "12-23-2022" }
-
{ id: 4, user_id: 1, calories: 400, category: "dairy", created_at: "12-23-2022" }
-
{ id: 5, user_id: 1, calories: 400, category: "drink", created_at: "12-23-2022" }
-
{ id: 6, user_id: 1, calories: 700, category: "fruit", created_at: "12-24-2022" }
-
{ id: 7, user_id: 1, calories: 100, category: "fruit", created_at: "12-24-2022" }
-
- The summary stats returned from the query should look like this:
-
{ date: "12-22-2022", totalCaloriesPerDay: 300 }
-
{ date: "12-23-2022", totalCaloriesPerDay: 1000 }
-
{ date: "12-24-2022", totalCaloriesPerDay: 800 }
-
- For instance, here's a set of 7 simplified nutrition item entries (actual data will look different):
- Should execute a SQL query that calculates at least the total calories consumed per day (aliased as
-
calculatePerCategoryCaloriesSummaryStats
- Should execute a SQL query that calculates at least the average calories consumed per category (aliased as
avgCaloriesPerCategory
and rounded down to one decimal place), along with the category (aliased ascategory
). - The query should return a row for each day containing the total calories consumed per day, and the average caloric content per nutrition entry.
- For instance, here's a set of 7 simplified nutrition item entries (actual data will look different):
-
{ id: 1, user_id: 1, calories: 100, category: "candy", created_at: "12-22-2022" }
-
{ id: 2, user_id: 1, calories: 200, category: "drink", created_at: "12-22-2022" }
-
{ id: 3, user_id: 1, calories: 200, category: "fruit", created_at: "12-23-2022" }
-
{ id: 4, user_id: 1, calories: 400, category: "dairy", created_at: "12-23-2022" }
-
{ id: 5, user_id: 1, calories: 400, category: "drink", created_at: "12-23-2022" }
-
{ id: 6, user_id: 1, calories: 700, category: "fruit", created_at: "12-24-2022" }
-
{ id: 7, user_id: 1, calories: 100, category: "fruit", created_at: "12-24-2022" }
-
- The summary stats returned from the query should look like this:
-
{ category: "candy", avgCaloriesPerCategory: 100.0 }
-
{ category: "drink", avgCaloriesPerCategory: 300.0 }
-
{ category: "fruit", avgCaloriesPerCategory: 266.6 }
-
{ category: "dairy", avgCaloriesPerCategory: 400.0 }
-
- For instance, here's a set of 7 simplified nutrition item entries (actual data will look different):
- Should execute a SQL query that calculates at least the average calories consumed per category (aliased as
-
- The
- In the
models/Activity.test.js
file:- Test the
calculateDailyCaloriesSummaryStats
method. Write test cases for:- The
calculateDailyCaloriesSummaryStats
method correctly calculates summary statistics per day - Only uses the
nutrition
entries belonging to the user when calculating summary statistics - Returns an empty array when the user has no
nutrition
entries
- The
- Test the
calculatePerCategoryCaloriesSummaryStats
method. Write test cases for:- The
calculatePerCategoryCaloriesSummaryStats
method correctly calculates average calories per category summary statistics - Only uses the
nutrition
entries belonging to the user when calculating summary statistics - Returns an empty array when the user has no
nutrition
entries
- The
- Test the
- In the
models/Activity.js
file:- Implement the features outlined in the tests until they're all passing
- Commit all work to
git
- In the
- The /activity routes
- In the
routes
directory, create two new files:routes/activity.js
androutes/activity.test.js
- A new Express router should be created that will be mounted at the
/activity
endpoint. It should handle:-
GET
requests to the/
endpoint- It should send a JSON response back to the client with summary stats for each resource in the following format:
-
{ "nutrition": { "calories": { "perDay": [...], "perCategory": [...] }, ...anyOtherStats }, ...statsForOtherResources }
-
- It should send a JSON response back to the client with summary stats for each resource in the following format:
-
- A new Express router should be created that will be mounted at the
- In the
routes/activity.test.js
file:- Test the
GET /activity
endpoint- Write test cases for:
- Provides a JSON response containing arrays of summary stats for resources, attributes, and metrics
- Correctly calculates
totalCaloriesPerDay
for a user'snutrition
entries - Correctly calculates
avgCaloriesPerCategory
for a user'snutrition
entries - Only returns summary stats based on entries that the currently authenticated user owns
- Throws an
UnauthenticatedError
if no valid user is logged in
- Write test cases for:
- Test the
- In the
routes/activity.js
file:- Implement the features outlined in the tests until they're all passing
- In the
- Commit all work to
git
- One of the last features of the API will be a model that calculates summary statistic on the different resources that users are tracking. This includes statistics like average calories per day, or max calories per category. To do that, we'll create a new