[HowTo 🎓] Finally create proper lifecycle hooks for users(-permissions)

Hey, I’m new to strapi and saw that people have been struggling to have proper lifecycle hooks for the past ~1.5 years, which seemed to work in the past. I found one solution including database hooks but that didn’t allow me to access the ctx object which provides more detailed information about the request and response. In my use case, I need to log which user accessed / changed the information of another. And for that, ctx.state is handy, which is not available in the database hook :neutral_face:

So I’ve thought hacking around with this users-permissions plugin would be a good idea. I have little to none of a clue about strapi’s inner workings, but after some sherlock holmes level of investigations (actually just an hour or so) I thought I might have found what’s called a pro gamer move :stuck_out_tongue_winking_eye:

Just monkey-patch the **** out of this plugin to do what we require from it.

Bonus: You’re even getting lifecycle functions for the users-permissions specific /me api.

Here is how you do it:

  1. create a file at src/extensions/users-permissions/content-types/user/lifecycles.ts with the following content:
export const beforeCreate = async (ctx) => {};

export const afterCreate = async (ctx) => {};

export const beforeUpdate = async (ctx) => {};

export const afterUpdate = async (ctx) => {};

export const beforeFind = async (ctx) => {};

export const afterFind = async (ctx) => {};

export const beforeFindOne = async (ctx) => {};

export const afterFindOne = async (ctx) => {};

export const beforeCount = async (ctx) => {};

export const afterCount = async (ctx) => {};

export const beforeDestroy = async (ctx) => {};

export const afterDestroy = async (ctx) => {};

export const beforeMe = async (ctx) => {
  console.log("before !");
};

export const afterMe = async (ctx) => {
  console.log("after !");
};

So far, so unsuspicious. Now let’s get dirty: In src/extensions/users-permissions/strapi-server.ts you put this:

import { Strapi } from '@strapi/strapi';
import * as hooks from './content-types/user/lifecycles';

module.exports = (plugin) => {
  const controllers = plugin.controllers.user;
  const methods = ['create', 'update', 'find', 'findOne', 'count', 'destroy', 'me'];

  for (const method of methods) {
    const oldMethod = controllers[method];

    controllers[method] = async (ctx) => {
      await hooks[`before${method[0].toUpperCase() + method.slice(1)}`](ctx);
      const result = await oldMethod(ctx);
      await hooks[`after${method[0].toUpperCase() + method.slice(1)}`](ctx);
      return result;
    };
  }

  return plugin;
};

Congratulations, you’ve monkey-patched the user permissions plugin. Now if you call users/me, you see the appropriate logs in your console. Use and abuse as per your requirements. You can use the hooks to make DB entries, here is how I write a log entry:

export const afterFindOne = async (ctx) => {

  const entry = await strapi.entityService.create('api::user-log.user-log', {
    data: {
      test: ctx.body.id.toString()
    }
  })
};

Check ctx.request, ctx.response and ctx.state for useful information, but I’m sure you already know by now !

What do you think of my solution? :wave:

System Information
  • Strapi Version: 4.5.5
  • Operating System: OSX
  • Database: Postgres
  • Node Version: 18 LTS
  • NPM Version: 9.2.0
  • Yarn Version: 1.17.3

I like the idea. But would it not just be better to add in the register function custom middleware to all the routes? example is strapi-plugin-protected-populate/register.js at main · Boegie19/strapi-plugin-protected-populate · GitHub

since that would have the same effect of what you are doing but you are only add a middleware and not interacting with the controllers at all.

On an other node what you did is not a proper implementation since the admin and the content-api paths both exist and you only log everything done thorough the content-api path. And are ignoring the admin one.

also where possible you should be using lifecycles if they give you the information you need after that the next step in my opinion is adding custom middleware to the routes.

Also last thing doing extensions are always tricky since if they change the code it would not error on update and you would find the fault in production after the update when someone try’s to use the route.

My personally recommendation is forking it or using patch-package so that you get warnings when you update that you should probably look at it. Aka using extensions to edit existing code is a bad practice since it will break after an update whiteout warning when you update. you should only do it to add new code for example a new field to a content-type.

indeed, as that was the goal for my usecase. Thanks for the advice regarding possible breaking changes in the plugin!

How I would do what you did is trough this way

module.exports = ({ strapi }) => {
  const insertMiddleware = (value, path, method) => {
    value.forEach((route) => {
      if (route.path == path && method == route.method) {
        if (typeof route.config === 'undefined') {
          route.config = {};
        }
        if (typeof route.config.middlewares === 'undefined') {
          route.config.middlewares = ['global::MiddlewareName'];
        } else {
          route.config.middlewares.push('global::MiddlewareName');
        }
        return true;
      }
    });
  };

  insertMiddleware(strapi.plugins["users-permissions"].routes["content-api"].routes,"/api/users/:id","GET")
}

Why since I am only adding a middleware in the register. it will basicly do the same thing you showed me only registers. this has a almost no chance of breaking since it will add a middleware to the route like you would do normally what strapi would not change since that would be an external breaking change.

1 Like

Easier way would be to use our new api for that documented here: Requests and Responses - Strapi Developer Docs

This does not work for me, meaning that the hooks are not being run.

where does it get stuck


It doesn’t get stuck, it’s just that my code is not being called (aka: I don’t see my console log)

And this is the code I used in “strapi-server” (basically yours, I just changed the methods to only generate create and update lifecycles)

const controllers = plugin.controllers.user;
  const methods = ['create', 'update'];
  console.log("applying lifecycle for users")
  for (const method of methods) {
    const oldMethod = controllers[method];

    controllers[method] = async (ctx) => {
      console.log(`before${method[0].toUpperCase() + method.slice(1)}`)
      await hooks[`before${method[0].toUpperCase() + method.slice(1)}`](ctx);
      const result = await oldMethod(ctx);
      await hooks[`after${method[0].toUpperCase() + method.slice(1)}`](ctx);
      return result;
    };
  }

fixed it???

Sorry for this, I actully replied wrong, I just tried the solution proposed by @n1cK.

@Boegie19 could you please detail about how to use your middleware? To be honest, I also didn’t quite get how middlewares get triggered in strapi. After reading the documentation I would assume that I have to declare it like that in a global ./src/middlewares/my-middleware.js:

export default (config, { strapi })=> {
  return (context, next) => {
     console.log("set role")
       const entry = await strapi.entityService.update('plugin::.user-permission.user', {
          data: {
             role: 3
          }
       })
  };
};

and then insert it in my routes like that:

module.exports = {
  routes: [
    {
      method: "GET",
      path: "/api/auth/local/register",
      handler: "???",
      config: {
        middlewares: ["my-middleware"],
      },
    },
  ],
};

The docs are correct I basicly do the same thing in my plugin only programaticly

thank you, I am trying that and get back with my solution. However, what do you mean with “my plugin” and where would you use the code that you wrote above?

in a plugin bootstrap function or within strapis bootstrap function

Same situation here for me. @n1cK code applied but no console.log is ever executed.

System Information

  • Strapi Version: 4.13.7
  • Operating System: Ubuntu 22.04
  • Database: MySQL Latest
  • Node Version: 18.16.1
1 Like

did you call the middleware somewhere?

That middleware was configured under my content type routes for the update and delete methods. I guess that is what you refer whether I called the middleware somewhere, am I wrong?

To all the people for whom this code doesn’t work: God knows if something changed in the recent strapi versions or not, but if you’re a bit familiar with the concept of monkey patching (Monkey patch - Wikipedia) , this is exactly what is going on in my solution. You can build it yourself:

  1. find the function that is actually being executed for which you want to add a custom behaviour.

let referenceToActualFunction = actualFunctionName

  1. Create a function that does everything you want and call the actual, old function inside it.
let myCoolerFunction = () => {

myPreHook();
referenceToActualFunction();
myPostHook();
}
  1. replace the old function with your new function in runtime:

`actualFunctionName = myCoolerFunction;``

So now whenever the actual function is ran, your custom one is executed, which vontains own logic and runs the actual old function. The functions you need to monkey patchhere can be found when console-logging strapi.entityService , containing strapi.entityService.create and the like.

2 Likes

I just figured out that an even better alternative to my solution this guide could even be: Controllers | Strapi Documentation , basically reimplementing the logic of the controller in its proper place so to say?

EDIT: Sorry, no, had to figure out that this solution is not working for the users-permissions plugin.