The user-helpers module

In the previous activity we allowed users to 'sign up', in this step we'll allow them to log in.

Normally user data is stored in a database, but for now when a user signs up, we'll store their information in a .json file and we'll use file i/o access it.

Now let's create a module that will allow us to work with the data in the users.json file. Create a file in the modules folder named user-helpers.js. Add this code to the file:

const fs = require("fs");

const path = __dirname + "/../users.json";
console.log("User data will be stored in this file: ", path);

function saveUsers(users){
  const json = JSON.stringify(users);
  fs.writeFileSync(path, json);
}

function getAllUsers(){
  // TODO: Add code here
}

function addUser(user){
  // TODO: Add code here
}

function getUserByEmail(email){
  // TODO: Add code here
}

function login(email, password){
  // TODO: Add code here
}


exports.saveUsers = saveUsers;
exports.getAllUsers = getAllUsers;
exports.addUser = addUser;
exports.getUserByEmail = getUserByEmail;
exports.login = login;

This module defines 5 functions and exports all of them, so that they can be imported into other files.

The saveUsers() method is already complete, but we'll add code for the other functions in the upcoming steps.

Note that the saveUsers() function has a parameter, that is intended to be an array of 'user' objects. In the body of the function, the array is 'parsed' into a JSON string. The string is then saved to a file named 'users.json', which is part of the path constant.

We are using the synchronous method writeFileSync() because it's simpler than dealing with callbacks. But it could create some bottlenecks that we could avoid by using the asynchronous version.

Now create a file named users-sample.js in the samples folder, so that we can test the functions in the user-helpers module. Put this code in the file:

const {saveUsers, getAllUsers, addUser, getUserByEmail, login} = require("../modules/user-helpers");

// Test the saveUsers() method
const userData = [
  {"firstName":"Bob", "lastName":"Smith", "email":"bob@smith.com", "password":"test123"}
]

saveUsers(userData);

The first line imports all of the functions from the user-helpers modules (we'll be testing them all soon). Then we create an array that has a single 'user' object in it. Finally we pass the array into the saveUsers() function, and as you know, it should convert the array into a JSON string and then save the string in the users.json file.

Run the sample code by entering this command into the terminal: node samples/users-sample.js. You should see that a file named users.json has been created in the project folder.

Now update the getAllUsers() function (in the user-helpers module) so that it looks like this:

function getAllUsers(){
  const usersJSON = fs.readFileSync(path, "utf-8");
  return JSON.parse(usersJSON);
}

The code we've added to the body reads the string in the users.json file (which is in the path variable), and then 'parses' it into an array (of 'users') and then returns the array.

To test this function, add the following code to user-samples.js:

// Test the getAllUsers() function
const users = getAllUsers();
console.log("All Users: ", users);

Now run the sample program (node samples/users-sample.js) and you should see a console log that shows the returned 'users' array.

Update the addUser() function to look like this:

function addUser(user){
  const users = getAllUsers();
  users.push(user);
  saveUsers(users);
}

Hopefully this code makes sense. To test it out, add this code to users-sample.js:

// Test the addUser() function
const newUser = {"firstName":"Betty", "lastName":"Jones", "email":"betty@jones.com", "password":"opensesame"};
addUser(newUser);

Update the getUserByEmail() function to look like this:

function getUserByEmail(email){
  const users = getAllUsers();
  return users.find(u => u.email === email);
}

Hopefully you understand this code. Test it by adding this to users-sample.js:

// Test the getUserByEmail() function
console.log(getUserByEmail("bob@smith.com"));

Now, when you run the sample program, the console should show you the user object that has the email address that was passed into the function.

Finally, update the login() function to look like this:

function login(email, password){
  const user = getUserByEmail(email);
  if(user && user.password === password){
    return user;
  }
  return false;
}

If there is a 'user' object that matches the email and password parameters, then we return it. Otherwise the function should return false.

To test it, add this code to users-sample.js:

// Test the login() function
console.log(login("bob@smith.com","test123")); // should return the 'user' obj for bob smith
console.log(login("bob@smith.com","blah")); // should return false
console.log(login("blah","blah")); // should return false

There is one last thing we can do to finish off our user-helpers module, update the saveUsers() function to look like this:

function saveUsers(users){
  const json = JSON.stringify(users, null, 2)
  fs.writeFileSync(path, json);
}

All we've done here is added a few extra parameters to the JSON.stringify() method, which will format the user objects nice and neat when they are saved. Run the sample program again, and then look in the user.json file, and you'll see what I mean.

It might also be wise to NOT export the saveUsers() function. If this function is not used properly, it could wipe out all of our user data. It really should only be invoked from inside it's own module. But we'll leave things as they are for now.

Here's a good article on file IO and json files in NodeJS

Finishing the Sign Up Page

Now that we can save user data to a file, we can finish the functionality for the sign up page.

In app.js update the signup-confirmation route to look like this:

app.post('/signup-confirmation', (req, res) => {

  // import the addUser function
  const {addUser} = require("./modules/user-helpers");
  
  // destructure the req.body object into individual variables
  const {firstName, lastName, email, password, confirmPassword} = req.body;
  
  // make sure that all required data has been sent
  if(firstName && lastName && email && password && confirmPassword){
    // make sure the passwords match
    if(password === confirmPassword){
      // If everything is valid, then add the new user
      addUser({firstName, lastName, email, password});
      res.send("Thank you for signing up!")
    }else{
      res.send("Invalid form submit - Passwords do not match!")
    }
  }else{
   res.send("Invalid form submit - All fields are required!");
  }
});

There's quite a bit going in the code for this route, but hopefully the comments do a good job of explanining it. Let me know if you have any questions about the code.

Make sure to restart the web server by pressing Ctrl + c in the terminal, and then entering node app.js. Hopefully users can now complete the sign up form and their data will end up in the users.json file.

Creating a Login Page

Now that we have users, we should create a login form so that we can 'authenticate' them.

Create a file in the views folder named login-layout.ejs and put this code in it:

<%- include('partials/top') %>
<form method="POST" action="/login">
  <label>Email:</label>
  <input type="text" name="email">
  <br>
  <label>Password:</label>
  <input type="password" name="password">
  <br>
  <input type="submit" value="Log In">
</form>
<%- include('partials/bottom') %>

Note that the method attribute is set to POST, and the action attribute is set to /login. When this form is subbmitted, it will send a POST request to the /login route.

Now, add these two routes to app.js:

app.get('/login', (req, res) => {
  res.render('login-layout', {
     title: "Log In"
  });
});

app.post('/login', (req, res) => {
  res.send("TODO: handle login POST")
});

To see the login page, go to this url: localhost:8080/login (make sure the app is running first).

When you click on the submit button, a POST request will be sent to /login and you should see the TODO message in your browser.

Now, update the 'post' route so that it looks like this:

app.post('/login', (req, res) => {
  
  // import the login() function
  const {login} = require("./modules/user-helpers");
  
  // destructure the req.body object to get the email and password from it
  const {email, password} = req.body;
  
  // attempt to login
  const user = login(email, password);
  if(user){
    res.send(`Hello ${user.firstName}`);
  }else{
    res.send("Invalid Login Attempt");
  }
});

Now restart the app and try to login with bob@smith.com and test123.

We now have a crude and simple system of managing and authenticating users. You could potentially start building a mult-user system, which is something we'll do in future classes.