18/ Edit username form and custom Strapi route and controller
Tags: nextauth, strapi, nextjs
Published: 2024-04-01 16:36:15

We can now build the functionality to update the user's data (username). This is actually quite complex because we will run into a lot of problems. That's why I'm going to walk you through the entire process, pointing our problems and solutions along the way.

This is what we will be making:

![change username animation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3q7sdq6w1ml1olf85vfd.gif)

Let's break this down in steps:

1. Toggle edit function
2. New username input field
3. Save button
4. Error or success message

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

## Setup

Let's first create the component and insert it into the `<Account />` component. Inside `<Account />`, we take this

```jsx
<div className='mb-2'>
  <div className='block italic'>Username: </div>
  <div>{currentUser.username}</div>
</div>
```

and replace it with this:

```jsx
<EditUsername username={currentUser.username}>
```

Then, create `<EditUsername />` component, paste the jsx into it and set up the prop:

```jsx
// frontend/src/components/auth/account/EditUsername.tsx

type Props = {
  username: string,
};

export default function EditUsername({ username }: Props) {
  return (
    <div className='mb-2'>
      <div className='block italic'>Username: </div>
      <div>{username}</div>
    </div>
  );
}
```

## Toggle functionality

This is simple, we use a boolean state edit, controlled by a button. When edit is false, we just show the value of username. On true, we show a little edit form. Here is the update:

```jsx
export default function EditUsername({ username }: Props) {
  const [edit, setEdit] = useState(false);
  return (
    <div className='mb-2'>
      <div className='block italic'>Username:</div>
      <div className='flex gap-1'>
        {!edit && <div>{username}</div>}
        {edit && 'TODO -> INPUT + BUTTON'}
        <button
          type='button'
          onClick={() => {
            setEdit((prev) => !prev);
          }}
          className='underline text-blue-700'
        >
          {edit ? 'close' : 'edit'}
        </button>
      </div>
    </div>
  );
}
```

## Adding a form

We need a form element with an input field (new name) and a submit button (save new name). We're going to wrap the entire component inside a form tag so we can display feedback outside of the `{edit &&}` conditional. We update the _Username:_ label to an actual html label element and then add the input and button elements inside the conditional `{edit &&}`:

```jsx
export default function EditUsername({ username }: Props) {
  const [edit, setEdit] = useState(false);
  return (
    <div className='mb-2'>
      <form>
        <label htmlFor='username' className='block italic'>
          Username:
        </label>
        <div className='flex gap-1'>
          {!edit && <div>{username}</div>}
          {edit && (
            <>
              <input
                type='text'
                className='bg-white border border-zinc-300 rounded-sm px-2 py-1 w-48'
                required
                name='username'
                id='username'
              />
              <button
                type='submit'
                className={`bg-blue-400 px-3 py-1 rounded-md disabled:bg-sky-200 disabled:text-gray-400 disabled:cursor-wait`}
              >
                save
              </button>
            </>
          )}
          <button
            type='button'
            onClick={() => {
              setEdit((prev) => !prev);
            }}
            className='underline text-sky-700 ml-1'
          >
            {edit ? 'close' : 'edit'}
          </button>
        </div>
      </form>
    </div>
  );
}
```

That is most of our jsx handled. We now only need some error and success feedback that we will add later.

## Flow

What do we actually need to do now? What are our steps?

1. Obviously, we need to update the username in the `Strapi` database. So, we have to call a `Strapi` api endpoint for this.
2. But then, we also need to update the data on our page. In other words, refetch the query or revalidate the cache.
3. Lastly, we have to update our `NextAuth` session, because it holds our now old username.

We grabbed our current user data from a fetch to the `Strapi` endpoint: `/users/me`. This is a fetch in a server component. If we were to update said user in `Strapi`, what would happen? Nothing. There is no way for our `<Account />` component to know that it's data has become stale. So, we have to do something manually.

In the previous chapter, we wrote out a `getCurrentUser` function that fetches the current user. We included a `Next` tag to the options of this fetch:

```jsx
{
  next: {
    tags: ['strapi-users-me'];
  }
}
```

This allows us to revalidate this tag: `revalidateTag('strapi-users-me')`. But there is a catch here:

> `revalidateTag` only invalidates the cache when the path is next visited. [...] The invalidation only happens when the path is next visited. [Next Docs](https://nextjs.org/docs/app/api-reference/functions/revalidateTag)

So, the user just changed the username in `Strapi`. We're yet to figure out how but somewhere in this process, we revalidate the tag. But the user will still see the old username! Only when the path is next visited, will the fetch be revalidated. Even then, the user might still see the stale data because of other caching mechanisms.

We could cheat here. Listen for the strapiResponse and on success show the new name not from cache but from state for example. But, there is a better way: using server actions:

> Server Actions integrate with the Next.js caching and revalidation architecture. When an action is invoked, Next.js can return both the updated UI and new data in a single server roundtrip. [Next Docs](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#behavior)

What does this mean? When we call `revalidateTag` in a server action, `Next` will immediately refetch this data (server-side) and send the data back together with the response from our server action. Client side, `Next` will use these data to update the cache. In other words, no more stale data! And this is exactly what we need.

So, to have fresh data, we need to combine `revalidateTag` and a server action. This also means that we will have to call the `Strapi` endpoint in a server action which is no problem, we've done this multiple times now. Let's do this first and deal with the third point (updating the token and session) later.

## Update username in Strapi

We used the `Strapi` `/users/me` endpoint to fetch the user data. Is there a similar endpoint to update the user? No! 2 ways to solve this. We could enable the general update user endpoint. In `Strapi` admin panel

```
Settings > Roles > Authenticated > users-permissions
```

We have a couple of sections like auth (the endpoints we've been using in this series like register and forgotPassword) but also a user section. In this section you can enable an update users endpoint. We could then update our current user by passing in the current user id and the updates. But, this opens up our entire users database for any authenticated users and I don't like that.

Option 2: we will be writing a custom endpoint that only allows for the current user to be updated. An update variant of `users/me`. While I was investigating how to do this I ran into a tutorial written by somebody from `Strapi` itself. However, while this tutorial send me on the right track it was bad security wise. Really bad! So, something new now. We're going to create a custom `Strapi` route and controller.

## Custom Strapi route and controller

Disclaimer, this is far outside my comfort zone so don't simply copy paste this! I would recommend you watch the [youtube video](https://www.youtube.com/watch?v=2ZwiiY6tnmw) I mentioned earlier because we mostly follow it. We just add a lot of things to the controller later.

The endpoint that we want to create will be `user/me`. We are going to add this to the Users and Permissions plugin. Create these folders and file (careful, this is in the `Strapi` backend):

```js
// backend/src/extensions/users-permissions/strapi-server.js

module.exports = (plugin) => {
  // *** custom controller ***
  plugin.controllers.user.updateMe = async (ctx) => {
    // do stuff
  };

  // *** custom route ***
  plugin.routes['content-api'].routes.push({
    method: 'PUT',
    path: '/user/me',
    handler: 'user.updateMe',
    config: {
      prefix: '',
      policies: [],
    },
  });

  return plugin;
};
```

This creates a `user/me` route (PUT) that will be handled by the `updateMe` controller. The folder structure makes it add the route to the Users and Permissions plugin.

This is the controller function as it was coded inside the youtube video:

```js
plugin.controllers.user.updateMe = async (ctx) => {
  // needs to be logged in
  if (!ctx.state.user || !ctx.state.user.id) {
    return (ctx.response.status = 401);
  }
  // do the actual update and return update name
  await strapi
    .query('plugin::users-permissions.user')
    .update({
      where: { id: ctx.state.user.id },
      data: ctx.request.body,
    })
    .then((res) => {
      ctx.response.body = { username: res.username };
      ctx.response.status = 200;
    });
};
```

We first check if there is a user and an id and then perform the actual update using `Strapi` queries and methods. This solution works but it is really flawed. Not because of what is there but because of what is missing. Using Postman I tested this solution. Note, you have to enable this new route (updateMe)in `Strapi admin` for authenticated users!

Here are some things I found:

- You can update things you shouldn't be able to update like provider or email. Not id, authorized or blocked, but still.
- Trying to update the password doesn't seem to work but makes you unable to sign in with either the new or old password. So it doesn't work but breaks everything.
- Username should be unique. Yet using this controller, you can simply update to an already existing one! This should not be allowed. It's not in the sign up endpoint.
- Lastly, we didn't sanitize our user input.

So, it works but is wildly insecure! Let's quickly fix these. Note: there may be things here I overlooked. I'm just gonna put the solution here, you can study it yourself. One last note, I use 2 `Strapi` helpers: `ApplicationError` and `ValidationError` that format errors into the nice `Strapi` error object that we've seen before: `{ data: null, error: { message: string, ... }}`

Here is our final file that fixes all of the above problems (tested):

```js
// backend/src/extensions/users-permissions/strapi-server.js

const _ = require('lodash');
const utils = require('@strapi/utils');
const { ApplicationError, ValidationError } = utils.errors;

module.exports = (plugin) => {
  // *** custom controller ***
  plugin.controllers.user.updateMe = async (ctx) => {
    // needs to be logged in
    if (!ctx.state.user || !ctx.state.user.id) {
      throw new ApplicationError('You need to be logged');
    }

    // don't let request without username through
    if (
      !_.has(ctx.request.body, 'username') ||
      ctx.request.body.username === ''
    ) {
      throw new ValidationError('Invalid data');
    }

    // only allow request with allowedProps
    const allowedProperties = ['username'];
    const bodyKeys = Object.keys(ctx.request.body);
    if (bodyKeys.filter((key) => !allowedProperties.includes(key)).length > 0) {
      // return (ctx.response.status = 400);
      throw new ValidationError('Invalid data');
    }

    // sanitize fields (a bit)
    const newBody = {};
    bodyKeys.map(
      (key) =>
        (newBody[key] = ctx.request.body[key].trim().replace(/[<>]/g, ''))
    );

    // don't let user chose username already in use!!!!
    // can't get this to work case insensitive
    if (_.has(ctx.request.body, 'username')) {
      const userWithSameUsername = await strapi
        .query('plugin::users-permissions.user')
        .findOne({ where: { username: ctx.request.body.username } });
      if (
        userWithSameUsername &&
        _.toString(userWithSameUsername.id) !== _.toString(ctx.state.user.id)
      ) {
        throw new ApplicationError('Username already taken');
      }
    }

    // do the actual update and return update name
    await strapi
      .query('plugin::users-permissions.user')
      .update({
        where: { id: ctx.state.user.id },
        data: newBody,
      })
      .then((res) => {
        ctx.response.body = { username: res.username };
        ctx.response.status = 200;
      });
  };

  // *** custom route ***
  plugin.routes['content-api'].routes.push({
    method: 'PUT',
    path: '/user/me',
    handler: 'user.updateMe',
    config: {
      prefix: '',
      policies: [],
    },
  });

  return plugin;
};
```

To close this section off, here are the 2 main sources I used: [youtube](https://www.youtube.com/watch?v=2ZwiiY6tnmw) and [strapi forum](https://forum.strapi.io/t/users-me-update/15187/13). In the next chapter we will call this new endpoint.

---

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