« Previous Tutorial Next Tutorial »

Now that our users can generate a reset password hash and have it emailed to them, we need to give them the opportunity to enter a new password. This will require some additions on the front-end, and a new API route. For starters, we’re going to focus on the front-end side of things. I always like to see stuff first and then make it do something, when possible.

So let’s start by creating a new file in /src/components/account called ChangePasswordPage.jsx. This will hold our form. Let’s start with the imports:

import React from 'react';
import { AvForm, AvGroup, AvInput, AvFeedback } from 'availity-reactstrap-validation';
import { Button, Label } from 'reactstrap';

Then on line 5, we’ll define a class:

export default class ChangePasswordPage extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <div className="row justify-content-center">
        <div className="col-10 col-sm-7 col-md-5 col-lg-4">
        </div>
      </div>
    );
  }
}

This isn’t very interesting or functional, so let’s add more to our render block. Between lines 13 and 14, add the following code:

          <p>
            Please enter and confirm a new password below to change the
            password associated with this email address.
          </p>

          <AvForm onValidSubmit={this.handleValidSubmit}>

            <AvGroup>
              <Label for="password">Password</Label>
              <AvInput
                id="password"
                minLength="8"
                name="password"
                onChange={this.handleInputChange}
                onKeyPress={this.handleKeyPress}
                placeholder="password"
                required
                type="password"
                value={this.state.password}
              />
              <AvFeedback>Passwords must be at least eight characters in length</AvFeedback>
            </AvGroup>

            <AvGroup>
              <Label for="password">Confirm Password</Label>
              <AvInput
                id="passwordCheck"
                minLength="8"
                name="passwordCheck"
                onChange={this.handleInputChange}
                onKeyPress={this.handleKeyPress}
                placeholder="password again"
                required
                type="password"
                validate={{ match: { value: 'password' } }}
                value={this.state.passwordCheck}
              />
              <AvFeedback>Passwords must match</AvFeedback>
            </AvGroup>

            <Button color="primary">Change Password</Button>

          </AvForm>

As you can see, all we’re doing is adding a couple of form fields with some validation on them. Now we need some helper methods, so below line 8, add a padding line, and the following code:

  // Handle input changes
  handleInputChange(e) {
    this.setState({ [e.currentTarget.id]: e.target.value });
  }

  // catch enter clicks
  handleKeyPress(target) {
    if (target.charCode === 13) {
      target.preventDefault();
      this.handleValidSubmit();
    }
  }

  // Handle submission once all form data is valid
  handleValidSubmit() {
    const formData = this.state;
    // todo: handle submit
  }

Last but not least, we need to update our constructor with bound functions and a state object for our form fields. Add a padding line below line 7, and then add this code:

    // bound functions
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleKeyPress = this.handleKeyPress.bind(this);
    this.handleValidSubmit = this.handleValidSubmit.bind(this);

    // component state
    this.state = {
      password: '',
      passwordCheck: '',
    };

You should be all set. Your final code should look like this:

import React from 'react';
import { AvForm, AvGroup, AvInput, AvFeedback } from 'availity-reactstrap-validation';
import { Button, Label } from 'reactstrap';

export default class ChangePasswordPage extends React.Component {
  constructor(props) {
    super(props);

    // bound functions
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleKeyPress = this.handleKeyPress.bind(this);
    this.handleValidSubmit = this.handleValidSubmit.bind(this);

    // component state
    this.state = {
      password: '',
      passwordCheck: '',
    };
  }

  // Handle input changes
  handleInputChange(e) {
    this.setState({ [e.currentTarget.id]: e.target.value });
  }

  // catch enter clicks
  handleKeyPress(target) {
    if (target.charCode === 13) {
      target.preventDefault();
      this.handleValidSubmit();
    }
  }

  // Handle submission once all form data is valid
  handleValidSubmit() {
    const formData = this.state;
    // todo: handle submit
  }

  render() {
    return (
      <div className="row justify-content-center">
        <div className="col-10 col-sm-7 col-md-5 col-lg-4">
          <p>
            Please enter and confirm a new password below to change the
            password associated with this email address.
          </p>

          <AvForm onValidSubmit={this.handleValidSubmit}>

            <AvGroup>
              <Label for="password">Password</Label>
              <AvInput
                id="password"
                minLength="8"
                name="password"
                onChange={this.handleInputChange}
                onKeyPress={this.handleKeyPress}
                placeholder="password"
                required
                type="password"
                value={this.state.password}
              />
              <AvFeedback>Passwords must be at least eight characters in length</AvFeedback>
            </AvGroup>

            <AvGroup>
              <Label for="password">Confirm Password</Label>
              <AvInput
                id="passwordCheck"
                minLength="8"
                name="passwordCheck"
                onChange={this.handleInputChange}
                onKeyPress={this.handleKeyPress}
                placeholder="password again"
                required
                type="password"
                validate={{ match: { value: 'password' } }}
                value={this.state.passwordCheck}
              />
              <AvFeedback>Passwords must match</AvFeedback>
            </AvGroup>

            <Button color="primary">Change Password</Button>

          </AvForm>
        </div>
      </div>
    );
  }
}

Save your file, and create a new one in /src/components/account/ called ChangePasswordPageContainer.jsx. This one’s a lot shorter right now, since we’re not going to handle connecting to the API in this tutorial, so here’s all of it:

import React from 'react';
import { connect } from 'react-redux';

import ChangePasswordPage from './ChangePasswordPage';

export class ChangePasswordPageContainer extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    const { hash } = this.props.match.params;
    return (
      <ChangePasswordPage hash={hash} />
    );
  }
}

export default connect()(ChangePasswordPageContainer);

ESLint will complain about a useless class, here, but don’t worry – in the next tutorial we’ll be making it useful. For now, we’re set here, so go ahead and save this file too. Then open up /src/Template.jsx. We need to add a route so we can see our new page. First, above line 4, add the following:

import ChangePasswordPage from './account/ChangePasswordPageContainer';

And then, below line 22, add this line:

          <Route path="/account/change-password/:hash" component={ChangePasswordPage} />

This will populate that “hash” parameter we’re referencing on line 12 of ChangePasswordPageContainer.jsx, and which we’ll eventually send to the API to determine which user we’re updating.

Save this file and we’re done with the front-end for now. You can fire up a browser and head for localhost:3000/account/change-password, and see the page. OR CAN YOU?! No, you can’t, because it only shows up if you provide it a hash. For now, you can use any random gibberish, like localhost:3000/account/change-password/asdfghjk and you’ll be able to see the form, and confirm that its validation is working.

We’ve still got some time, so let’s build our API endpoint. Open up /routes/api/authentication.js. We need a routine that compares the hash against the database in order to determine which user to update, and then does so … assuming it finds a user that matches the hash, anyway. Underneath line 82, add a padding line, and then here’s the code:

// POST to savepassword
router.post('/savepassword', async (req, res) => {
  let result;
  try {
    // look up user in the DB based on reset hash
    const query = User.findOne({ passwordReset: req.body.hash });
    const foundUser = await query.exec();

    // If the user exists save their new password
    if (foundUser) {
      // user passport's built-in password set method
      foundUser.setPassword(req.body.password, (err) => {
        if (err) {
          result = res.send(JSON.stringify({ error: 'Password could not be saved. Please try again' }));
        } else {
          // once the password's set, save the user object
          foundUser.save((error) => {
            if (error) {
              result = res.send(JSON.stringify({ error: 'Password could not be saved. Please try again' }));
            } else {
              // Send a success message
              result = res.send(JSON.stringify({ success: true }));
            }
          });
        }
      });
    }
  } catch (err) {
    // if the hash didn't bring up a user, error out
    result = res.send(JSON.stringify({ error: 'Reset hash not found in database' }));
  }
  return result;
});

As you can see, this does a lookup based on the hash. If it finds a user, it moves on. If not, it gives an error. Assuming a user is found, it uses Passport’s built-in setPassword method, which will generate a new hash and salt and all that for the user’s password (remember: we don’t ever store plain-text passwords). Assuming that doesn’t bomb out—which it really never should, but better safe than sorry—we save the user to the database with its new password and, again, barring any DB errors, pass on some success JSON.

OK, save this file and we’ve got our saving routine. In the next tutorial, we’ll get into the actions and reducers necessary to wire it up. Until then!

« Previous Tutorial Next Tutorial »