Web322 Assignment6
Web322 Assignment6
Assessment Weight:
9% of your final course Grade
Objective:
Part A: Work with Client Sessions and data persistence using MongoDB to add user registration and Login/Logout
functionality & tracking (logging)
Part B: Update the password storage logic to include "hashed" passwords (using bcrypt.js)
NOTE: You will be using NoSQL (MongoDb) *AND* SQL (Postgres) at the same time. This isn’t really an industry standard,
but it shows you that you can use multiple technologies in one project.
Specification:
For this assignment, we will be allowing users to "Register" for an account on your WEB322 App. Once users are
registered, they can log in and access all related items/category views. By default, these views will be hidden from the
end user and unauthenticated users will only see the "store" and "about" views / top menu links. Once this is complete,
we will add bcrypt.js to our code to ensure that all stored passwords are "hashed"
NOTE: If you are unable to start this assignment because Assignment 5 was incomplete - email your professor for a clean
version of the Assignment 5 files to start from (effectively removing any custom CSS or text added to your solution).
Follow the instructions from the under the section: "Setting up a MongoDB Atlas account"
Continue following the instructions until you create a new database (named whatever you like) and connection
string, to be used in the following steps.
If you already have an account, you can only have 1 Cluster for free. So you can use the same cluster you’ve used
for other projects/tests/classes.
2. Create a new file at the root of your web322-app folder called "auth-service.js"
3. "Require" your new "auth-service.js" module at the top of your server.js file as "authData"
4. Inside your auth-service.js file write code to require the mongoose module and create a Schema variable to
point to mongoose.Schema (Hint: refer to the Week 8 notes)
NOTE: this will be an array of objects that use the following specification:
userAgent String
6. Once you have defined your "userSchema" per the specification above, add the line:
initialize()
Much like the "initialize" function in our store-service module, we must ensure that we are able to connect to
our MongoDB instance before we can start our application.
We must also ensure that we create a new connection (using createConnection() instead of connect() - this will
ensure that we use a connection local to our module) and initialize our "User" object, if successful
Additionally, if our connection is successful, we must resolve() the returned promise without returning any data
If our connection has an error, we must, reject() the returned promise with the provided error:
To achieve this, use the following code for your new initialize function, where connectionString is your
completed connection string to your MongoDB Atlas database as identified above:
module.exports.initialize = function () {
return new Promise(function (resolve, reject) {
let db = mongoose.createConnection("connectionString");
db.on('error', (err)=>{
reject(err); // reject the promise with the provided error
});
db.once('open', ()=>{
User = db.model("users", userSchema);
resolve();
});
});
};
registerUser(userData)
This function is slightly more complicated, as it needs to perform some data validation (ie: do the passwords
match? Is the user name already taken?), return meaningful errors if the data is invalid, as well as saving
userData to the database (if no errors occurred). To accomplish this:
o You may assume that the userData object has the following properties:
.userName, .userAgent, .email, .password, .password2 (we will be using these field names when we
create our register view). You can compare the value of the .password property to the .password2
property and if they do not match, reject the returned promise with the message: "Passwords do not
match"
o Otherwise (if the passwords successfully match), we must create a new User from the userData passed
to the function, ie: let newUser = new User(userData); and invoke the newUser.save() function (Hint:
refer to the Week 8 notes)
If an error (err) occurred and its err.code is 11000 (duplicate key), reject the returned promise
with the message: "User Name already taken".
If an error (err) occurred and its err.code is not 11000, reject the returned promise with the
message: "There was an error creating the user: err" where err is the full error object
If an error (err) did not occur at all, resolve the returned promise without any message
checkUser(userData)
This function is also more complex because, while we may find the user in the database whose userName
property matches userData.userName, the provided password (ie, userData.password) may not match (or the
user may not be found at all / there was an error with the query). In either case, we must reject the returned
promise with a meaningful message. To accomplish this:
o Invoke the find() method on the User Object (defined in our initialize method) and filter the results by
only searching for users whose user property matches userData.userName, ie:
User.find({ userName: userData.userName }) (Hint: refer to the Week 8 notes)
If the find() promise resolved successfully, but users is an empty array, reject the returned promise
with the message "Unable to find user: user" where user is the userData.userName value
If the find() promise resolved successfully, but the users[0].password (there should only be one
returned user) does not match userData.password, reject the returned promise with the error
"Incorrect Password for user: userName" where userName is the userData.userName value
If the find() promise resolved successfully and the users[0].password matches userData.password,
then we must perform the following actions to record the action in the "loginHistory" array before
we can resolve the promise with the users[0] object:
Using the returned user object (ie, users[0]), push the following object onto its
"loginHistory" array:
Next, invoke the update method on the User object where userName is
users[0].userName and $set the loginHistory value to users[0].loginHistory. (Hint: refer
to the Week 8 notes for a refresher on update)
Finally, if the above was successful, resolve the returned promise with the users[0]
object. If it was unsuccessful, reject the returned promise with the message: "There
was an error verifying the user: err" where err is the full error object
If the find() promise was rejected, reject the returned promise with the message "Unable to find
user: user" where user is the userData.userName value
storeData.initialize()
.then(function(){
app.listen(HTTP_PORT, function(){
console.log("app listening on: " + HTTP_PORT)
});
}).catch(function(err){
console.log("unable to start server: " + err);
});
Since our server also requires authData to be working properly, we must add its initialize method (ie:
authData.initialize) to the promise chain:
storeData.initialize()
.then(authData.initialize)
.then(function(){
app.listen(HTTP_PORT, function(){
console.log("app listening on: " + HTTP_PORT)
});
}).catch(function(err){
console.log("unable to start server: " + err);
});
2. Be sure to "require" the new "client-sessions" module at the top of your server.js file as clientSessions.
3. Ensure that we correctly use the client-sessions middleware with appropriate cookieName, secret, duration and
activeDuration properties (HINT: Refer to Week 10 notes under "Step 2: Create a middleware function to setup
client-sessions.")
4. Once this is complete, incorporate the following custom middleware function to ensure that all of your
templates will have access to a "session" object (ie: {{session.userName}} for example) - we will need this to
conditionally hide/show elements to the user depending on whether they're currently logged in.
5. Define a helper middleware function (ie: ensureLogin from the Week 10 notes) that checks if a user is logged in
(we will use this in all of our item / category routes). If a user is not logged in, redirect the user to the "/login"
route.
6. Update all routes that begin with one of: "/items", "/categories", "/item" or "/category" (ie: everything that is
not "/", "/store" or "/about" - this should be 9 routes) to use your custom ensureLogin helper middleware.
GET /register
This "GET" route simply renders the "register" view without any data (See register.hbs under Adding New
Routes below)
POST /register
This "POST" route will invoke the authData.RegisterUser(userData) method with the POST data (ie: req.body).
o If the promise resolved successfully, render the register view with the following data:
{successMessage: "User created"}
o If the promise was rejected (err), render the register view with the following data:
{errorMessage: err, userName: req.body.userName} - NOTE: we are returning the user back to the
page, so the user does not forget the user value that was used to attempt to register with the system
POST /login
Before we do anything, we must set the value of the client's "User-Agent" to the request body, ie:
req.body.userAgent = req.get('User-Agent');
Next, we must invoke the authData.CheckUser(userData) method with the POST data (ie: req.body).
o If the promise resolved successfully, add the returned user's userName, email & loginHistory to the
session and redirect the user to the "/items" view, ie:
authData.checkUser(req.body).then((user) => {
req.session.user = {
userName: // authenticated user's userName
email: // authenticated user's email
loginHistory: // authenticated user's loginHistory
}
res.redirect('/items);
})
o If the promise was rejected (ie: in the "catch"), render the login view with the following data (where err
is the parameter passed to the "catch": {errorMessage: err, userName: req.body.userName} - NOTE:
we are returning the user back to the page, so the user does not forget the user value that was used to
attempt to log into the system
GET /logout
This "GET" route will simply "reset" the session (Hint: refer to the Week 10 notes) and redirect the user to
the "/" route, ie: res.redirect('/');
GET /userHistory
This "GET" route simply renders the "userHistory" view without any data (See userHistory.hbs under Adding
New Routes below). IMPORTANT NOTE: This route (like the 9 others from above) must also be protected by
your custom ensureLogin helper middleware.
layouts/main.hbs
To enable users to register for accounts, login / logout of the system, and conditionally hide / show menu items,
we must make some small changes to our main.hbs.
Update the code inside the <div class="collapse navbar-collapse">…</div> block in the header, just below the
<ul class="nav navbar-nav">…</ul> element (this element has the "Store" and "About" links) according to the
following specification: (Note: pay close attention to the formatting when copying/pasting code from this
document).
NOTE: The below code will replace the existing code:
<ul class="nav navbar-nav navbar-right">
{{#navLink "/items"}}Items{{/navLink}}
{{#navLink "/categories"}}Categories{{/navLink}}
</ul>
If session.user exists (ie: the user is logged in), show the following HTML:
If session.user does not exist (ie: the user is not logged in), show the following HTML:
login.hbs
This (new) view must consist of the "login form" which will allow the user to submit their credentials (using
POST) to the "/login" POST route:
name: "password"
password placeholder: "Password"
required
Above the form, we must have a space available for error output: Show the element: <div class="alert alert-
danger"> <strong>Error:</strong> {{errorMessage}}</div> only if there is an errorMessage rendered with the
view.
register.hbs
This (new) view must consist of the "register form" which will allow the user to submit new credentials (using
POST) to the "/register" POST route. IMPORTANT NOTE: this form is only visible if successMessage was not
rendered with the view (refer to the "/register" POST route above for more information). If successMessage was
rendered with the view, we will show different elements.
name: "password"
password placeholder: "Password"
required
name: "password2"
password placeholder: "Confirm Password"
required
name: "email"
email placeholder: "Email Address"
required
Above the form, we must have a space available for error output: Show the element: <div class="alert alert-
danger"> <strong>Error:</strong> {{errorMessage}}</div> only if there is an errorMessage rendered with the
view.
Additionally, we must also have a space available for success output: Show the elements: <div class="alert
alert-success"> <strong>Success:</strong> {{successMessage}}</div><a class="btn btn-success pull-right"
href="/login"> Proceed to Log in </a><br /><br /><br /> only if
there is a successMessage rendered with the view (this will be rendered instead of the form.
userHistory.hbs
This (new) view simply renders the following table using the globally available session.user.loginHistory object
Column Value
Login Date/Time This will be the dateTime value for the current loginHistory object
Client Information This will be the userAgent value for the current loginHistory object
Additionally, in the page title <h2>…</h2> block, add the code to show the userName and email properties of
the logged in user (session.user) in the following format: userName ( email ) History
Part B - Hashing Passwords
We will be using the "bcryptjs" 3rd party module, so we must go through the usual procedure to obtain it (and include it
in our "auth-service.js" module).
1. Open the integrated terminal and enter the command: npm install "bcryptjs"
2. At the top of your auth-service.js file, add the line: const bcrypt = require('bcryptjs');
You should now see a list of databases & collections. Simply hover over the collection that you wish to remove
(ie: users) and click the trash can icon that appears.
Lastly, enter the name of the collection (ie: users) in the confirmation dialog to drop the "users" collection
Updating registerUser(userData)
Recall from the Week 12 notes - to encrypt a value (ie: "myPassword123"), we can use the following code:
bcrypt.hash("myPassword123", 10).then(hash=>{ // Hash the password using a Salt that was generated using 10 rounds
// TODO: Store the resulting "hash" value in the DB
})
.catch(err=>{
console.log(err); // Show any errors that occurred during the process
});
Use the above code to replace the user entered password (ie: userData.password) with its hashed version (ie:
hash) before continuing to save userData to the database and handling errors.
If there was an error, reject the returned promise with the message "There was an error encrypting the
password" and do not attempt to save userData to the database.
Updating checkUser(userData)
Recall from the Week 12 notes - to compare an encrypted (hashed) value (ie: hash) with a plain text value (ie:
"myPassword123", we can use the following code:
If the passwords do not match (ie: result === false) reject the returned promise with the message "Incorrect Password
for user: userName" where userName is the userData.userName value
Push commits to the same private web322-app GitHub repository either through the integrated terminal (git
push) or through the button interface on Visual Studio Code (publish, sync, etc.)
If set up correctly from Assignment 2, it will automatically be deployed to Vercel but if there are any problems,
follow the Vercel Guide on Web322.ca for more details on pushing to GitHub and linking your app to Vercel for
deployment
IMPORTANT NOTE: Since we are using a free account on Vercel, we are limited to only 3 apps, so if you have
been experimenting on Vercel and have created 3 apps already, you must delete one. Once you have received a
grade for Assignment 1, it is safe to delete this app (login to the Vercel website, click on your app and then click
Advanced and finally, Delete App).
The “hscanlansen” GitHub account should already be added as a collaborator to your web322-app GitHub
repository for proper access as your GitHub repository SHOULD NOT BE PUBLIC!
Assignment Submission:
Add the following declaration at the top of your server.js file:
/*********************************************************************************
* WEB322 – 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:
* Date
* Student ID:
* Vercel Web App URL: ________________________________________________________
*
* GitHub Repository URL: ______________________________________________________
*
********************************************************************************/
Publish your application to GitHub/Vercel and test to ensure correctness
Compress your web322-app folder and Submit your file to My.Seneca under Assignments -> Assignment 6
(MAKE SURE TO TEST LOCALLY FIRST! – download zip, unzip, run ‘node server.js’)
Important Note:
If the assignment will not run (using "node server.js") due to an error, the assignment will receive a
grade of zero (0).