17/ Change password flow in Strapi and NextAuth

Peter Jacxsens - Apr 1 - - Dev Community

We have nearly finished. We'll first be creating a user account page. On this page, the user will be able to change the password and edit user data. We will limit this user data to the username but that should get you started. Note that the entire user account page will be guarded. Obviously, only signed in users can view the user account page. Let's make the page itself.

All the code in this chapter is available on github (branch: changeusername).

User account page

// frontend/src/app/(auth)/account/page.tsx

import Link from 'next/link';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/authOptions';
import Account from '@/components/auth/account/Account';

export default async function AccountPage() {
  const session = await getServerSession(authOptions);
  if (!session) {
    return (
      <div className='bg-zinc-100 rounded-sm px-4 py-8 mb-8'>
        <h2 className='font-bold text-lg mb-4'>Not allowed</h2>
        <p>
          You need to be{' '}
          <Link href='/signin' className='underline'>
            signed in
          </Link>{' '}
          to view this page.
        </p>
      </div>
    );
  }
  if (session.provider === 'google') {
    return (
      <div className='bg-zinc-100 rounded-sm px-4 py-8 mb-8'>
        <h2 className='font-bold text-lg mb-4'>Account</h2>
        <p>You are logged in to this website with your google account.</p>
      </div>
    );
  }
  return <Account />;
}
Enter fullscreen mode Exit fullscreen mode

Quick breakdown: we get a server session from NextAuth and display an error when there is no signed in user. When there is a session, we check if the provider is Google. If it is, we return You are logged in to this website with your google account.. There is no password to change and the username comes from the Google account and we won't allow edits from users using the GoogleProvider. Obviously you would need to fine tune this in a real app. Next, the <Account /> component.

// frontend/src/components/auth/account/Account.tsx

export default function Account() {
  return (
    <div className='bg-zinc-100 rounded-sm px-4 py-8 mb-8'>
      <h2 className='font-bold text-lg mb-4'>Account</h2>

      <div className='mb-8'>
        <h3 className='font-bold mb-4 text-sky-700'>User Data</h3>
        <div className='mb-2'>
          <div className='block italic'>Username: </div>
          <div>Bob</div>
        </div>
        <div className='mb-2'>
          <div className='block italic'>Email: </div>
          <div>bob@example.com</div>
          <div>(You cannot edit your email.)</div>
        </div>
        <div className='mb-2'>Last updated: ----</div>
      </div>

      <div className='mb-8'>
        <h3 className='font-bold mb-4 text-sky-700'>Change password</h3>
        [change password component]
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is a hardcoded page for now with 2 sections: user data and change password. In user data we display the username, email and last updated info. Note that in Strapi we shouldn't edit the email as it counts as a unique identifier. The change password component will be a form that we handle later.

Where do we get our user data from? You could be tempted to get it from a nextAuth session but that is incorrect! It would work in this case but in real life you would have more data like address, preferences, email subscriptions, ... You wouldn't add all those to your NextAuth session.

So, we need to make a api call to Strapi. There is a Strapi endpoint for this: /users/me. This is a GET request so it doesn't take a body. It does need headers:

{
  headers: {
    Authorization: `Bearer ${token}`,
  },
}
Enter fullscreen mode Exit fullscreen mode

This is the basic Authorization header you have to add if you query Strapi for non public content. What is token? It is the Strapi token that should be inside our NextAuth session. The token is how Strapi verifies that the user is actually an authenticated user. If you know Strapi basics, this should be clear. Note that we could also add parameters to this endpoint like f.e. populate.

fetch currentUser in Strapi

I created a helper function fetcher.ts. This function takes a path, parameters (object with f.e. populate) and options (object with f.e. Authorization headers). It then constructs a url using qs and makes the request to Strapi. On success it will return the Strapi data, on error it will throw an error that will get caught by our root error.tsx file. It also adds Types and so. You can see the fetcher function on github.

We call this fetcher inside another function getCurrentUser.ts passing in the Strapi token and this should return us our current user.

// frontend/src/lib/fetchData/getCurrentUser.ts

import { StrapiCurrentUserT } from '@/types/strapi/StrapiCurrentUserT';
import fetcher from './fetcher';

export async function getCurrentUser(token: string) {
  const options = {
    headers: {
      Authorization: `Bearer ${token}`,
    },
    next: { tags: ['strapi-users-me'] },
  };
  const user: StrapiCurrentUserT = await fetcher('/users/me', {}, options);
  return user;
}
Enter fullscreen mode Exit fullscreen mode

Note the line next: { tags: ['strapi-users-me'] }. This is a Next tag that will allow up to revalidate this fetch later on. We now call getCurrentUser inside our <Account /> component (= server component):

  const session = await getServerSession(authOptions);
  const currentUser = await getCurrentUser(session!.strapiToken!);
  console.log('currentUser', currentUser);
Enter fullscreen mode Exit fullscreen mode

Note the ! in await getCurrentUser(session!.strapiToken!). That's how we tell TypeScript that there will be a session (we're in a guarded component) and there will be a strapiToken. This is the log:

currentUser {
  id: 2,
  username: 'Bob',
  email: 'bob@example.com',
  provider: 'local',
  confirmed: true,
  blocked: false,
  createdAt: '2024-03-15T10:59:28.300Z',
  updatedAt: '2024-03-15T10:59:28.300Z'
}
Enter fullscreen mode Exit fullscreen mode

We use the currentUser to populate the user data in our <Account /> component:

<div className='mb-8'>
  <h3 className='font-bold mb-4 text-sky-700'>User Data</h3>
  <div className='mb-2'>
    <div className='block italic'>Username: </div>
    <div>{currentUser.username}</div>
  </div>
  <div className='mb-2'>
    <div className='block italic'>Email: </div>
    <div>{currentUser.email}</div>
    <div>(You cannot edit your email.)</div>
  </div>
  <div className='mb-2'>
    Last updated: {new Date(currentUser.updatedAt).toLocaleString()}
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

And our <Account /> component now looks like this:

Account page

Quick recap. To populate our account page, we don't use data from a NextAuth session. Instead we use a Strapi endpoint /users/me that retrieves the current user's data when a Strapi token is included. We call this endpoint using the getCurrentUser function that in turn calls the fetcher function. In case of error, Next error boundary will catch it. If successful, we use it to display data on the account page.

One last detail. To actually get to the account page, we update <NavbarUser /> so the user's name becomes the link to /account.

link to account page

Conclusion

We will leave it at this for this chapter. In the next chapter, we will implement the edit user name functionality.


If you want to support my writing, you can donate with paypal.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .