When developing applications, both user experience and security is paramount . One challenge is allowing users with different levels of access to use the system smoothly while also keeping a detailed record of their actions. This becomes particularly tricky when we want to track every move users make for accountability and security reasons. That's where audit trailing comes in – it's like a logbook of events that happen in our application, covering things like data changes, how the application responds, and which users are involved. It's basically our way of keeping tabs on everything that happens in the application. Lets take a look at how we can solve this challenge building our own application
Implementation
Let's begin our project by initializing a new Node.js application and configure typescript.
npm init -y && tsc --init
Next, let's install the necessary dependencies including express
, typescript
, nodemon
, ts-node
and @types/express
npm i express && npm i typescript @types/express nodemon ts-node -D
Let's edit our package.json file to add our start script .
"scripts": {
"start": "nodemon src/index.ts"
},
Create a src
directory and add an index.ts
file to kick off our application.
//src/index.ts
import express, { Request, Response } from 'express';
const app = express();
app.get('/', (req: Request, res: Response)=> {
try {
return res.status(200).json({
msg: 'Success'
})
} catch (error) {
return res.status(500).json({
msg: 'Something went wrong, Try again !'
})
}
});
app.listen(5000, ()=> {
console.log('App is running')
})
We have successfully laid the foundation for our Express application, let's proceed to create a middleware function for robust audit logging. This middleware will seamlessly track every incoming request and the corresponding response, providing a comprehensive audit trail for accountability and security.
//src/audit-log.middleware.ts
import { NextFunction, Request, Response } from "express";
export const auditLoggerInterceptResponse = async (req: Request, res: Response, next: NextFunction) => {
//Save the initial res.json method to a variable for later use
const originalJson = res.json;
//Overide the res.json method with a custom implementation
res.json = function (body: any) {
// Create a payload capturing relevant information about the request and response
const payload = {
url: req.originalUrl,
method: req.method,
body: req.body,
params: req.params,
headers: req.headers,
statusCode: res.statusCode,
response: body,
};
console.log(payload);
//Save Payload
/*
Implement database logic
Note: Make use of Promise chaining for promises . Using async await
would not match the return type of res.json. This may result in an error
*/
// Call the original `res.json` method to send the response
return originalJson.call(this, body);
};
// Move to the next middleware or route handler in the Express middleware stack
next();
};
Let me break down how this function works
1 . Save the Initial res.json Method:
const originalJson = res.json;
The original res.json method is saved to the variable originalJson
for later use.
2 . Override res.json with Custom Implementation:
res.json = function (body: any) {...};
The res.json method is overridden with a custom implementation inside the middleware.
This custom implementation captures relevant information about the incoming request (req) and the outgoing response (res).
It creates a payload object containing details such as the request URL, method, body, parameters, headers, response status code, and response body.
3 . Database Logic Reminder:
A comment reminds developers to implement database logic for saving the payload.
It suggests using Promise chaining for promises, as using async/await might not match the return type of res.json.
** Note: For simplicity, the implementation above omits the database logic section. In a real-world scenario, you would have to integrate your preferred database logic within the designated section to ensure a tailored and robust audit trail for your application **
4 . Call the Original res.json Method:
return originalJson.call(this, body);
The original res.json method is called to ensure that the response is sent to the client as intended.
5 . Move to the Next Middleware or Route Handler:
next();
The next() function is called to move to the next middleware or route handler in the Express middleware stack.
In a nutshell this middleware modifies the res.json function so that when called in our routes it would perform our custom implementation. This tweak acts as a potent tool for improving our application's audit trail, incorporating custom actions during the response phase.
Let's proceed to call our middleware in our index.ts
as a global middleware
//src/index.ts
import express, { Request, Response } from 'express';
import { auditLoggerInterceptResponse } from './audit-log.middleware';
const app = express();
app.use(auditLoggerInterceptResponse);
app.get('/', (req: Request, res: Response)=> {
try {
return res.status(200).json({
msg: 'Success'
})
} catch (error) {
return res.status(500).json({
msg: 'Something went wrong, Try again !'
})
}
});
app.listen(5000, ()=> {
console.log('App is running')
})
Concluding our exploration, making a request to the GET route reveals the middleware function's response in our console, showcasing the powerful audit logging capabilities we've integrated. This hands-on experience solidifies the significance of our middleware as a crucial tool for observing user interactions, ultimately bolstering security and accountability within our Node.js application.
Github Repository: Repository Link
Top comments (1)
Great Article. It has really helped out.