For this assignment, we will continue our development effort from Assignment 5.
Note: If you require a working version of assignment 5 to continue with this assignment, please email your professor.
For this assignment, we will restrict access to our app to only users who have registered.Β Registered users will also have the benefit of having their favourites and history lists saved, so that they can return to them later and on a different device.Β To achieve this, we will primarily be working with concepts from Weeks 8 and 9, such as incorporating JWT in a Web API, as well as UI considerations for working with a secured web API in Next.js
Sample Solution:
https://web422βa6βfallβ2022.vercel.app
Step 1: Creating a βUserβ API
To enable our βMet Artworkβ App to register / authenticate users and persist their βfavouritesβ / βhistoryβ lists, we will need to create our own βUserβ API and publish it online (Cyclic).Β However, before we begin writing code we must first create a βusersβ Database on MongoDB Atlas to persist the data.Β This can be accomplished by:
Β
- Logging into your account on MongoDB Atlas: https://account.mongodb.com/account/login
- Click on the βBrowse Collectionsβ button in the βDatabase Deploymentsβ screen (next to the ββ¦β button)
- Once MongoDB Atlas is finished βRetrieving list of databases and collectionsβ¦β, you should see a list of your databases with a β+ Create Databaseβ button.
- Choose whatever βDATABASE NAMEβ you like, and add βusersβ as your βCOLLECTION NAMEβ
- Once this is complete, go back to the previous view (βDatabase Deploymentsβ) and click the βConnectβ button, followed by βConnect your applicationβ
- Copy the βconnection stringβ β it should look something like:
Β
mongodb+srv://YourMongoDBUser:<password>@clusterInfo.abc123.mongodb.net/?retryWrites=true&w=majo rity
- Add your Database User password in place of <password> and your βDATABASE NAMEβ (from above) after the text net/ in the above connection string
- Save your updated βconnection stringβ value (weβll need it when we create our User API)
Β
Now that we have a database created on MongoDB Atlas, we can proceed to create our User API using Node / Express.Β To begin, you can use the following code as a starting point:
Β
https://patβcrawfordβsdds.netlify.app/shared/fallβ2022/web422/A6/userβapi.zip
Β
You will notice that the starter code contains everything that we will need to start building our API.Β The only task left is for us to secure the routes and publish the server online (Cyclic).Β You will notice however that (like assignment 1) this solution also makes use of β.envβ.Β Once the code is online, you will once again need to ensure that Cyclic is aware of the values for the variables.
Β
At the moment, .env contains two values: MONGO_URL and JWT_SECRET.Β MONGO_URL is used by the βuserserviceβ module and JWT_SECRET will be used by your code to sign a JWT payload as well as to validate an incoming JWT.
Β
Begin by updating this file, such that the MONGO_URL is your updated βconnection stringβ (from above, without quotes) and your JWT_SECRET is a βlong, unguessable stringβ (also without quotes).Β You may wish to use a Password Generator, ie: https://www.lastpass.com/passwordβgenerator to help generate a secret.
Β
With our environment variables in place, we can now concentrate on securing our routes.Β This will involve correctly setting up βpassportβ to use a βJwtStrategyβ (passportJWT.Strategy) and initializing the passport middleware for use in our server.Β Everything required to accomplish this task is outlined in the JSON Web Tokens (JWT) section of the course notes.Β The primary differences are:
Β
- We will be using the value of βsecretOrKeyβ from env.JWT_SECRET (from our .env file) instead of hardcoding it in our server.js
- The Strategy will not be making use of βfullNameβ (jwt_payload.fullName) or βroleβ (jwt_payload.role), since our User data does not contain these properties
Β
The following is a list of specifications required for our User API once βpassportβ has been set up (HINT most of the code described below is very similar to the code outlined in JSON Web Tokens (JWT), so make sure you have them close by for reference):
Β
Β
POST /api/user/login
Β
This is the only route that contains logic that needs to be updated, specifically:
Β
- If the user is valid (ie, the βcheckUser()β promise resolves successfully) use the returned βuserβ object to generate a βpayloadβ object consisting of two properties: _id and userName that match the value returned in the βuserβ object. This will be the content of the JWT sent back to the client.
Β
Sign the payload using βjwtβ (Hint: Using the βjsonwebtokenβ module) with the secret from process.env.JWT_SECRET (from our .env file).
Β
Once you have your signed token, include it a βtokenβ property within the JSON βmessageβ returned to the client.
Β
Routes Protected Using the passport.authenticate() Middleware
Β
The final step in securing the API is to make sure that the majority of our routes are protected from unauthorized access.Β This involves correctly adding the βpassport.authenticate()β middleware to the following routes:
Β
- GET β/api/user/favouritesβ
- PUT β/api/user/favourites/:idβ
- DELETE β/api/user/favourites/:idβ
- GET β/api/user/historyβ
- PUT β/api/user/history/:idβ
- DELETE β/api/user/history/:idβ
Β
With these changes in place, your User API should now be complete.Β The final step is to push it to Cyclic (recall: Getting Started With Cyclic from WEB322 and our Assignment 1 in this course).
Β
However, there is one small addition that we need to ensure is in place for our User API to work once itβs on Cyclic β Setting up the MONGO_URL and JWT_SECRET Config Variables:
Β
- Login to Cyclic to see your dashboard
- Click on the βwrenchβ (Options and Configs) icon for your newly created application
- Click on the βVariablesβ tab at the top (next to βEnvironmentsβ)
- Enter your JWT_SECRET (from .env) in the corresponding textbox (without quotes)
- Similarly, enter MONGO_URL in the corresponding textbox (without quotes) and hit the βSaveβ button
Β
NOTE: If Cyclic did not automatically detect the βJWT_SECRETβ and βMONGO_URLβ environment variables, you will have to add them using βCreate Newβ
This will ensure that when we refer to either MONGO_URL or JWT_SECRET in our code using process.env, we will end up with the correct value.
Β
This completes the first part of the assignment (setting up your User API).Β Please record the URI, ie: βhttps://some-randomName.cyclic.app/api/userβ somewhere handy, as this will be the βNEXT_PUBLIC_API_URLβ used in our Next.js application.
Β
Β
Step 2: Updating our Next.js App (utility / βlibβ functions)
Β
Now that we have our User API in place, we can make some key changes in our Next.js App to ensure that only registered / logged in users can view the data, as well as to finally persist their favourites / history lists in our mongoDB βusersβ collection.
Β
HINT: Once again, most of the code described below is very similar to the code outlined in the Authentication (Logging In) section of the notes, so make sure you have them close by for reference.
Β
Β
Adding .env
Β
Since we just completed setting up our User API on Cyclic, why donβt we start by adding it to a new .env file as: NEXT_PUBLIC_API_URL, ie:
Β
NEXT_PUBLIC_API_URL=βhttps://some-randomName.cyclic.app/api/userβ
Β
Β
Creating an βAuthenticateβ library
Β
We will be requiring users to be authenticated to view / interact with our data, so our next step should be to write the logic to enable this feature in a separate library, ie:Β βmy-app/lib/authenticate.jsβ:
Β
Once you have created the βauthenticate.jsβ file, you can use Building an βAuthenticationβ Library from the course notes as a starting point:
Β
- Include the following functions from the notes (these can remain the same)
Β
- setToken(token) o getToken() o removeToken() o readToken() o isAuthenticated()
- authenticateUser(user, password)
Β
- We must also create another function: registerUser(user, password, password2). This function is almost identical to βauthenticateUser(user, password), however it has the following key differences:
Β
- Makes a βpostβ request to β/registerβ instead of β/loginβ
Β
- In addition to providing βuserNameβ and βpasswordβ in the body of the request, it also passes βpassword2β
Β
- If it was successful (ie: status is 200), we do not invoke the βsetToken()β function β we simply return true
Β
Β
Creating a βUserDataβ library
Β
For this application, we will require a second library to work with the new functionality available from our User API, specifically: adding, modifying and deleting favourites and history items.Β To begin, create the file βuserData.jsβ within the newly create βlibβ folder, ie: βmy-app/lib/userData.jsβ: Once you have created the βuserData.jsβ file, add the following functions:
Β
NOTE: Each of the following functions must be defined as βasynchronousβ (ie: βasyncβ) and follows the same logic, ie (pseudocode):
Β
Β
Make a GET, PUT or DELETE request using fetch to the appropriate route starting with process.env.NEXT_PUBLIC_API_URL, ie: process.env.NEXT_PUBLIC_API_URL/favourites/someID, etc.
Β
(For every request, make sure to include an βAuthorizationβ header with a value in the format βJWT TOKENβ, where TOKEN is the value obtained from executing the βgetToken()β function (defined above) from your βAuthenticateβ library
Β
If the operation was successful (ie: status is 200), return the data (ie: the result from calling res.json())
Β
If the operation was not successful (ie: status was not 200), return an empty array, ie: []
Β
Β
Apply the above logic to each of the below functions:
Β
- addToFavourites(id) β PUT request to /favourites/id
Β
- removeFromFavourites(id) β DELETE request to /favourites/id
Β
- getFavourites() β GET request to /favourites
Β
- addToHistory(id) β PUT request to /history/id
Β
- removeFromHistory(id) β DELETE request to /history/id
Β
- getHistory() β GET request to /history
Β
Step 3: Updating our Next.js App (Login and Register components)
Β
Before we start working with the history / favourites directly in the database using our new βlibβ functions, we should add the components / pages to enable the user to register and log into the system.
Β
Β
Creating a βloginβ Page
Β
To begin, start by creating a new file: login.js within the pages directory of your app.
Β
Once this is created, proceed to follow the course notes on: βCreating A βLoginβ Pageβ (making sure to redirect to β/favouritesβ instead of β/vehiclesβ after a successful login).
Β
After implementing the βAlertβ to show errors, test the app by running βnpm run devβ and navigating manually to the /login route.
Β
You should see that you are unable to login, as no users are currently in the system.Β However, you should be able to confirm that the request is being made and that the errors are showing correctly within the βAlertβ component.
Β
Before moving on to the βRegisterβ component and re-testing the login functionality, we must write some additional code to ensure that the atoms defined in store.js are correctly updated with the values from the back end once the user logs in.Β To achieve this, we must:
Β
- Reference both the βfavouritesAtomβ and the βsearchHistoryAtomβ using the βuseAtomβ hook (HINT: Be sure to include the corresponding import statements).
Β
- Import both the βgetFavouritesβ and βgetHistoryβ functions from our newly created βuserData.jsβ file
Β
- Create an βasynchronousβ (async) function called βupdateAtomsβ within the βLoginβ component that updates both the favourites and history with the return values from the βgetFavouritesβ and βgetHistoryβ functions, ie:
Β
async function updateAtoms(){
setFavouritesList(await getFavourites());Β Β Β Β Β setSearchHistory(await getHistory());
}
Β
- Invoke the βupdateAtomsβ function once the user has been authenticated, before redirecting to the β/favouritesβ route, ie:
Β
await updateAtoms();
Β
Here, we can pull the correct favourites and history lists from the API for the logged in user, before they begin to navigate the site.
Β
Β
Creating a βregisterβ Page
Β
Next, we will focus on creating the βregisterβ page, so that we may create users in the system and correctly test the new functionality.Β Begin by creating a new file: register.js within the pages directory of your app.
Β
Once this is created, you can use the now completed login.js file as a starting point.Β Proceed to copy the whole file into βregister.jsβ and rename the component from βLoginβ to βRegisterβ.Β Next, make the following modifications to the code:
Β
- Replace the import for βauthenticateUserβ with βregisterUserβ, ie:
Β
import { registerUser } from β../lib/authenticateβ;
Β
- Remove the imports for βgetFavouritesβ, βgetHistoryβ, βuseAtomβ, βfavouritesAtomβ and βsearchHistoryAtomβ
Β
- Remove the βuseAtom()β function calls from within the βRegisterβ component function
Β
- Remove the βupdateAtoms()β function and the code that invokes it (ie: await updateAtoms())
Β
- Add a βpassword2β value to the state (using useState) with a default value of ββ
Β
- When the form is submitted, instead of invoking βauthenticateUserβ, invoke βregisterUserβ with the βpassword2β value from the state, ie:
Β
await registerUser(user, password, password2);
Β
- Instead of redirecting to β/favouritesβ when the user has logged in, redirect to β/loginβ once the user has registered
Β
- Change the card content to read something related to registering (instead of logging in), ie:
Β
Register
Register for an account:
Β
- Add another <Form.Group> to capture the βpassword2β value. Be sure to include an appropriate label, ie: βConfirm Passwordβ
Β
- Finally, change the button text from βLoginβ to βRegisterβ
Β
With all of these changes in place, we should have a functioning βRegisterβ component / page.Β To test this, ensure that your app is running (npm run dev) and manually navigate to the β/registerβ route and attempt to register for an account on the system.
Β
NOTE: Be sure to test all aspects of the functionality, ie: registering for a duplicate user, mismatched passwords, etc.
Β
Once you have successfully registered for an account, you should be redirected to β/loginβ.Β Proceed to log in with your newly created account.Β You should be redirected to β/favouritesβ (although there will be no favourites shown) and the JWT should be added to local storage.
Β
Β
Step 4: Updating our Next.js App (New βFavouritesβ functionality)
Β
With our system now able to allow users to log in and store the resulting JWT in local storage, letβs update the favourites functionality to use the new API / functionality:
Β
Updating βArtworkCardDetailβ
Β
The main UI for adding / removing favourites exists primarily within the βArtworkCardDetailβ component (specifically, the β+ favouriteβ button).Β To add the new functionality here, we must make the following changes to the βArtworkCardDetail.jsβ file, containing the βArtworkCardDetailβ component:
Β
- Import both the βaddtoFavouritesβ and βremoveFromFavouritesβ functions from our βuserData.jsβ file
Β
- Change the default value for the βshowAddedβ state value to false
Β
- Use the React βuseEffectβ hook to update showAdded instead, ie:
Β
useEffect(()=>{
setShowAdded(favouritesList?.includes(objectID)) }, [favouritesList])
Β
- Modify the βfavouritesClickedβ function so that its βasynchronousβ (async)
Β
- Change the code to βsetFavouritesListβ (ie: updating the atom value) according to the following:
Β
- If showAdded is true (ie: it is in the favourites list), set the favourites list by invoking:
setFavouritesList(await removeFromFavourites(objectID value)) (where objectID value, is the value passed by βpropsβ to the component)
Β
- If showAdded is false (ie: it is not in the favourites list), set the favourites list by invoking:
setFavouritesList(await addToFavourites(objectID value))
Β
Updating βFavouritesβ
Β
Finally, we must make one small update to the βFavouritesβ component (βpages/favourites.jsβ).Β Since the process of populating the favourites list is not instantaneous (ie: pulling it from the API), we want to make sure that our favourites list doesnβt temporarily show the βNothing Hereβ message.Β To resolve this, simply add the following line of code below the line to βuseAtom()β within the component function (ie: after our hooks):
Β
if(!favouritesList) return null;
Β
Additionally, we must ensure that we remove the default value (empty array) for the favouritesAtom within the βstore.jsβ file, ie:
Β
export const favouritesAtom = atom();
Β
If you test the functionality now, you should see that youβre able to add favourites as before, after first logging in with your test user (created when testing the register functionality).Β However, if you inspect the user in the database, you should also see that the item.
Β
Unfortunately, if you refresh the βfavouritesβ page, you will see that once again your favourites list is empty.Β We will fix this issue later on in the assignment.
Β
Β
Step 5: Updating our Next.js App (New βHistoryβ functionality)
Β
The next step is to use a similar strategy to update our βhistoryβ list, such that the values are added / removed from the database.
Β
Updating βMainNavβ
Β
Begin by opening the βMainNavβ Component (components/MainNav.js) and making the following changes:
Β
- Import the βaddtoHistoryβ function from our βuserData.jsβ file
Β
- Modify the βsubmitFormβ function so that its βasynchronousβ (async)
Β
- Change the code to βsetSearchHistoryβ (ie: updating the atom value) to the following
Β
o setSearchHistory(await addToHistory(`title=true&q=${searchField}`))
Β
(where searchField is the value of the βsearchβ form field in the navigation bar)
Β
Updating βAdvancedSearchβ (search.js)
Β
Next, we must update the logic in our βAdvancedSearchβ component (pages/search.js) so that it also makes use of our new logic for persisting the data:
Β
- Import the βaddtoHistoryβ function from our βuserData.jsβ file
Β
- Modify the βsubmitFormβ function so that its βasynchronousβ (async)
Β
- Change the code to βsetSearchHistoryβ (ie: updating the atom value) to the following
Β
o setSearchHistory(await addToHistory(queryString))
Β
(where queryString is the calculated value generated within the βsubmitFormβ function)
Β
Updating βHistoryβ
Β
Finally, we must make a few small changes to the βHistoryβ component (βpages/history.jsβ). As with our favourites component, the process of populating the history list is not instantaneous (ie: pulling it from the API).Β Therefore, we want to make sure that our history list doesnβt temporarily show the βNothing Hereβ message.Β To resolve this, simply add the following line of code below the line to βuseRouter()β within the component function (ie: after our hooks):
Β
if(!favouritesList) return null;
Β
Also as before, we must ensure that we remove the default value (empty array) for the searchHistoryAtom within the βstore.jsβ file, ie:
Β
export const searchHistoryAtom = atom();
Β
However, since itβs also possible to manipulate the history list on this page (ie: removing history items), we must also make the following additional changes to the βHistoryβ component (βpages/historyβ):
Β
- Import the βremoveHistoryβ function from our βuserData.jsβ file
Β
- Modify the βremoveHistoryClickedβ function so that its βasynchronousβ (async)
Β
- Change the code to βsetSearchHistoryβ (ie: updating the atom value) to the following
Β
o setSearchHistory(await removeFromHistory(searchHistory[index]))
Β
(where searchHistory is the value from your βsearchHistoryAtomβ)
Β
If you test the functionality now, you should see that youβre able to add history as before, after first logging in with your test user (created when testing the register functionality).Β However, if you inspect the user in the database, you should also see that the item.
Β
Unfortunately, (as with the favourites page) if you refresh the βhistoryβ page, you will see that your history list is empty.Β Once again, we will fix this issue later on in the assignment.
Β
Β
Step 6: Updating our Next.js App (βRoute Guardβ functionality)
Β
To ensure that users can only access the search / favourites functionality after they have successfully logged into the system, we must implement a βRoute Guardβ as discussed in the course notes.Β Additionally, we will add some logic to populate our βatomsβ when the route guard is first mounted β this will help us resolve the issue of our βfavouritesβ and βhistoryβ lists disappearing when we refresh the pages.
Β
Begin by recreating the βRoute Guardβ example from the notes, including:
Β
- Adding the complete βRouteGuard.jsβ file within the βcomponentsβ directory.
- Updating _app.js to use the new <RouteGuard>β¦</RouteGuard> Component
Β
Once this is complete, we must make the following changes to the βRouteGuardβ component:
Β
- Add β/registerβ to the PUBLIC_PATHS array
- Reference both the βfavouritesAtomβ and the βsearchHistoryAtomβ using the βuseAtomβ hook (HINT: Be sure to include the corresponding import statements).
Β
- Import both the βgetFavouritesβ and βgetHistoryβ functions from our newly created βuserData.jsβ file
Β
- Copy the βasynchronousβ (async) function βupdateAtoms()β defined in the βLoginβ component (above) and paste it within the βRouteGuardβ component function
Β
- Invoke the βupdateAtoms()β at the beginning of the βuseEffect()β hook function (this will ensure that our atoms are up to date when the user refreshes the page)
Β
With this step completed, try testing your app again.Β You should see that the favourites and history lists are saved for the logged in user and they also remain in the UI even after a page is refreshed.Β Additionally, if you manually remove the token from LocalStorage (βApplication Tabβ in the Chrome Dev Tools) and try refreshing or accessing a secure page, you should be redirected back to the βloginβ page.
Β
Β
Step 7: Updating our Next.js App (βNavbarβ UI)
Β
As with the example from the notes, we must also update our βNavbarβ (ie: βMainNavβ component) to reflect whether or not the current user is logged in and give them the ability to βlog outβ:
Β
First, to create the βLog outβ functionality, define a function (ie: βlogout()β within the βMainNavβ (components/MainNav.js) component function), according the following guidelines:
Β
- It must set the βexpandedβ state value to false (in order to collapse the menu)
- Invoke the βremoveToken()β function from the βauthenticateβ lib
- Use the βuseRouter()β hook (router.push()) to redirect the user to the β/loginβ page
Β
With our βlogout()β function in place, we can finally concentrate on updating the Navbar content as well as showing / hiding specific elements within <Navbar.Collapse>β¦</Navbar.Collapse>.
Β
Before we begin however, we must ensure that we have access to current value of the token by invoking the βreadToken()β function from the βauthenticateβ lib (see: βUpdating the Navigation Componentβ).
Β
Now that we potentially have the token (stored in a token variable), we can use the value to update the Navbar to show user content / add new items:
Β
- If the user is logged in (ie: value of token is truthy)
Β
- Show the βAdvanced Searchβ navigation item o Show the βSearchβ form o Show the βUser Nameβ dropdown
- Update the text βUser Nameβ to show the userName value from the token
- Add a new <NavDropdown.Item>β¦</NavDropdown.Item> item with the text Logout that, when clicked, will invoke the newly created βlogout()β function (above)
- (Optionally) remove the βactiveβ property from the βFavouritesβ and βSearch Historyβ items
Β
- If the user is not logged in (ie: the value of token is falsy)
Β
- Show a new <Nav>β¦</Nav> element (beneath the <Nav className=βme-autoβ>β¦</Nav> element that contains two links for Register (β/registerβ) and Login (β/loginβ)
Β
NOTE: For each of the links, be sure to include the active property and to set the βexpandedβ state value to false when clicked (use the β/β and β/searchβ links as examples)
Β
Step 8: Publishing our App on Vercel
Β
If you test the app locally now, you should see that it functions the same as the example code, ie: you can create multiple accounts and for each account, store different favourites / search histories.
Β
As a final step, we must place this code online.Β For this purpose, we will use βVercelβ, as in the course notes.Β For this final part of the assignment, follow the βIntroduction to Vercelβ and record the production URL for your assignment submission.
Β
NOTE: The instructions assume that you have already pushed your Next.js code to a private GitHub repository.
Β
Β
Assignment Submission:
- Add the following declaration at the top of your index file:
/********************************************************************************* *Β WEB422 β Assignment 06
- I declare that this assignment is my own work in accordance with Seneca Academic Policy.Β No part of this *Β assignment has been copied manually or electronically from any other source (including web sites) orΒ *Β distributed to other students.
*
- Name: ______________________ Student ID: ______________ Date: ________________
*
- Vercel App (Deployed) Link: _____________________________________________________
*
********************************************************************************/
Β
- Next, Compress (.zip) both your User API and your js App source code folders together (omitting the node_modules folder, as usual) in order to produce a single .zip file for your submission.
- Submit your compressed file (containing both your User API and the Node.js App) to My.Seneca under Assignments -> Assignment 6
Important Note:
- NO LATE SUBMISSIONS for assignments. Late assignment submissions will not be accepted and will receive a grade of zero (0).
- After the end (11:59PM) of the due date, the assignment submission link on My.Seneca will no longer be available.
- Submitted assignments must run locally, ie: start up errors causing the assignment/app to fail on startup will result in a grade of zero (0) for the assignment.







