16/ Forgot password flow with Strapi and NextAuth
Tags: nextauth, strapi, nextjs
Published: 2024-04-01 16:34:58

What do we need to do?

1. Make a page where the user requests a password reset. The user should enter the email.
2. Add link to this page.
3. Send an email to the email address with a token.
4. Make a page where the user sets a new password.

Note: we again won't be using `NextAuth` because we don't need it.

Note 2: we will ensure that the user can only request a password reset when the user is signed out.

The code for this chapter is available on [github, branch forgotpassword](https://github.com/peterlidee/NNAS/tree/forgotpassword).

## 1. Request password reset

This is very similar to the request email confirmation page that we created in the previous chapter. This is the `Strapi` endpoint:

```jsx
const strapiResponse: any = await fetch(
  process.env.STRAPI_BACKEND_URL + '/api/auth/forgot-password',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email }),
    cache: 'no-cache',
  }
);
```

On a side note. Where do all these endpoints come from? `Strapi` is open source. We can read the source code. All these endpoint come from the Users and permissions plugin. So, if we go to `Strapi` on github and browse around the files a bit eventually you will find the [auth.js](https://github.com/strapi/strapi/blob/develop/packages/plugins/users-permissions/server/routes/content-api/auth.js) file that contains all of the routes. You can also find the [Strapi controllers](https://github.com/strapi/strapi/blob/develop/packages/plugins/users-permissions/server/controllers/auth.js) in there if you're interested.

Let's create a page, a component and a server action:

```jsx
// frontend/scr/app/(auth)/password/requestreset/page.tsx

import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';
import RequestPasswordReset from '@/components/auth/password/RequestPasswordReset';
import { authOptions } from '@/app/api/auth/[...nextauth]/authOptions';

export default async function RequestResetPage() {
  const session = await getServerSession(authOptions);
  if (session) redirect('/account');
  return <RequestPasswordReset />;
}
```

Note that we guard this page. If the user is logged in, we redirect him to `/account`. We will be building this page later on. Why do we do this at page level? Because I got some kind of error when I tried to do it in the actual component: `Warning: Cannot update a component ('Router') while rendering a different component`.

In our server action, we validate the formData with `Zod`, make the request to `strapi` and handle errors. In this case, we won't redirect on success but return a success object: `{ error: false, message: 'Success' }`. We will handle this success in our form components. This is our `requestPasswordResetAction`:

```jsx
// frontend/src/components/auth/password/requestPasswordResetAction.ts
'use server';

import { z } from 'zod';
import { RequestPasswordResetFormStateT } from './RequestPasswordReset';

const formSchema = z.object({
  email: z.string().email('Enter a valid email.').trim(),
});

export default async function requestPasswordResetAction(
  prevState: RequestPasswordResetFormStateT,
  formData: FormData
) {
  const validatedFields = formSchema.safeParse({
    email: formData.get('email'),
  });
  if (!validatedFields.success) {
    return {
      error: true,
      message: 'Please verify your data.',
      fieldErrors: validatedFields.error.flatten().fieldErrors,
    };
  }
  const { email } = validatedFields.data;

  try {
    const strapiResponse: any = await fetch(
      process.env.STRAPI_BACKEND_URL + '/api/auth/forgot-password',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email }),
        cache: 'no-cache',
      }
    );
    const data = await strapiResponse.json();

    // handle strapi error
    if (!strapiResponse.ok) {
      const response = {
        error: true,
        message: '',
      };
      // check if response in json-able
      const contentType = strapiResponse.headers.get('content-type');
      if (contentType === 'application/json; charset=utf-8') {
        const data = await strapiResponse.json();
        response.message = data.error.message;
      } else {
        response.message = strapiResponse.statusText;
      }
      return response;
    }

    // we do handle success here, we do not use a redirect!!
    return {
      error: false,
      message: 'Success',
    };
  } catch (error: any) {
    // network error or something
    return {
      error: true,
      message: 'message' in error ? error.message : error.statusText,
    };
  }
}
```

Finally, in our actual form component, we listen for success return in our `useFormState` state and display a success message. Else, we return the form. Also note that we updated our Types to account for the possible success object. The rest should be clear.

![request password reset](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5zwmwf0cvhilshb4n81o.png)

```jsx
'use client';

import { useFormState } from 'react-dom';
import PendingSubmitButton from '../PendingSubmitButton';
import requestPasswordResetAction from './requestPasswordResetAction';

type InputErrorsT = {
  email?: string[];
};
type NoErrorFormStateT = {
  error: false;
  message?: string;
};
type ErrorFormStateT = {
  error: true;
  message: string;
  inputErrors?: InputErrorsT;
};

export type RequestPasswordResetFormStateT =
  | NoErrorFormStateT
  | ErrorFormStateT;

const initialState: NoErrorFormStateT = {
  error: false,
};

export default function ForgotPassword() {
  const [state, formAction] = useFormState<
    RequestPasswordResetFormStateT,
    FormData
  >(requestPasswordResetAction, initialState);

  if (!state.error && state.message === 'Success') {
    return (
      <div className='bg-zinc-100 rounded-sm px-4 py-8 mb-8'>
        <h2 className='font-bold text-lg mb-4'>Check your email</h2>
        <p>
          We sent you an email with a link. Open this link to reset your
          password. Careful, expires ...
        </p>
      </div>
    );
  }

  return (
    <div className='mx-auto my-8 p-8 max-w-lg bg-zinc-100 rounded-sm'>
      <h2 className='text-center text-2xl text-blue-400 mb-8 font-bold'>
        Request a password reset
      </h2>
      <p className='mb-4'>
        Forgot your password? Enter your account email here and we will send you
        a link you can use to reset your password.
      </p>
      <form action={formAction} className='my-8'>
        <div className='mb-3'>
          <label htmlFor='email' className='block mb-1'>
            Email *
          </label>
          <input
            type='email'
            id='email'
            name='email'
            required
            className='bg-white border border-zinc-300 w-full rounded-sm p-2'
          />
          {state.error && state?.inputErrors?.email ? (
            <div className='text-red-700' aria-live='polite'>
              {state.inputErrors.email[0]}
            </div>
          ) : null}
        </div>
        <div className='mb-3'>
          <PendingSubmitButton />
        </div>
        {state.error && state.message ? (
          <div className='text-red-700' aria-live='polite'>
            {state.message}
          </div>
        ) : null}
      </form>
    </div>
  );
}
```

## 2. Add a link to request password reset

Our signed out user needs to be able to go to the page that we just created. We keep it simple and add a _forgot password_ link next to the submit button on the _sign in_ form.

![link to forgot password page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dojnezthab0u3fajf176.png)

## 3. Send a forgot password email from Strapi

To send our mail, we first have to add a setting

```
Settings > Users & Permissions plugin > Advanced settings > Reset password page
```

We set this field to `http://localhost:3000/password/reset`.

Then, we need to update the `Strapi` email template:

```
Settings > Users & Permissions plugin > Email templates > Reset password
```

You should alter the shipper name, email and subject. The body of the mail looks like this by default

```html
<p>We heard that you lost your password. Sorry about that!</p>
<p>But don’t worry! You can use the following link to reset your password:</p>
<p><%= URL %>?code=<%= TOKEN %></p>
<p>Thanks.</p>
```

Where `<%= URL %>?code=<%= TOKEN %>` resolves to `http://localhost:3000/password/reset?code=***somecode***`, as we would expect. We only need to update this line so it actually becomes a link:

```html
<p><a href="<%= URL %>?code=<%= TOKEN %>">Reset password link</a></p>
```

And save.

## 4. Build the password reset page

Now we need to actually build this page. To reset a password, we need a form with 2 fields: password and confirm password. But, the `Strapi` endpoint requires 3 values: password, confirm password + the token (code searchParam). On success, `Strapi` will then return a user object + a new `Strapi` token. We will deal with this later. Let's first build the page:

```jsx
// frontend/src/app/(auth)/password/reset/page.tsx

import { authOptions } from '@/app/api/auth/[...nextauth]/authOptions';
import { getServerSession } from 'next-auth';
import ResetPassword from '@/components/auth/password/ResetPassword';
import { redirect } from 'next/navigation';

type Props = {
  searchParams: {
    code?: string,
  },
};

export default async function page({ searchParams }: Props) {
  // if the user is logged in, redirect to account where password change is possible
  const session = await getServerSession(authOptions);
  if (session) redirect('/account');
  return <ResetPassword code={searchParams.code} />;
}
```

Note that we don't let signed in users access this page and that we pass the code (a reset password token) to the actual component.

We will be using a server action that returns a success or error object but does not redirect. But, this leads to an immediate problem. How do we pass the code prop from our form component to our server action? This is easy to solve. We just put it into our initial `useFormState` state:

```jsx
const initialState{
    error: false,
    code: code || '',
  };
const [state, formAction] = useFormState(
  resetPasswordAction,
  initialState
);
```

Our server action, `resetPasswordAction` then has access to this via it's prevState argument:

```jsx
export default async function resetPasswordAction(prevState, formData) {
  // make strapi request passing prevState.code
}
```

But, this leads to another problem. Suppose the user mistypes and enters a wrong confirm your password value. `Strapi` will detect this an return an error object. We catch this error in our server action (`!strapiResponse.ok`) and then from our server action return an error object to our form.

The `useFormState` state equals the return value from the server action. Where at this point is our code value? It is gone, unless we return it from our server action. If we return this from our server action:

```js
return {
  error: true,
  message: 'something is wrong',
  code: prevState.code,
};
```

Then the state that lives in our form will be reset with this code value.

Imagine we didn't return code from our server action. Our user just got the error message: _passwords don't match_. He fixes this error and resubmits the form. The form calls formAction. `useFormState` catches this call and calls `resetPasswordAction` with it's state and the formData. What is the state at that point: `{ error: true, message: 'something is wrong' }`. (no code property)

Our server action goes to work and tries to call `Strapi`. From the formData we take password + confirm password. But, `Strapi` also expects the code but we can't give it. It no longer is inside state! And `Strapi` will error out: _need the code_.

So, we need to pass the code back from our server action whenever we return an error. When there is no error, the form won't be called again so we don't need it anymore. But, we do need it on **every** error we return! Including f.e. the `Zod` errors. Hopefully this makes sense.

On to the form component:

```jsx
// frontend/src/components/auth/password/resetPassword.tsx

'use client';

import { useFormState } from 'react-dom';
import resetPasswordAction from './resetPasswordAction';
import Link from 'next/link';
import PendingSubmitButton from '../PendingSubmitButton';

type Props = {
  code: string | undefined;
};
type InputErrorsT = {
  password?: string[];
  passwordConfirmation?: string[];
};
export type ResetPasswordFormStateT = {
  error: boolean;
  message?: string;
  inputErrors?: InputErrorsT;
  code?: string;
};

export default function ResetPassword({ code }: Props) {
  const initialState: ResetPasswordFormStateT = {
    error: false,
    code: code || '',
  };
  const [state, formAction] = useFormState<ResetPasswordFormStateT, FormData>(
    resetPasswordAction,
    initialState
  );

  if (!code) return <p>Error, please use the link we mailed you.</p>;
  if (!state.error && 'message' in state && state.message === 'Success') {
    return (
      <div className='bg-zinc-100 rounded-sm px-4 py-8 mb-8'>
        <h2 className='font-bold text-lg mb-4'>Password was reset</h2>
        <p>
          Your password was reset. You can now{' '}
          <Link href='/signin' className='underline'>
            sign in
          </Link>{' '}
          with your new password.
        </p>
      </div>
    );
  }

  return (
    <div className='mx-auto my-8 p-8 max-w-lg bg-zinc-100 rounded-sm'>
      <h2 className='text-center text-2xl text-blue-400 mb-8 font-bold'>
        Reset your password
      </h2>
      <p className='mb-4'>
        To reset your password, enter your new password, confirm it by entering
        it again and then click send.
      </p>
      <form action={formAction} className='my-8'>
        <div className='mb-3'>
          <label htmlFor='password' className='block mb-1'>
            Password *
          </label>
          <input
            type='password'
            id='password'
            name='password'
            required
            className='bg-white border border-zinc-300 w-full rounded-sm p-2'
          />
          {state.error && state?.inputErrors?.password ? (
            <div className='text-red-700' aria-live='polite'>
              {state.inputErrors.password[0]}
            </div>
          ) : null}
        </div>
        <div className='mb-3'>
          <label htmlFor='passwordConfirmation' className='block mb-1'>
            confirm your password *
          </label>
          <input
            type='password'
            id='passwordConfirmation'
            name='passwordConfirmation'
            required
            className='bg-white border border-zinc-300 w-full rounded-sm p-2'
          />
          {state.error && state?.inputErrors?.passwordConfirmation ? (
            <div className='text-red-700' aria-live='polite'>
              {state.inputErrors.passwordConfirmation[0]}
            </div>
          ) : null}
        </div>
        <div className='mb-3'>
          <PendingSubmitButton />
        </div>
        {state.error && state.message ? (
          <div className='text-red-700' aria-live='polite'>
            Error: {state.message}
          </div>
        ) : null}
      </form>
    </div>
  );
}
```

There is a couple of things to note. We check it there is a code (the token) and if state holds a success message. If not, we display the form. The only other thing that differs here from earlier forms is that we didn't use a discriminated union Type this time. It proved difficult with the code property. So we opted for a simpler Type where most properties are optional. This is works and is correct just not as specific as it could be.

Our server action:

```jsx
// frontend/src/component/auth/password/resetPasswordAction.ts

'use server';

import { z } from 'zod';
import { ResetPasswordFormStateT } from './ResetPassword';
import { StrapiErrorT } from '@/types/strapi/StrapiError';

const formSchema = z.object({
  password: z.string().min(6).max(30).trim(),
  passwordConfirmation: z.string().min(6).max(30).trim(),
});

export default async function resetPasswordAction(
  prevState: ResetPasswordFormStateT,
  formData: FormData
) {
  const validatedFields = formSchema.safeParse({
    password: formData.get('password'),
    passwordConfirmation: formData.get('passwordConfirmation'),
  });
  if (!validatedFields.success) {
    return {
      error: true,
      message: 'Please verify your data.',
      inputErrors: validatedFields.error.flatten().fieldErrors,
      code: prevState.code,
    };
  }
  const { password, passwordConfirmation } = validatedFields.data;

  try {
    const strapiResponse: any = await fetch(
      process.env.STRAPI_BACKEND_URL + '/api/auth/reset-password',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          password,
          passwordConfirmation,
          code: prevState.code,
        }),
        cache: 'no-cache',
      }
    );

    // handle strapi error
    if (!strapiResponse.ok) {
      const response = {
        error: true,
        message: '',
        code: prevState.code,
      };
      // check if response in json-able
      const contentType = strapiResponse.headers.get('content-type');
      if (contentType === 'application/json; charset=utf-8') {
        const data: StrapiErrorT = await strapiResponse.json();
        response.message = data.error.message;
      } else {
        response.message = strapiResponse.statusText;
      }
      return response;
    }

    // success
    // no need to pass code anymore
    return {
      error: false,
      message: 'Success',
    };
  } catch (error: any) {
    return {
      error: true,
      message: 'message' in error ? error.message : error.statusText,
      code: prevState.code,
    };
  }
}
```

This should all make sense, we already explained the code property in the return object.

## strapiResponse.ok

There is a thing though. On success, we don't actually use the strapiResponse. What is a successful strapiResponse? A user + a `Strapi` jwt token. Oh, can we automatically sign in then? Maybe. But there are some problems:

1. We left out `NextAuth`. So, we would have to incorporate `NextAuth`.
2. How do we sign in? Using `signIn` from `NextAuth`. But `signIn` is a **client-side** only function. We have our user, but inside a **server-side** server action. So how do we get our user from the server to the client?

We will come back to this in the [last chapter](https://dev.to/peterlidee/nextauth-strapi-final-pieces-and-conclusion-48dm).

Right now, on success, we just ask the user to sign in using a success message. This pattern is maybe not optimal but also not unheard of. On the upside, it works!

## Summary

We just setup the forgot password flow using only `Strapi` and leaving out `NextAuth`. We added a request a password reset page, we handled sending an email and finished by creating the actual reset the password page.

Some minor problems were easily handled and mostly this was straightforward. There are 3 more things we have to handle:

1. Create a user account page.
2. Enable the user to change their password inside this page.
3. Enable the user to edit their data inside this page.

---

If you want to support my writing, you can [donate with paypal](https://www.paypal.com/donate/?hosted_button_id=4D78YQU4V5NEJ).