Using Strapi Policies To Create Editable User Profiles

drewtownchi

Drew Town

Posted on May 5, 2020

Using Strapi Policies To Create Editable User Profiles

Strapi's roles & permissions plugin will get you a long way in registering, sign-in and managing users in your application. Unfortunately, Strapi does not provide a built-in strategy for allowing users to manage their own personal information via a user profile, instead leaving those decisions to you, the developer.

We will use Strapi's policy customizations to extend the Strapi API. The policy will allow authenticated users to update their user profile in a secure manner.

Extending The User Content-Type

Display names, subscription status, and user image are a few examples of the information we may want to allow the user to edit. In contrast, we will exclude e-mail address, role and other sensitive fields from user editing. In this example we will tackle, display name and if they subscribe to the weekly newsletter.

Using Strapi's "Content-Types Builder" select the User collection type. Select "Add another field", choose Text and give it a name of displayName. Next, add another field and this time choose Boolean and give it a name of newsletter.

Content-Types Builder admin panel with display name of type text and newsletter of type boolean created

Implementing The Policy

Create the file extensions/users-permissions/config/policies/userUpdate.js in your Strapi project. This file is where we will define the policy that Strapi will utilize when it receives a PUT request to the route /users.

module.exports = async (ctx, next) => {
  // If the user is an administrator we allow them to perform this action unrestricted
  if (ctx.state.user.role.name === "Administrator") {
    return next();
  }

  const { id: currentUserId } = ctx.state.user;
  // If you are using MongoDB do not parse the id to an int!
  const userToUpdate = Number.parseInt(ctx.params.id, 10);

  if (currentUserId !== userToUpdate) {
    return ctx.unauthorized("Unable to edit this user ID");
  }

  // Extract the fields regular users should be able to edit
  const { displayName, newsletter } = ctx.request.body;

  // Provide custom validation policy here
  if (displayName && displayName.trim() === "") {
    return ctx.badRequest("Display name is required");
  }

  // Setup the update object
  const updateData = {
    displayName,
    newsletter
  };

  // remove properties from the update object that are undefined (not submitted by the user in the PUT request)
  Object.keys(updateData).forEach((key) => updateData[key] === undefined && delete updateData[key]);
  if(Object.keys(updateData).length === 0) {
    return ctx.badRequest("No data submitted")
  }

  ctx.request.body = updateData;
  return next();
};

Later on we will dig deeper into what each part of this policy is doing. But for now, let's continue setting up Strapi to use this new policy.

Setting Permissions in the Strapi Admin

Verify that an authenticated user has access to the me and update actions via the roles & permissions plugin in the admin under the user permissions section. When checking the update option select our newly create userUpdate policy in the advanced settings. By selecting the policy from the policy select dropdown we'll ensure that each request made is checked by the policy before the controller receives the request.

Selecting these actions will allow a user to to make GET requests to /users/me and PUT requests to /users.

Strapi Roles & Permission's plugin with me and update methods checked for Authenticated user and user update policy selected in the advanced settings

Note: Authenticated, in this case, means that we sent the request with an authorization header that includes a valid bearer token returned by the login route.

const res = await axios.get('http://localhost:1337/users/me', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
})

Retrieving The Current User

The roles & permissions plugin includes a route /users/me that allows an authenticated user to retrieve information about themselves. If you are using a front-end with store such as Vuex or Redux, you may already have this information handy in your front-end application. We will use the data from this route in order to pre-populate our form fields for editing.

Creating and submitting a form on your client application is outside the scope of this article.

Now that we've verified we can access information about the authenticated user, we can allow a user to change some information about themselves using a PUT request to the update route that utilizes our new policy. Let's take a closer look at what this policy does.

Digging Deeper Into The Policy

Let's break this policy into a few chunks to analyze what it is doing.

Parse The Request Data

First, we verify who the user is, whether they are an admin, or they are a regular user trying to edit their own information.

  // If the user is an administrator we allow them to perform this action unrestricted
  if (ctx.state.user.role.name === "Administrator") {
    return next();
  }

  const { id: currentUserId } = ctx.state.user;
  // If you are using MongoDB do not parse the id to an int!
  const userToUpdate = Number.parseInt(ctx.params.id, 10);

  if (currentUserId !== userToUpdate) {
    return ctx.unauthorized("Unable to edit this user ID");
  }

If the user is an administrator, let the request go through as we assume they have all the permissions to perform any action on any user.

We are using object destructing to extract the authenticated user's id from the Strapi context and the ID parameter from the URL parameters. The ctx (context) variable passed into the policy is provide by the Roles & Permissions plugin and will include information about the currently authenticated user such as the id\ we are extracting.

If you are using MongoDB do not parse the ID to an int. MongoDB uses a UID user ID pattern.

Since we are using the plugin's existing controller, it is expecting a URL parameter for the user we are editing. Meaning a put request will need to go to the route /users/1 or /users/23 depending on the user being updated. Therefore, we need to verify the user is editing their own user information and not another user's information.

Extract The Data

  // Extract the fields regular users should be able to edit
  const { displayName, newsletter } = ctx.request.body;

Next, we extract the displayName and newsletter from the request body that the user submitted.

It is important to not blindly accept all parameters from the user as they could maliciously include additional fields such as role and elevate themselves to admin privileges.

Validation

  // Provide custom validation policy here
  if (displayName && displayName.trim() === "") {
    return ctx.badRequest("Display name is required");
  }

Within the policy is an excellent time to perform any additional validation. Even though Strapi has some validation built-in, such as string and boolean fields must match their respective types, you may not want to let users to have a display name of "Admin" or a series of spaces for example. In the policy you can perform your own simple validation or pull in a validation library of your choice.

Massage The Update Data

  const updateData = {
    displayName,
    newsletter
  };

  // remove properties from the update object that are undefined (not submitted by the user in the PUT request)
  Object.keys(updateData).forEach((key) => updateData[key] === undefined && delete updateData[key]);
  if(Object.keys(updateData).length === 0) {
    return ctx.badRequest("No data submitted")
  }

  ctx.request.body = updateData;
  return next();

We setup the updateData variable using ES2015 object property shorthand. Our variable names submitted by the user's request match the names we setup in the Strapi content builder so we can quickly initialize the update object. If your names don't match, you will need to use the standard object assignment.

Filter out any values that are undefined (not included in the PUT request), and if the user did not submit any valid data, we can short-circuit and return a badRequest letting the user know.

Finally, replace the ctx.request.body with our sanitized updateData and return next() to let Strapi know the request has passed the policy test and the controller can proceed.

Sending a request from the client

We've now allowed authenticated users to request data about themselves and send an update request with a policy applied to the request. When you are ready to send a request from the client, you can send an update like the following example using Axios.

const res = await axios.put('http://localhost:1337/users/1',
{
  displayName: "John Smith",
  newsletter: true
},
{
  headers: {
    Authorization: `Bearer ${token}`,
  },
})
💖 💪 🙅 🚩
drewtownchi
Drew Town

Posted on May 5, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related