Frosty CMS: Flash Messages 🧨

Intro

Continuing our series on Frosty CMS, where we are building a blog CMS from scratch with Node.js, Express, MongoDB, Mongoose, EJS and Passport. In this session we will be adding flash messages to notify the user about various things as they interact with the application. For example why they got re-routed. Why their sign-up form failed (‘Email has already been registered’), etc etc.

npm connect-flash

Flash messages were originally native to Express 2.x but they were extracted. We will be using a package called connect-flash to bring them back. Here is a quick explanation of flash from the npm connect-flash page.

The flash is a special area of the session used for storing messages. Messages are written to the flash and cleared after being displayed to the user. The flash is typically used in combination with redirects, ensuring that the message is available to the next page that is to be rendered. -connect-flash npm page

Adding Message Variable to Navbar

To make it so that we don’t have to add our message to every page template, we can add our messages to the bottom of the navbar.

<!-- flash messages -->
<h1><%= message %></h1>

Make Message Variable Globally Available

Similar to our currentUser variable, we need to make this message variable globally available.

// ***************************
// GLOBAL VARIABLES
// ***************************

// pass through user data on every route
app.use((req,res,next) => {
    res.locals.currentUser = req.user;
    res.locals.message = req.flash('flashName');
    next();
});

So we add that to our app.js and all of our route files.

Adding Flash Messages to Routes

Let’s start with a simple message that just confirms for the user that they have been logged out. We go to our logout route:

// logout user
router.get("/logout",(req, res) => {
    req.logout();
    res.redirect("/");
});

and we add a flash message so that we get the following:

// logout user
router.get("/logout",(req, res) => {
    req.logout();
    req.flash('flashName', 'You have been logged out.');
    res.redirect("/");
});

Note that the ID flashName matches the global variable that we defined above. So what we have done here is said that, when we get a request for flash named flashName, that is automatically stored in the variable message. So the variable message is equal to ‘You have been logged out.

you have been logged out

Styling

Now we just wrap our message in a couple Bootstrap classes and if statement to only display the message if it’s not empty.

<!-- flash messages -->
<% if(message != ""){ %>
  <div class='container-lg mt-4'>
    <div class="alert alert-primary" role="alert">
      <%= message %>
    </div>
  </div>
<% } %>

bootstrap styled flash message

Multiple Alerts

So far this works great if we have just one alert. But we want to have several different alerts, a green for success and a red for failure. To do this we basically repeat this process but we just add multiple flash variables this time.

// ***************************
// GLOBAL VARIABLES
// ***************************

// pass through user data on every route
app.use((req,res,next) => {
    res.locals.currentUser = req.user;
    res.locals.error = req.flash('error');
    res.locals.success = req.flash('success');
    next();
});

So now our keys are error and success, and our variables are also error and success.

Now if we go back to our route for retrieving user profiles we have a function that runs to check if the user profile still exists.

// user profile page
router.get("/:id/profile",middleware.isLoggedIn, (req, res) => {
    
    function checkProfileExists(requestedProfileID){
        return new Promise((resolve,reject)=>{
           console.log("Checking that Profile of User " + requestedProfileID + "still exists");
           User.findById(requestedProfileID, (err, foundProfile) =>{
               if(err){
                   console.log(err);
               } else if(!foundProfile) {
                   req.flash('error', 'Profile does not exist.');
                   res.redirect('back');
                   // redirect does not end statement like res.render
                   // so we must return to end process
                   return;
               } else {
                   console.log(foundProfile);
                   resolve(true);
               }
           });
        });
    }

And we can see that we have replaced our resolve with a redirect and a flash error that the profile doesn’t exist. Then if we try to click on the link to the user profile user1 (who no longer exists) we can see that we get a nice error message.

Fixed Position Flash Messages

And because the previous error messages showing below the navbar were messing with our settings menu I went ahead and made them a fixed overlay to the navbar.

<!-- flash messages -->
<% if(success != ""){ %>
  <div class='alert-wrapper'>
    <div class="alert alert-success alert-fixed" role="alert">
      <%= success %>
    </div>
  </div> 
<% } %>
<% if(error != ""){ %>
  <div class='alert-wrapper'>
    <div class="alert alert-danger alert-fixed" role="alert">
      <%= error %>
    </div>
  </div> 
<% } %>
/* Alert Wrapper to center alert*/
.alert-wrapper {
  position: absolute;
  top: 0px; 
  left: 50%;
  z-index:9999;
}

/* Alert Box*/
.alert-fixed {
    position: relative; 
    left: -50%;
    border-radius:0px
}

Passport Registration Flash Messages

This one is a bit of a special case because Passport already has written all of it’s own error messages. If we want to use the default error messages we can simply pass in the err object, which looks like this in the console.

{ UserExistsError: A user with the given username is already registered
    at Promise.resolve.then.then.then.existingUser (/home/ubuntu/environment/node_modules/passport-local-mongoose/index.js:237:17)
    at process._tickCallback (internal/process/next_tick.js:68:7)
  name: 'UserExistsError',
  message: 'A user with the given username is already registered' }

Since this is an object we can’t pass the whole object into our template, we just want to pass the message. So we actually need to pass in err.message.

User.register(newUser, req.body.password, (err, user) => {
                console.log("attempting user registration");
                if (err) {
                    console.log(err);
                    // populating flash with default messages from passport
                    req.flash('error', err.message);
                    res.redirect('back');
                    return;
                }

                passport.authenticate("local")(req, res, () => {
                    req.flash('success', 'New user registered.');
                    res.redirect("/");
                    return;
                });
                console.log("user registration successful: " + newUser.username);
            });

a user with the given username is already registered message

Passport Login Flash Messages

These are a special case where you need to check the documentation. You need to set an option of failureFlash: true in the login route.

// login user: authenticate user
// app.post("/login", middleware, callback)
router.post("/login",passport.authenticate("local", {
                                                    successRedirect: "..",
                                                    failureRedirect: "/login",
                                                    failureFlash: true
                                                }), (req, res) => {}
);

And of course have a place for that to land on the form.

<% if(error != '') {%>
    <div class="alert alert-danger" role="alert">
        <%= error %>
    </div>
    <% } %>

password or username is incorrect message