Search code examples
node.jsbackendentity-relationshipstrapi

Private strapi data for current user


I have order app created on strapi, how can I secure data from other users, but user who create order can view him.

I use relation one-to-many from user to many orders, when ?populate=orders requests other authorized users can view them, and when I turn off find action this field removing from /users/me?populate=orders. How can I set visibility for order by user who made it?


Solution

  • you would need a middleware for this, the more general approach is to use policy to make users not able to fetch other user data, like here: Why all users in Strapi have access to update all users profile?

    So let's mod code a bit to fit your case, the steps we are going to do are following:

    • Create global middleware.
    • Inject middleware to user-permissions routes.
    • Mod middleware to remove orders from request if user aren't the one that is registered.

    The result is if we do /api/users?populate=orders or /api/users/:id?populate=orders we would not receive orders.

    So:

    strapi generate
    
    ? Strapi Generators middleware
    ? Middleware name orders
    ? Where do you want to add this middleware? Add middleware to root of project
    

    So by default middleware would log in console:

    'In orders middleware.'
    

    We need to apply it to find, findOne, update, delete routes of user-permissions plugin:

    src/extensions/user-permissions/strapi-server.js

    module.exports = (plugin) => {
        for (let i = 0; i < plugin.routes["content-api"].routes.length; i++) {
            const route = plugin.routes["content-api"].routes[i];
            // checks if this one of the routes we target
            if (
                route.handler === "user.findOne" ||
                route.handler === "user.find" ||
                route.handler === "user.update" ||
                route.handler === "user.destroy"
            ) {
                // if it's append middleware to route
                plugin.routes["content-api"].routes[i] = {
                    ...route,
                    config: {
                        ...route.config,
                        middlewares: route.config.middlewares
                            ? [...route.config.middlewares, "global::orders"]
                            : ["global::orders"],
                    },
                };
            }
        }
    
        return plugin;
    };
    
    
    yarn build
    yarn develop
    

    If you did the step correct you should see:

    [2023-02-14 14:59:51.472] http: GET /api/users (30 ms) 200
    [2023-02-14 15:00:01.852] info: In orders middleware.
    

    every time you hit /api/users

    Next step we going to tweak middleware, so middleware has two stages, first stage is before await next() it's stage that executed before controller, the second stage is after await next() is stage executed after controller.

    We are going to modify the second stage and if we find attribute orders we are going to remove it from response: src/middlewares/orders.js

    'use strict';
    
    /**
     * `orders` middleware
     */
    
    module.exports = (config, { strapi }) => {
      return async (ctx, next) => {
        // before controller
        await next();
        // after controller
        // we need to check if the reponse is correct, 
        // otherwise we will have error message in the data
        if (ctx.response.status === 200) {
          // get the authenticated user, if no user - undefined
          const { user } = ctx.state;
          // get data from response
          let data = ctx.response.body;
          // check if data is array
          if (Array.isArray(data)) {
            // run sanitize function for each element
            data = data.map(item => sanitizeItem(item, user))
          } else {
            // else run for single item
            data = sanitizeItem(data, user);
          }
          // apply result to response
          ctx.response.body = data;
        }
    
      };
    };
    
    // sanitizer function
    const sanitizeItem = (item, user) => {
      // check if user is not undefined
      if (user) {
        // check if user id is same as the item.id (user from request)
        if (user.id === item.id) {
          // if it's same return full object
          return item;
        }
      }
      // else extract orders from object
      let { orders, ...rest } = item;
      return rest;
    }
    

    And whoala:

    [
        {
            "id": 1,
            "username": "user1",
            "email": "[email protected]",
            "provider": "local",
            "confirmed": true,
            "blocked": false,
            "createdAt": "2023-02-14T11:39:43.246Z",
            "updatedAt": "2023-02-14T11:39:43.246Z",
            "orders": [
                {
                    "id": 1,
                    "title": "Order 1",
                    "createdAt": "2023-02-14T11:39:01.990Z",
                    "updatedAt": "2023-02-14T11:39:01.990Z"
                },
                {
                    "id": 2,
                    "title": "Order 2",
                    "createdAt": "2023-02-14T11:39:09.182Z",
                    "updatedAt": "2023-02-14T11:39:09.182Z"
                }
            ]
        },
        {
            "id": 2,
            "username": "user2",
            "email": "[email protected]",
            "provider": "local",
            "confirmed": true,
            "blocked": false,
            "createdAt": "2023-02-14T11:40:04.793Z",
            "updatedAt": "2023-02-14T11:40:04.793Z"
        }
    ]
    

    I'm authenticated as user1