Headless WordPress: Password Reset with ReactJS & WPGraphQL

Previously I demonstrated how we can log in to a WordPress site from a headless app using cookies, and how to handle logging out—this also covered setting up Apollo Client with React. In this post I’ll show how to build a password reset form!

Required GraphQL mutations

Unlike login and logout, WP GraphQL actually provides the mutations we require out of the box.

  • Send Password Reset Email – This mutation requires a user name or email address and will trigger a password reset request to be sent to the user.
  • Reset User Password – This mutation takes input of a username, the password reset key, and a new password. It sets the new password for the given user.

To see these mutations and others available to you, within WordPress admin you can head to GraphQL > GraphQL IDE.

These are the mutations we will use

Custom hooks

To interact with these mutations I created some custom React hooks. Note that I’m using Apollo Client to query the GraphQL API which I covered in my previous post.

useSendPasswordResetEmailMutation

This hook provides a sendPasswordResetEmail function. It takes a single parameter, username (which can also be an email address). It returns a promise allowing you to do something once the reset email has been sent.

import { gql, useMutation } from '@apollo/client';

const QUERY = gql`
	mutation SendPasswordResetEmail($username: String!) {
		sendPasswordResetEmail(input: { username: $username }) {
			clientMutationId
		}
	}
`;

export const useSendPasswordResetEmailMutation = () => {
	const [ mutation, mutationResults ] = useMutation( QUERY );

	const sendPasswordResetEmail = ( username ) => {
		return mutation( {
			variables: {
				username,
			},
		} );
	};

	return { sendPasswordResetEmail, results: mutationResults };
};Code language: JavaScript (javascript)

useResetUserPasswordMutation

We also need a custom hook to set a new password. This is used after the user has clicked the link in the password reset email.

My implementation of the custom hook returns a function that expects to be passed the login, a new password, and the “key” given to the user. Once validated, the user password will be updated, otherwise the promise will throw an error.

import { gql, useMutation } from '@apollo/client';

const QUERY = gql`
	mutation ResetUserPassword(
		$key: String!
		$login: String!
		$password: String!
	) {
		resetUserPassword(
			input: { key: $key, login: $login, password: $password }
		) {
			clientMutationId
		}
	}
`;

export const useResetUserPasswordMutation = () => {
	const [ mutation, mutationResults ] = useMutation( QUERY );

	const resetUserPassword = ( key, login, password ) => {
		return mutation( {
			variables: {
				key,
				login,
				password,
			},
		} );
	};

	return { resetUserPassword, results: mutationResults };
};Code language: JavaScript (javascript)

These hooks would be used as follows:

const { sendPasswordResetEmail } = useSendPasswordResetEmailMutation();
const { resetUserPassword } = useResetUserPasswordMutation();

// You would use these within an event handler or callback! It returns a promise.
sendPasswordResetEmail( 'test@email.com' );
resetUserPassword( key, 'test@email.com', 'newpassword' );Code language: JavaScript (javascript)

Whilst these mutations can be used directly, in my app I combined them into a single custom hook which handles status and errors. You can see this code on GitHub here (the useResetPassword hook).

Fixing the Password Reset Email

One gotcha I encountered was that the password reset email sent by WordPress links to the actual WordPress domain, rather than the headless site. We need to edit the email from WordPress to fix this—annoyingly the only way to do this is to replace the entire contents of the email, but you can customise this as much as you want which is a bonus.

add_filter( 'retrieve_password_message', function( $message, $key, $user_login, $user_data ) {
	$site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
	$message   = __( 'Someone has requested a password reset for the following account:' ) . "\r\n\r\n";
	/* translators: %s: Site name. */
	$message .= sprintf( __( 'Site Name: %s', 'morrics-magical-cauldron' ), $site_name ) . "\r\n\r\n";
	/* translators: %s: User login. */
	$message .= sprintf( __( 'Username: %s', 'morrics-magical-cauldron' ), $user_login ) . "\r\n\r\n";
	$message .= __( 'If this was a mistake, ignore this email and nothing will happen.' ) . "\r\n\r\n";
	$message .= __( 'To reset your password, visit the following address:' ) . "\r\n\r\n";
	$message .= HEADLESS_FRONTEND_URL . '/account/reset/' . rawurlencode( $user_login ) . "/$key\r\n\r\n";
	$requester_ip = $_SERVER['REMOTE_ADDR'];
	if ( $requester_ip ) {
		$message .= sprintf(
		/* translators: %s: IP address of password reset requester. */
			__( 'This password reset request originated from the IP address %s.' ),
			$requester_ip
		) . "\r\n";
	}
	return $message;
}, 10, 4 );
Code language: PHP (php)

With the highlighted change in place, the password reset email will link to whatever URL you provide. In my example I am linking through to morrics-magical-cauldron.netlify.app/account/reset/<login>/<key>.

On the app side, I am mapping this URL to my reset password form component using Reach Router, but you could also use React Router or any other routing library. You just need to ensure your app can read the user login and key from the current URL, or query-string.

Forms

Now we have GraphQL setup on the server, some hooks to handle queries to the GraphQL API from the app, and we’ve fixed the password reset URL, we can build some form components to collect user data and trigger the events!

The following two form examples are just basic examples and are not complete, but should give you an idea of how the custom hooks work. You’ll still need to build in error handling and status messages for the final version!

Send Password Reset Email Form

export const ResetForm = () => {
	const [ username, setUsername ] = React.useState( '' );
	const { sendPasswordResetEmail } = sendPasswordResetEmail();

	const onReset = ( e ) => {
		e.preventDefault();
		sendResetPasswordEmail( username );
	};

	return (
		<form onSubmit={ onReset }>
			<label>Username or Email Address</label>
			<input type="text" value={ username } onChange={ ( value ) => setUsername( value ) } />
			<button onClick={ onReset }>Reset</button>
		</form>
	);
};Code language: JavaScript (javascript)

Reset Password Form

// resetLogin and resetKey come from the querystring/page URL and are passed to this component as props.
export const ResetPasswordForm = ( { resetLogin, resetKey } ) => {
	const [ password, setPassword ] = useState( '' );
	const { resetUserPassword } = useResetUserPasswordMutation();

	const onReset = ( e ) => {
		e.preventDefault();
		resetUserPassword( resetLogin, resetKey, password );
	};

	return (
		<form onSubmit={ onReset }>
			<label>Enter new password</label>
			<input type="password" value={ password } onChange={ ( value ) => setPassword( value ) } />
			<button onClick={ onReset }>Set New Password</button>
		</form>
	);
};Code language: JavaScript (javascript)

You can view my Reset Password Form component here, and my lost password form here. My final versions handle errors from the API, and show the user the status of their request.

Wrapping up

You can see a working example of the password reset flow in my D&D app here, and the source code is also public on GitHub.

It’s great that WP GraphQL handles password reset functionality by default. As I said in my last post, Apollo Client really makes the process of interacting with GraphQL simple, and (although I didn’t show examples) Reach Router is a fantastic routing library for sending users to the correct page of your app and accessing the query string. I highly recommend both!

  1. Headless WordPress: Cookie Based Login using GraphQL
  2. Headless WordPress: Log-out using GraphQL & ReactJS
  3. Headless WordPress: Password Reset with ReactJS & WPGraphQL
  4. Headless WordPress: User Registration with ReactJS & WPGraphQL

Posted

in

by

%d bloggers like this: