CSE 340: Web Backend Development

W05 Learning Activity: User Login

Overview

In this activity, you will learn how to create a login system that checks a user's credentials and saves that information to the session to remember who is logged in.

As part of that process, you will build login functionality that verifies user passwords, creates sessions, and protects routes that require a logged-in user. By the end of this activity, you will have a working login system for your application.

Preparation Material

Authentication

As you recall from the previous learning activity on Authentication and Authorization, authentication is the process of verifying who a user is.

In your case, the user provides an email and password to prove who they are. The server then checks if those credentials are correct. If they are correct, the server creates a session to remember that the user is logged in.

The Login Process

When a user submits a login form, several steps happen:

  1. The server receives the email and password from the form.
  2. The server looks up the user in the database using the email.
  3. The server compares the submitted password with the stored password.
  4. If the passwords match, the server creates a session for the user.
  5. The server stores the user information (not including the password hash) in the session.

Password Security with bcrypt

As you have learned previously, passwords should never be stored in plain text in the database. Instead, they are stored as hashed values. The bcrypt library has a compare function that can check if a submitted password matches the hashed password stored in the database. This keeps passwords secure even if someone gains access to the database.

Sessions and Authentication

A session is a way for the server to remember information about a user between different page requests. When a user logs in successfully, their user information is stored in the session. The server can then check the session on future requests to see if the user is logged in.

Protecting Routes

Some pages in your application should only be available to logged-in users. You will create middleware that checks if a user is logged in. Middleware is code that runs before your route handlers and can block access if the user is not logged in. This middleware will check the session for user information and redirect unauthorized users to the login page.

Activity Instructions

Create Model Functions for User Authentication

You need to create functions in your model that will handle finding users and checking passwords. Complete the following:

  1. Open your user model file src/models/users.js .
  2. Create a function named findUserByEmail that accepts an email address as a parameter and returns the user from the database with that email. This function should look as follows:
    const findUserByEmail = async (email) => {
        const query = `
            SELECT user_id, name, email, password_hash, role_id 
            FROM users 
            WHERE email = $1
        `;
        const query_params = [email];
        
        const result = await db.query(query, query_params);
    
        if (result.rows.length === 0) {
            return null; // User not found
        }
        
        return result.rows[0];
    };
    
  3. Create a function named verifyPassword that accepts a plain text password and a hashed password as parameters. It then uses bcrypt.compare() to check if they match. Return true if they match, false if they do not. Your function should look as follows:
    const verifyPassword = async (password, passwordHash) => {
        return bcrypt.compare(password, passwordHash);
    };
    
  4. Create a function named authenticateUser that takes an email and password as parameters. This function should:
    • Use findUserByEmail to get the user.
    • If no user is found, return null.
    • Use verifyPassword to check if the password is correct.
    • If the password is correct, remove the password_hash from the user object and return the user object. If not, return null.
  5. The only function the controller needs access to is the authenticateUser function, so make sure to export it, but you do not need to export the other two functions.

Create Controller Functions for Login

Now you need to create controller functions to handle showing the login form and processing the login. Complete the following:

  1. Open your src/controllers/users.js controller file.
  2. Import the authenticateUser function from your user model.
  3. Create a function called showLoginForm that renders the login view.
  4. Create a function called processLoginForm that does the following:
    • Gets the email and password from the request body.
    • Calls authenticateUser with the email and password.
    • Check to see if a user object is returned. If so:
      • Add the user object to the session object: req.session.user = user;.
      • Add a success flash message that the login was successful.
      • Add a console.log() statement to log the user in the console for debugging purposes.
      • Redirect to the home page.
    • If authentication fails (the function returns null):
      • Add an error flash message that the login failed.
      • Redirect the user back to the login page.
  5. Create a function called processLogout that does the following:
    • Destroys the session using req.session.destroy()
    • Adds a success flash message indicating the user has logged out.
    • Redirects the user to the login page.
  6. Export all three functions.
Sample Code (click to expand)
const showLoginForm = (req, res) => {
    res.render('login', { title: 'Login' });
};

const processLoginForm = async (req, res) => {
    const { email, password } = req.body;

    try {
        const user = await authenticateUser(email, password);
        if (user) {
            // Store user info in session
            req.session.user = user;
            req.flash('success', 'Login successful!');

            if (res.locals.NODE_ENV === 'development') {
                console.log('User logged in:', user);
            }

            res.redirect('/');
        } else {
            req.flash('error', 'Invalid email or password.');
            res.redirect('/login');
        }
    } catch (error) {
        console.error('Error during login:', error);
        req.flash('error', 'An error occurred during login. Please try again.');
        res.redirect('/login');
    }
};

const processLogout = async (req, res) => {
    if (req.session.user) {
        delete req.session.user;
    }

    req.flash('success', 'Logout successful!');
    res.redirect('/login');
};

Create Routes for Login

Complete the following:

  1. Open your routes file src/controllers/routes.js .
  2. Import the login controller functions you created.
  3. Add a GET route for /login that calls the showLoginForm function.
  4. Add a POST route for /login that calls the processLoginForm function.
  5. Add a GET route for /logout that calls the processLogout function.
Sample Code (click to expand)
// User login routes
router.get('/login', showLoginForm);
router.post('/login', processLoginForm);
router.get('/logout', processLogout);

Create the Login View

You need to create a view that displays a login form. Complete the following:

  1. In your src/views folder, create a new view file called login.ejs.
  2. Add a form with the method set to POST and the action set to your login route
  3. Inside the form, add:
    • An input field for email with type="email" and name="email"
    • An input field for password with type="password" and name="password"
    • A submit button
  4. Add labels for each input field to make the form clear
Sample Code (click to expand)
<%- include('partials/header') %>
    <main>
        <h1><%= title %></h1>

        <form action="/login" method="POST">
            <div class="form-group">
                <label for="email">Email (Username)</label>
                <input
                    type="email"
                    id="email"
                    name="email"
                    required
                />
            </div>
            <div class="form-group">
                <label for="password">Password</label>
                <input
                    type="password"
                    id="password"
                    name="password"
                    required
                />
            </div>
            <button type="submit">Login</button>
        </form>
    </main>   

<%- include('partials/footer') %>

Stop here and test your login functionality

At this point, you should be able to log in to your application. There will not be any restrictions on accessing routes yet or even anything different in the user interface, but you should be able to see the flash message indicating success.

By taking a minute to verify this part of the process first, you can ensure that the basic login mechanism and password hashing features are working correctly before moving on to more complex features.

Add Variable for UI display

Many parts of the UI will display differently depending on whether a user is logged in or not. To make this easy for the Views, you will add a variable to the response object that indicates the login status of the user. This variable can then be used in your templates to render UI elements.

  1. Open you server.js file.
  2. Find the middleware function that made the NODE_ENV variable available on the response object. It should look something like this:
    app.use((req, res, next) => {
        res.locals.NODE_ENV = NODE_ENV;
        next();
    });
    
  3. Add code to this function to set a new variable res.locals.isLoggedIn that is true if req.session.user exists, otherwise false.
  4. The function should now look as follows:
    app.use((req, res, next) => {
        res.locals.isLoggedIn = false;
        if (req.session && req.session.user) {
            res.locals.isLoggedIn = true;
        }
    
        res.locals.NODE_ENV = NODE_ENV;
        next();
    });
    

Enhance Application with Login State

Now that you have login functionality working and a way to check if a user is logged in, you will update your navigation to display a login or logout button depending one whether the user is logged in or not.

  1. Open your header partial view file src/views/partials/header.ejs .
  2. Previously, you added a Register link to the navigation. Now, update as follows:
    • If the user is logged in: Include a link to the logout route (/logout)
    • If the user is not logged in: Include both the register link and a link to the login form (/login)
Sample Code (click to expand)
            <% if (isLoggedIn) { %>
                <li><a href="/register">Register</a></li>
                <li><a href="/login">Login</a></li>
            <% } else { %>
                <li><a href="/logout">Logout</a></li>
            <% } %>

Test Your Login Functionality

Now you need to test that everything works correctly. Complete the following:

  1. Start your server and navigate to the login page in your browser
  2. Try logging in with incorrect credentials and verify that you see an error message
  3. Log in with correct credentials and verify that:
    • You are redirected to the appropriate page
    • The navigation shows a logout link
  4. Navigate to different pages and make sure the session persists (you should continue to see the logout link).
  5. Log out and verify that the navigation shows login and register links
  6. Fix any errors you find

Testing Tip

Use your browser's developer tools to check the network tab and see what data is being sent when you submit the login form. You can also check the application tab to see session cookies.

Next Step

Complete the other Week 05 Learning Activities

After you have completed all the learning activities for this lesson, return to Canvas to submit a quiz.

Other Links: