Description
Objective: 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 list saved, so that they can return to it later and on a different device. To achieve this, we will primarily be working with concepts from Week 11, such as incorporating JWT in a Web API, as well as using route guards, http interceptors and localstorage in our Angular app. Sample Solution: https://tender-borg-af89d2.netlify.app Step 1: Creating a “User” API To enable our Music App to register / authenticate users and persist their “favourites” list, we will need to create our own “User” API and publish it online (Heroku). 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:@clusterInfo.abc123.mongodb.net/myFirstDatabase?retryWrit es=true&w=majority • Add your Database User password in place of and your “DATABASE NAME” (from above) in place of myFirstDatabase • 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-2021/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 create / secure the routes and publish the server online (Heroku). You will notice however, that there is an extra file included in the sample solution: “.env”. This file is used by dotenv to store environment variables that can be accessed from our code locally with process.env. Once the code is online, these values will also need to be created as environment (“config”) variables within that environment – see: https://devcenter.heroku.com/articles/config-vars. At the moment, this file contains two values: MONGO_URL and JWT_SECRET. MONGO_URL is used by the “user-service” 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 creating and 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 Week 11 Notes under the heading “Adding the code to server.js”. The primary differences are: • We will be using the value of “secretOrKey” from process.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 route specifications required for our User API (HINT most of the code described below is very similar to the code outlined in the Week 11 notes, so make sure you have them close by for reference): POST /api/user/register This route invokes the “.registerUser()” method of “userService” and provides the request body as the single parameter. • If the promise resolves successfully, send the returned (success) message back to the client as a json formatted object containing a single “message” property (with the value of the returned message). • If the promise is rejected, send the returned (error) message back to the client using the same strategy (ie: a JSON formatted object with a single “message” property). However, the status code must also be set to 422. POST /api/user/login This is the route responsible for validating the user in the body of the request, as well as generating a “token” to be sent in the response. This is accomplished by invoking the “.checkUser()” method of “userService” and providing the request body as the single parameter. • If the 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 the “jwt” module (required at the top of server.js as “jsonwebtoken”) and the secret from process.env.JWT_SECRET (from our .env file). Once you have your signed token, send a JSON formatted object back to the client with a “message” property that reads “login successful” and a “token” property that has the signed token. • If the promise is rejected, send the returned (error) message back to the client using the same strategy (ie: a JSON formatted object with a single “message” property). However, the status code must also be set to 422. GET /api/user/favourites – (protected using the passport.authenticate() middleware) Here, we simply have to obtain the list of favourites for the user (only if the user provided a valid JWT). This can be accomplished by invoking the “.getFavourites()” method of “userService” and providing req.user._id as the single parameter. This way, we will provide the correct favourites list for the correct user, based on the _id value sent to the server in the JWT. • If the promise resolves successfully, send the JSON formatted data back to the client • If the promise is rejected, send a message back to the client as a json formatted object containing a single “error” property (with the value of the returned error). PUT /api/user/favourites/:id – (protected using the passport.authenticate() middleware) This route is responsible for adding a specific favourite (sent as the “id” route parameter) to the user’s list of favourites (only if the user provided a valid JWT). This is accomplished by invoking the “.addFavourite()” method of “userService” and providing req.user._id as the first parameter and the id route parameter as the second parameter. • If the promise resolves successfully, send the JSON formatted data back to the client • If the promise is rejected, send a message back to the client as a json formatted object containing a single “error” property (with the value of the returned error). DELETE /api/user/favourites/:id – (protected using the passport.authenticate() middleware) Finally, this route is responsible for removing a specific favourite (sent as the “id” route parameter) from the user’s list of favourites (only if the user provided a valid JWT). This is accomplished by invoking the “.removeFavourite()” method of “userService” and providing req.user._id as the first parameter and the id route parameter as the second parameter. • If the promise resolves successfully, send the JSON formatted data back to the client • If the promise is rejected, send a message back to the client as a json formatted object containing a single “error” property (with the value of the returned error). With these routes in place, your User API should now be complete. The final step is to push it to Heroku (recall: Getting Started With Heroku 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 Heroku – Setting up the MONGO_URL and JWT_SECRET Config Variables: • Login to Heroku to see your dashboard • Click on your newly created application • Click on the “Settings” tab at the top (next to “Access”) • Click the “Reveal Config Vars” button • Enter JWT_SECRET in the “KEY” textbox and add your JWT_SECRET (from .env) in the “VALUE” textbox (without quotes) and hit the “Add” button • Similarly, enter MONGO_URL in the “KEY” textbox and add your MONGO_URL (from .env) in the “VALUE” textbox (without quotes) and hit the “Add” button 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-123.herokuapp.com/api/user” somewhere handy, as this will be the “userAPIBase” used in our Angular application. Step 2: Updating our Angular App Now that we have our User API in place, we can make some key changes in our Angular App to ensure that only registered / logged in users can view the data, as well as to finally persist their favourites list in our mongoDB “users” collection. (HINT: Once again, most of the code described below is very similar to the code outlined in the Week 11 notes, so make sure you have them close by for reference). Updating environment.ts / environment.prod.ts Since we just completed setting up our User API on Heroku, why don’t we start by adding it to our environment.ts and environment.prod.ts files as: userAPIBase, ie: export const environment = { … userAPIBase: Users API on Heroku, ie: “https://some-randomName-123.herokuapp.com/api/user” }; Installing @auth0/angular-jwt / Types Before we start creating our services and new components, we must first use npm to install @auth0/angular-jwt and add the following types in their own files: • File: /src/app/User.ts export default class User{ _id: string = “”; userName: string = “”; password: string = “”; } • File: /src/app/RegisterUser.ts export default class RegisterUser{ userName: string = “”; password: string = “”; password2: string = “”; } Creating an “AuthService” Since we will be requiring users to be authenticated to view / interact with our data, our next step should be to create an “AuthService” Once you have created your “AuthService” (using the command ng g s Auth), you can use the following as a starting point to build out your service: https://pat-crawford-sdds.netlify.app/shared/fall-2021/web422/A6/auth.service.ts.txt You will notice that a number of imports are in place for you, including “environment”. This will allow you to reference your “userAPIBase” environment value using the code: environment.userAPIBase Finally, we must complete the service by implementing the following functions: • getToken() – return type string This method simply pulls the item “access_token” from “localStorage” and returns it. • readToken() – return type User This method also pulls the item “access_token” from “localStorage”, however it uses “helper” from “JwtHelperService()” to decode it and return the decoded value. • isAuthenticated() – return type Boolean Once again, this method pulls “access_token” from “localstorage”. If the token was present in localStorage, return true, otherwise return false • login(user) – return type Observable This method will take the user parameter (type: User) and attempt to “log in” using the User API. This is done by sending the user data via a POST request to environment.userAPIBase/login using the HttpClient service (http). The return value for this method is the return value from the http.post method call (ie: the Observable) • logout() All this method does is remove “access_token” from “localStorage” • register(registerUser) return type Observable This method will take the registerUser parameter (type: registerUser) and attempt to “register” using the User API. This is done by sending the registerUser data via a POST request to environment.userAPIBase/register using the HttpClient service (http). The return value for this method is the return value from the http.post method call (ie: the Observable) Creating a “RegisterComponent” and testing the AuthService With our AuthService in place, it makes sense to move on to creating a “RegisterComponent” and see if we can use our App to register users in the system. To begin, create a new RegisterComponent using the “ng” command Once this is created, you may use the following CSS (register.component.css) as a starting point (please note: this is optional, as you’re free to style the app however you wish) • mat-card { max-width: 400px; margin: 2em auto; text-align: center; } mat-form-field { display: block; } .success{ text-align: center; padding-top:10px; } You may also use the following html as a starting point for your template • https://pat-crawford-sdds.netlify.app/shared/fall-2021/web422/A6/register.component.html.txt (Again, this is only a suggestion, as you’re free to style the app however you wish.) With the boilerplate code in place, we can now start on writing the logic for the component class template according to the following specification: • The component must have the following properties: o registerUser (default value: {userName: “”, password: “”, password2: “”}) – NOTE: this is the data that is synced to the form o warning (default value “”) o success (default value false) o loading (default value: false) • The component requires an instance of the “AuthService” • There is only one method implemented: onSubmit(), which must be invoked when the form in registration.component.html is submitted. This function must: o First, ensure that registerUser.userName is not blank (“”) and that registerUser.password equals registerUser.password2 o If this is the case, set loading to true and invoke the register method on the instance of “AuthService” with the registerUser object (defined as a property in the class) as its single parameter. Be sure to subscribe to the returned Observable. o If the Observable broadcasts that it was successful, then we must set: ▪ success to true ▪ warning to “” ▪ loading to false o If the Observable broadcasts that there was an error, then we must set ▪ success to false ▪ warning to the error message broadcast from the observable, ie: err.error.message ▪ loading to false • The rest of the logic is handled in the template itself. By setting different states in the component, ie: success, warning, message, etc. we can respond appropriately in the template, for example: o Ensure that the form successfully handles the submit event and all of the form fields are bound correctly to their corresponding properties in the “registerUser” object o If a warning exists, show the warning in the element near the top of the template (currently stating: Optional Warning) o Set the disabled property on the submit button if loading is true o Show a different submit button message if loading is true, ie: “Processing Request” (NOTE: This will help keep the user informed if our User API is currently “asleep” when the user tries to register) o Show the