« Previous Tutorial Next Tutorial »

Now that we have our front-end validation in place, let’s do a little work on the back-end to make sure we’re not saving multiple users with the same email address. We also need to tighten up our error reporting a little bit, because it’s currently possible for our error box to throw errors of its own when faced with malformed data.

Let’s jump in. Open /routes/api/authentication.js. Find our register function starting on line 45. Change line 46 to the following:

router.post('/register', async (req, res) => {

And then, just below that, add the following:

  // First, check and make sure the email doesn't already exist
  const query = User.findOne({ email: req.body.email });
  const foundUser = await query.exec();
  if (foundUser) { return res.send(JSON.stringify({ error: 'Email or username already exists' })); }

What we’re doing here should be obvious: we’re making the function async so we can hit the database and wait on the result before proceeding. If the result is that the query finds a user, we exit out of the function while sending an error. You may notice that we’re not specifically saying whether the email or username produced a match. This is a security feature meant to dissuade potential abusers from just spamming the registration form with email addresses until they find one that already exists in the DB, and then making hack attempts against it. It’s the same reason our login form doesn’t specify whether the email or password was incorrect on a failed login. It’s not a perfect gateway, but it at least acts as something of a barrier.

Now let’s only proceed if we haven’t found a user. We’re going to wrap the rest of the function in an if statement, so above line 53, add this code:

  if (!foundUser) {

Then change the indentation on lines 54 to 71, and below line 71, add a closing brace. Async functions should always return a value, so below that, add a spacing line, and then add this code:

  // return an error if all else fails
  return res.send(JSON.stringify({ error: 'There was an error registering the user' }));

Last but not least, add a return in front of line 57, like this:

    return User.register(newUser, req.body.password, (err) => {

and change line 69, which has the wrong error message (this is why copy-paste is often not a good idea, kids!). Here’s the correct code:

        return res.send(JSON.stringify({ error: 'There was an error registering the user' }));

We’re good to go here. Save the file, and let’s fix some error issues. One thing you may have noticed is that the error box never goes away unless the user clicks the close icon. This can lead to confusing things like failing a login (bringing up the error box), then succeeding, and still having the error box floating there telling you the login failed. That’s what we in the business like to call “really bad UX.” We’re going to fix that by calling our clearError action at certain times when running other actions. We’re also going to massage some of the errors sent by our registration function.

To start, open up /src/actions/authentication.js and below line 2, import our clearError action like this:

import { clearError } from './error';

Then change line 11 to send an error as part of the action by changing the code to this:

export const registrationFailure = error => ({ type: 'AUTHENTICATION_REGISTRATION_FAILURE', error });

Now head for our logUserIn function. This is one place where we’re going to clear the error if fired. So below line 48, add this code:

    // clear the error box if it's displayed
    dispatch(clearError());

Dispatching this action even if the error box isn’t actually visible is not a big deal. It’s not a measurable performance hit, and has no real impact, so it’s fine to do it every time a login is attempted.

Scroll to the logUserOut function and do the same thing, adding the exact same code underneath line 96. Then do it again for the registerUser function, underneath line 131.

Good, now we need to make some adjustments to account for different errors that may be coming in. Passport returns a JSON packet if a username is already taken, so line 159, which checks for the existence of returned JSON, isn’t enough to detect success or failure. Therefore, change it to the following:

      if (json && json.username) {

This way we can tell whether the incoming JSON is a packet of user data, which would indicate a successful registration, or an error message. Unfortunately, we can’t use Passport’s default error message because it specifies that it was the username that already exists, and we want to be hazier than that, so change line 163 to the following:

        dispatch(registrationFailure(new Error(json.error.message ? 'Email or username already exists' : json.error)));

This is just replacing Passport’s error message, if it exists, or otherwise sending along whatever error is coming back from the server (all of which we defined ourselves in our API file).

Finally, change line 167 to the following:

      dispatch(registrationFailure(new Error(error.message || 'Registration Failed. Please try again.')));

This is just going “If there’s an existing message, send it along. Otherwise, add a message.”

We’re set, so save the file and let’s move on to /src/components/shared/ErrorBox.jsx. We’ve only got a single line to change here. It’s line 11, and when you’re done it should look like this:

          <strong>Error:</strong> {error && error.message ? error.message : 'An undefined error occurred' }

This makes sure that even in the event of a malformed error, the user gets something back, albeit a message that’s about as helpful as Apple’s famous bomb icon. Oh well. One can only spend so much time worrying about edge cases.

Save the file, and let’s do a bit of testing. Switch over to a browser, make sure you’re logged out, and then hit the registration page. You’ll need to hard-refresh because we changed action files, which don’t hot-reload. First, try creating a user with an email address that’s already in use. Hooray! We’re properly catching that issue. Now use a new email, but an existing username. Again, we catch it. Finally, switch over to the log-in page, type some garbage, bring up our “login failed” error, and then use a successful login, and watch the error disappear.

Cool, we’re looking pretty good here. Next time, we’ll build a quick “reset password” page and wire it up, including emailing the user a link to complete the reset. See you there!

« Previous Tutorial Next Tutorial »