Audit logs on Strapi v4.6.x
At the time I am writing this article, this option is only available in the “GOLD PLAN” version. I couldn’t assess how it was implemented, but certainly in a more elegant way which I’m going to present now.
Premise:
Be able to audit changes to some system tables, identifying the user who made the change, and what was changed.
Solution:
Store auditing logs inside a new table, using middleware and Strapi lifecycle hooks.
Steps:
- Create new collection type "AuditLog"
- Create new collection type "Article" (this is only to use on this tutorial)
- Create a helper with the array of UIDs to audit
- Create the main lifecycle file
- Create the lifecycle file inside the API
- Create the middleware
- Configure Strapi with the new middleware
Create new collection type “AuditLog”
Fields:
- uid — Long Text
- params — JSON
- result — JSON
- user — JSON
- actionType — Long Text
- changedId — Short Text
Create new collection type “Article”
Fields:
- name — Short Text
Create a helper with the array of UIDs to audit
This is a helper with a simple array of UIDs to avoid Strapi audit every request to the backend API.
./src/misc/uidAutitList.jsmodule.exports = [
'api::article.article',
]Create the main lifecycle file
Every collection needs their own "lifecycle.js" file, and here I'm centralizing the logic of all files that we need manually input inside "./src/api/<collectionName>/content-types/<collectionName>/lifecycles.js" path
./src/misc/lifecycle.js
const arrUidToAudit = require('./uidAuditList')
module.exports = {
afterCreate(event) {
const { result, params, model } = event;
if (arrUidToAudit.includes(model.uid)) {
strapi.entityService.create('api::audit-log.audit-log', {
data: {
uid: model.uid,
params: params,
result: result,
actionType: 'ADD'
}
})
}
},
afterUpdate(event) {
const { result, params, model } = event;
if (arrUidToAudit.includes(model.uid)) {
strapi.entityService.create('api::audit-log.audit-log', {
data: {
uid: model.uid,
params: params,
result: result,
actionType: 'UPDATE'
}
})
}
},
beforeDelete(event, ctx) {
const { result, params, model } = event;
params.populate = { createdBy: true, updatedBy: true }
},
async afterDelete(event) {
const { result, model } = event;
if (arrUidToAudit.includes(model.uid)) {
await strapi.db.query('api::audit-log.audit-log').update({
where: { uid: model.uid, changedId: result.id},
data: {
result: result
}
});
}
}
}The "afterDelete" lifecycle will update the audit item created on the middleware (see next steps) with more informations about the deleted record.
Create the lifecycle file inside the API
We need to place this file inside each collection that we want to enable lifecycle auditing.
./src/api/article/content-types/article/lifecycles.jsconst lifecycles = require("../../../../misc/lifecycles");
module.exports = lifecyclesCreate the middleware
This step was necessary as the “beforeDelete” and “afterDelete” lifecycle methods do not contain any information about who made the request to delete the item.
So I intercepted all “DELETE” requests to get the JWT token user information, as you can see in the script below:
./src/middlewares/audit.js// path: /middlewares/audit.js
const jwt_decode = require("jwt-decode");
const arrUidToAudit = require('../misc/uidAuditList')
module.exports = () => {
return async (ctx, next) => {
const { request } = ctx
if (request.method === 'DELETE') {
const urlArr = request.url.split('/')
const uid = urlArr[3]
const id = urlArr[4]
//
// filter uid's to log
if (arrUidToAudit.includes(uid)) {
const tokenSemBearer = ctx?.request?.header?.authorization?.replace("Bearer ", "");
let decoded = jwt_decode(tokenSemBearer);
const userId = decoded.id;
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
select: '*',
where: { id: userId },
});
await strapi.entityService.create('api::audit-log.audit-log', {
data: {
uid: uid,
changedId: id,
params: {id: id},
user: user,
actionType: 'DELETE'
}
})
}
}
await next();
};
};Configure Strapi with the new middleware
Default configuration for application middewares.
./config/middlewares.jsmodule.exports = [
'strapi::errors',
'strapi::security',
'strapi::cors',
'strapi::poweredBy',
'strapi::logger',
'strapi::query',
'strapi::body',
'strapi::session',
'strapi::favicon',
'strapi::public',
'global::audit', // here's the new middleware
];Next steps
- check multi add/delete lifecycle
- wrap it on a plugin
