UNPKG

@kingdiablo/auditor

Version:

A lightweight and customizable audit logger for Node.js apps. Tracks database changes, errors, and user actions with support for external loggers like Winston or Pino.

1 lines 71.3 kB
{"version":3,"sources":["../src/core/Auditor.ts","../src/database/mongodb/config.ts","../src/core/AppConfigs.ts","../src/utils/helper.ts","../src/utils/user.ts","../src/middleware/requestLogger.ts","../src/middleware/errorLogger.ts","../src/router/router.ts"],"sourcesContent":["import chalk from \"chalk\";\r\nimport { Schema } from \"mongoose\";\r\nimport { auditModel, checkForMongodb } from \"../database/mongodb\";\r\nimport { errorLogger, requestLogger } from \"../middleware\";\r\nimport { UIRouter, checkForFramework, downloadDependency } from \"../router\";\r\nimport { Framework, SupportedLoggersRequest, TAuditOptions, TEvent, TFileConfig } from \"../types/type\";\r\nimport { createFile, generateAuditContent, getFileLocation, handleLog, logAuditEvent } from \"../utils\";\r\nimport { userProfile } from \"../utils/user\";\r\nimport { AppConfig } from \"./AppConfigs\";\r\n\r\n\r\nexport class Audit<F extends Framework = \"express\"> {\r\n private logFilePath: string = \"\";\r\n private defaultFileConfigs: TFileConfig[] = [\r\n {\r\n fileName: \"error.log\",\r\n folderName: \"audits\",\r\n fullPath: \"\",\r\n },\r\n {\r\n fileName: \"request.log\",\r\n folderName: \"audits\",\r\n fullPath: \"\",\r\n },\r\n {\r\n fileName: \"db.log\",\r\n folderName: \"audits\",\r\n fullPath: \"\",\r\n },\r\n {\r\n fileName: \"action.log\",\r\n folderName: \"audits\",\r\n fullPath: \"\",\r\n },\r\n ];\r\n private fileConfig: TFileConfig = {\r\n fileName: \"audit.log\",\r\n folderName: \"audit\",\r\n fullPath: \"\",\r\n };\r\n private isInitialized: boolean = false;\r\n\r\n private auditOptions: TAuditOptions<F> = {\r\n dbType: \"none\",\r\n destinations: [\"console\"],\r\n framework: \"express\" as F,\r\n logger: console,\r\n useTimeStamp: true,\r\n splitFiles: false,\r\n captureSystemErrors: false,\r\n useUI: false,\r\n };\r\n\r\n\r\n\r\n /**\r\n * Creates an instance of the Auditor class with the provided options.\r\n *\r\n * @param options - Optional configuration object for the auditor.\r\n * - `logger`: Custom logger to use; defaults to `console` if not provided.\r\n * - `dbType`: Type of database to use; defaults to `\"none\"`.\r\n * - `destinations`: Array of destinations for audit logs; defaults to `[\"console\"]`.\r\n * - `framework`: The framework being used (e.g., \"express\"); defaults to `\"express\"`.\r\n * - `useTimeStamp`: Whether to include timestamps in logs; defaults to `true`.\r\n * - `splitFiles`: Whether to split logs into multiple files; defaults to `false`.\r\n * - `captureSystemErrors`: Whether to capture system errors; defaults to `false`.\r\n *\r\n * Initializes the `auditOptions` property by merging the provided options with sensible defaults.\r\n */\r\n constructor(private options?: TAuditOptions<F>) {\r\n this.auditOptions = {\r\n ...options,\r\n logger: options?.logger || console,\r\n dbType: options?.dbType || \"none\",\r\n destinations: options?.destinations || [\"console\"],\r\n framework: options?.framework as F ?? \"express\" as F,\r\n useTimeStamp: options?.useTimeStamp ?? true,\r\n splitFiles: options?.splitFiles ?? false,\r\n captureSystemErrors: options?.captureSystemErrors ?? false,\r\n useUI: options?.useUI ?? false,\r\n };\r\n }\r\n\r\n\r\n /**\r\n * Initializes the auditor by creating the necessary file location and logging a success message.\r\n *\r\n * This method sets up the audit configuration by invoking `CreateFileLocation` with the current file configuration,\r\n * and logs a confirmation message using the configured logger.\r\n *\r\n * @remarks\r\n * Should be called before performing any audit operations to ensure the environment is properly configured.\r\n */\r\n async Setup() {\r\n if (this.isInitialized) {\r\n this.auditOptions.logger?.warn(chalk.yellow(\"Audit already initialized.\"));\r\n return;\r\n }\r\n\r\n this.CreateFileLocation(this.fileConfig);\r\n\r\n AppConfig.setAuditOption(this.auditOptions);\r\n AppConfig.setDefaultFileConfig(this.defaultFileConfigs);\r\n AppConfig.setFileConfig(this.fileConfig);\r\n AppConfig.setLogFilePath(this.logFilePath);\r\n AppConfig.setCaptureSystemErrors(this.auditOptions.captureSystemErrors ?? false);\r\n AppConfig.setFrameWork(this.auditOptions.framework!);\r\n AppConfig.setUseUI(this.auditOptions.useUI ?? false);\r\n\r\n if (this.auditOptions.dbType === \"mongoose\") {\r\n const result = checkForMongodb();\r\n if (!result) return;\r\n }\r\n\r\n if (this.auditOptions.useUI) {\r\n const hasFramework = checkForFramework();\r\n if (hasFramework) {\r\n AppConfig.getAuditOption()?.logger?.info(chalk.yellow(\"In order to use this module some dependency will be downloaded\"));\r\n await downloadDependency();\r\n }\r\n }\r\n\r\n\r\n\r\n this.isInitialized = true;\r\n AppConfig.setIsInitialized(this.isInitialized);\r\n\r\n this.HandleSystemErrors();\r\n\r\n this.auditOptions.logger?.info(chalk.green(\"Default Audit config set successfully\"));\r\n }\r\n\r\n /**\r\n * Logs an event to the configured destinations (console and/or file).\r\n *\r\n * Depending on the configuration, this method will:\r\n * - Add a timestamp to the event if `useTimeStamp` is enabled.\r\n * - Log the event to the console if \"console\" is included in `destinations`.\r\n * - Log the event to a file if \"file\" is included in `destinations`.\r\n * - If `splitFiles` is enabled, logs to a specific action log file.\r\n * - Otherwise, logs to the default file configuration.\r\n * - If the file path is not set when logging to a file, logs an error.\r\n *\r\n * @param event - The event object to be logged.\r\n */\r\n Log(event: TEvent) {\r\n\r\n const item = generateAuditContent({ ...event });\r\n\r\n if (!this.isInitialized) {\r\n this.auditOptions?.logger?.info(chalk.red(\"Not Initialized. Setup Is Required\"));\r\n return;\r\n }\r\n\r\n if (this.auditOptions.destinations?.includes(\"console\")) {\r\n logAuditEvent(item);\r\n }\r\n\r\n if (this.auditOptions.destinations?.includes(\"file\")) {\r\n\r\n const actionFile = this.defaultFileConfigs.find(x => x.fileName === \"action.log\") as TFileConfig;\r\n const file = this.auditOptions.splitFiles ? actionFile : this.fileConfig;\r\n\r\n if (!file.fullPath) {\r\n this.auditOptions.logger?.error(chalk.red(\"Unable to locate file path\"));\r\n return;\r\n }\r\n\r\n logAuditEvent(item, file);\r\n }\r\n\r\n }\r\n\r\n /**\r\n * Logs an error event to the configured destinations (console and/or file).\r\n * @example\r\n * LogError(new Error(\"Unexpected failure\"))\r\n * Logs error details to the console and file depending on configuration.\r\n * @param {any} error - The error object to be logged.\r\n * @description\r\n * - Extracts the error stack trace to capture informative details.\r\n * - Compiles additional user context such as IP and user agent when available.\r\n * - Handles uninitialized state by printing a warning message.\r\n * - Utilizes timestamp inclusion and log destination logic based on audit options.\r\n */\r\n LogError(error: any) {\r\n\r\n if (!this.isInitialized) {\r\n this.auditOptions?.logger?.info(chalk.red(\"Not Initialized. Setup Is Required\"));\r\n return;\r\n }\r\n\r\n let stackLine = \"\";\r\n if (error.stack) {\r\n const lines = error.stack.split('\\n');\r\n stackLine = lines.length > 1 ? lines[1].trim() : lines[0]?.trim() ?? \"\";\r\n }\r\n\r\n const item = generateAuditContent({\r\n type: \"error\",\r\n action: \"unknown\",\r\n outcome: \"error\",\r\n method: \"user called\",\r\n statusCode: error.statusCode ?? 500,\r\n userId: userProfile.getUserId() ?? \"unknown\",\r\n ip: userProfile.getIp() ?? \"unknown\",\r\n userAgent: userProfile.getUserAgent() ?? \"unknown\",\r\n message: error.message ?? \"an error occurred\",\r\n stack: stackLine ?? \"no stack available\",\r\n });\r\n\r\n if (this.auditOptions.destinations?.includes(\"console\")) {\r\n logAuditEvent(item);\r\n }\r\n\r\n if (this.auditOptions.destinations?.includes(\"file\")) {\r\n\r\n const actionFile = this.defaultFileConfigs.find(x => x.fileName === \"error.log\") as TFileConfig;\r\n const file = this.auditOptions.splitFiles ? actionFile : this.fileConfig;\r\n\r\n if (!file.fullPath) {\r\n this.auditOptions.logger?.error(chalk.red(\"Unable to locate file path\"));\r\n return;\r\n }\r\n\r\n logAuditEvent(item, file);\r\n }\r\n\r\n }\r\n\r\n /**\r\n * Audits a given schema model by invoking the `auditModel` function with the current configuration,\r\n * a generated timestamp, and the provided schema.\r\n *\r\n * @template T - The type of the schema being audited.\r\n * @param schema - The schema object to be audited.\r\n */\r\n AuditModel<T>(schema: Schema<T>) {\r\n auditModel(schema);\r\n }\r\n\r\n /**\r\n * Configures the file logging settings for the auditor.\r\n *\r\n * This method sets up the file configuration used for logging audit events to a file.\r\n * It checks if \"file\" is included in the destinations and if split file logging is disabled.\r\n * If these conditions are not met, it logs appropriate warnings and returns early.\r\n * Otherwise, it sets up the file configuration with the provided options or defaults.\r\n *\r\n * @param config - Optional configuration object for the file logger, excluding the `fullPath` property.\r\n * - `folderName` (optional): The folder where the log file will be stored. Defaults to `\"audit\"`.\r\n * - `fileName` (optional): The name of the log file. Defaults to `\"audit.log\"`.\r\n */\r\n SetFileConfig(config: Omit<TFileConfig, \"fullPath\">) {\r\n\r\n if (!this.auditOptions.destinations?.includes(\"file\")) {\r\n this.auditOptions.logger?.warn(chalk.yellowBright(\"You need to add file to destinations for this to work properly\"));\r\n return;\r\n }\r\n\r\n if (this.auditOptions.splitFiles) {\r\n this.auditOptions.logger?.info(chalk.yellow(\"Cannot configure file as it is not supported when using splitfile\"));\r\n return;\r\n }\r\n\r\n const folder = config?.folderName ?? \"audit\";\r\n const baseFileName = config?.fileName ?? \"audit\";\r\n const fileName = baseFileName.endsWith(\".log\") ? baseFileName : `${baseFileName}.log`;\r\n\r\n this.fileConfig = {\r\n ...config,\r\n folderName: folder,\r\n fileName,\r\n fullPath: \"\",\r\n };\r\n }\r\n\r\n /**\r\n * Logs all incoming requests using the configured logger.\r\n *\r\n * Depending on the `splitFiles` configuration, this method will either:\r\n * - Log requests to a dedicated \"request.log\" file if `splitFiles` is enabled.\r\n * - Log requests to the default log file otherwise.\r\n *\r\n * @returns The result of the `requestLogger` function, which handles the actual logging process.\r\n */\r\n RequestLogger(): SupportedLoggersRequest[F] {\r\n return requestLogger[this.auditOptions.framework!];\r\n }\r\n\r\n /**\r\n * Logs all errors using the configured error logger.\r\n *\r\n * If the `splitFiles` option is enabled in the configuration, errors are logged to a separate\r\n * \"error.log\" file as specified in the default file configurations. Otherwise, errors are logged\r\n * to the main file configuration.\r\n *\r\n * @returns The result of the error logger function, which handles error logging based on the current configuration.\r\n */\r\n ErrorLogger() {\r\n return errorLogger[this.auditOptions.framework!];\r\n }\r\n\r\n /**\r\n * Asynchronously creates and returns the UI component or handler based on the specified framework\r\n * in the audit options.\r\n *\r\n * @returns {Promise<any>} A promise that resolves to the UI component or handler corresponding to the selected framework.\r\n */\r\n CreateUI() {\r\n\r\n if (!AppConfig.getUseUI()) {\r\n AppConfig.getAuditOption()?.logger?.info(chalk.yellow(\"Add the useUI option in the constructor to download the dependency\"));\r\n return UIRouter[this.auditOptions.framework!];\r\n }\r\n\r\n return UIRouter[this.auditOptions.framework!];\r\n\r\n }\r\n\r\n\r\n private CreateFileLocation = (config: TFileConfig) => {\r\n if (!this.auditOptions.destinations?.includes(\"file\")) return;\r\n\r\n if (this.auditOptions.splitFiles) {\r\n this.defaultFileConfigs.forEach(item => {\r\n this.GenerateFile(item);\r\n });\r\n return;\r\n }\r\n this.GenerateFile(config);\r\n };\r\n\r\n\r\n private GenerateFile = (config: TFileConfig) => {\r\n const dir = createFile(config);\r\n config.fullPath = dir;\r\n this.logFilePath = dir;\r\n this.defaultFileConfigs = this.defaultFileConfigs.map(item => {\r\n if (item.fileName === config.fileName) {\r\n return { ...item, fullPath: dir };\r\n }\r\n return item;\r\n });\r\n };\r\n\r\n /**\r\n * Sets up system error handling and logging for audit purposes.\r\n * @example\r\n * handleSystemErrors()\r\n * Initializes processes to log exceptions, rejections, signals, and exits.\r\n * @description\r\n * - Listens for Node.js process events such as uncaught exceptions, unhandled rejections, SIGTERM, SIGINT, and exit.\r\n * - Utilizes the `generateAuditContent` utility to format the logs.\r\n * - Logs generated content using `handleLog` with the error file configuration.\r\n * - Only activates if `captureSystemErrors` is true within audit options.\r\n */\r\n private HandleSystemErrors = () => {\r\n if (!this.auditOptions.captureSystemErrors) return;\r\n const file = getFileLocation(\"error.log\");\r\n process.on(\"uncaughtException\", (error, origin) => {\r\n const fullStack = error instanceof Error ? error.stack : undefined;\r\n const content = generateAuditContent({\r\n type: \"error\",\r\n action: \"unknown\",\r\n message: error.message ?? \"an uncaughtException\",\r\n outcome: \"uncaughtException\",\r\n error,\r\n origin,\r\n fullStack,\r\n });\r\n handleLog(file, content);\r\n });\r\n process.on(\"unhandledRejection\", (reason) => {\r\n const content = generateAuditContent({\r\n type: \"error\",\r\n action: \"unknown\",\r\n message: \"an unhandledRejection\",\r\n outcome: \"unhandledRejection\",\r\n reason,\r\n });\r\n handleLog(file, content);\r\n });\r\n\r\n process.on(\"SIGTERM\", () => {\r\n const content = generateAuditContent({\r\n type: \"signal\",\r\n action: \"terminated\",\r\n message: \"was terminated\",\r\n outcome: \"SIGTERM\",\r\n signal: \"SIGTERM\",\r\n });\r\n handleLog(file, content);\r\n process.exit(0);\r\n });\r\n\r\n process.on(\"SIGINT\", () => {\r\n const content = generateAuditContent({\r\n type: \"signal\",\r\n action: \"terminated\",\r\n message: \"app was terminated\",\r\n outcome: \"SIGINT\",\r\n signal: \"SIGINT\",\r\n });\r\n handleLog(file, content);\r\n process.exit(0);\r\n });\r\n\r\n\r\n process.on(\"exit\", (code) => {\r\n const content = generateAuditContent({\r\n type: \"system\",\r\n action: \"exit\",\r\n message: \"app was exited\",\r\n outcome: \"exit\",\r\n code,\r\n });\r\n handleLog(file, content);\r\n });\r\n\r\n };\r\n}\r\n","import chalk from \"chalk\";\r\nimport { Schema } from \"mongoose\";\r\nimport { AppConfig } from \"../../core/AppConfigs\";\r\nimport { checkForModule, generateAuditContent, getFileLocation, getTimeStamp, logAuditEvent } from \"../../utils\";\r\nimport { userProfile } from \"../../utils/user\";\r\n\r\n\r\n\r\nlet hasMongoose = false;\r\n\r\n\r\n/**\r\n * Checks if the \"mongoose\" module is available in the current environment.\r\n *\r\n * @returns {boolean} Returns `true` if the \"mongoose\" module is found, otherwise `false`.\r\n */\r\nexport const checkForMongodb = () => {\r\n hasMongoose = checkForModule(\"mongoose\");\r\n return hasMongoose;\r\n};\r\n\r\n/**\r\n * Enhances a Mongoose schema with auditing hooks for logging database operations.\r\n *\r\n * @template T - The type of the schema's document.\r\n * @param config - The audit configuration options, including a logger.\r\n * @param timeStamp - A string representing the timestamp to include in audit logs.\r\n * @param schema - The Mongoose schema to augment with audit hooks.\r\n *\r\n * @remarks\r\n * - Requires Mongoose to be installed; logs an error if not present.\r\n * - Adds pre-save and post-save hooks to log \"create\" or \"update\" actions.\r\n * - Adds a post-find hook to log \"find\" actions.\r\n * - Logs include model name, collection name, document ID, and timestamp.\r\n */\r\nexport const auditModel = <T>(schema: Schema<T>) => {\r\n const config = AppConfig.getAuditOption()!;\r\n if (config.dbType === \"none\") {\r\n config.logger?.error(chalk.yellow(\"Cannot audit db while DB type is set to none\"));\r\n return;\r\n }\r\n if (!hasMongoose) {\r\n config.logger?.error(chalk.red(\"Please install mongoose to use audit\"));\r\n return;\r\n }\r\n\r\n handleSaveSchema(schema);\r\n handleFindSchema(schema);\r\n handleUpdateSchema(schema);\r\n handleDeletingSchema(schema);\r\n};\r\n\r\nconst handleSaveSchema = (schema: Schema) => {\r\n const log = generateLog();\r\n\r\n schema.pre('save', function (next) {\r\n (this as any)._wasNew = this.isNew;\r\n next();\r\n });\r\n\r\n schema.post(\"save\", (doc: any) => {\r\n const { modelName } = doc.constructor;\r\n const message = `doc was ${doc._wasNew ? \"created\" : \"updated\"}`;\r\n log(\"save\", modelName, {}, message);\r\n });\r\n};\r\n\r\nconst handleFindSchema = (schema: Schema) => {\r\n const log = generateLog();\r\n\r\n schema.post(\"find\", function (docs) {\r\n const modelName = this.model.collection.name;\r\n const message = `looking for ${modelName}`;\r\n log(\"read\", modelName, this.getFilter(), message, docs.length);\r\n });\r\n\r\n schema.post(\"findOne\", function (doc: any) {\r\n const modelName = this.model.collection.name;\r\n const message = `looking for a single ${modelName}`;\r\n log(\"read\", modelName, this.getFilter(), message);\r\n });\r\n};\r\n\r\nconst handleUpdateSchema = (schema: Schema) => {\r\n const log = generateLog();\r\n schema.post(\"updateOne\", function (doc: any) {\r\n const modelName = this.model.collection.name;\r\n const message = `updating a single ${modelName} doc`;\r\n log(\"update\", modelName, this.getFilter(), message);\r\n });\r\n\r\n schema.post(\"findOneAndUpdate\", function (doc: any) {\r\n const modelName = this.model.collection.name;\r\n const message = `finding & updating a single ${modelName} doc`;\r\n log(\"update\", modelName, this.getFilter(), message);\r\n\r\n });\r\n\r\n schema.post(\"updateMany\", function (docs) {\r\n const modelName = this.model.collection.name;\r\n const message = `updating multiple ${modelName} docs`;\r\n log(\"update\", modelName, this.getFilter(), message, docs.modifiedCount);\r\n });\r\n};\r\n\r\n/**\r\n* Sets up Mongoose schema middleware to log deletion operations.\r\n* @example\r\n* handleDeletingSchema(userSchema)\r\n* // No return value; sets up post-hooks on provided schema\r\n* @param {Schema} schema - Represents the Mongoose schema on which to add audit logging for delete operations.\r\n* @returns {void} No return value; sets up middleware on the provided schema.\r\n* @description\r\n* - Utilizes Mongoose post-hooks to capture delete operations like `findOneAndDelete`, `deleteOne`, and `deleteMany`.\r\n* - Logs details of delete operations such as model name, filter criteria, action type, and affected document count if applicable.\r\n* - Requires the `generateLog` function to generate log entries which depend on user profile and configuration settings.\r\n* - Ensures that information related to each delete operation is audited and possibly persisted based on audit configuration options.\r\n*/\r\nconst handleDeletingSchema = (schema: Schema) => {\r\n const log = generateLog();\r\n schema.post(\"findOneAndDelete\", function () {\r\n const modelName = this.model.collection.name;\r\n const message = `finding & deleting a single ${modelName} doc`;\r\n log(\"delete\", modelName, this.getFilter(), message);\r\n });\r\n\r\n schema.post(\"deleteOne\", function () {\r\n const modelName = this.model.collection.name;\r\n const message = `deleting a single ${modelName} doc`;\r\n log(\"delete\", modelName, this.getFilter(), message);\r\n });\r\n\r\n schema.post(\"deleteMany\", function (docs) {\r\n const modelName = this.model.collection.name;\r\n const message = `deleting multiple ${modelName} docs`;\r\n log(\"delete\", modelName, this.getFilter(), message, docs.deletedCount);\r\n });\r\n};\r\n\r\n\r\ntype TdbAction = \"save\" | \"read\" | \"update\" | \"delete\";\r\nconst generateLog = () => {\r\n const config = AppConfig.getAuditOption()!;\r\n\r\n return (action: TdbAction, modelName: string, filter: any, message: string, length?: number) => {\r\n const content = generateAuditContent({\r\n type: \"db\",\r\n action,\r\n collection: modelName,\r\n criteria: filter,\r\n ...(length !== undefined ? { resultCount: length } : {}),\r\n message,\r\n userId: userProfile.getUserId(),\r\n endPoint: userProfile.getEndPoint(),\r\n ip: userProfile.getIp(),\r\n userAgent: userProfile.getUserAgent(),\r\n ...(config.useTimeStamp ? { timeStamp: getTimeStamp() } : {}),\r\n });\r\n\r\n if (config.destinations?.includes(\"console\"))\r\n logAuditEvent(content);\r\n\r\n const dbFile = getFileLocation(\"db.log\");\r\n\r\n if (!dbFile) return;\r\n\r\n if (config.destinations?.includes(\"file\"))\r\n logAuditEvent(content, dbFile);\r\n };\r\n};","import { Framework, TAuditOptions, TFileConfig } from \"../types\";\r\n\r\n/**\r\n * Singleton configuration manager for the application.\r\n * \r\n * Provides getter and setter methods for managing global configuration options such as:\r\n * - Audit options (`TAuditOptions`)\r\n * - Log file path\r\n * - File configuration (`TFileConfig`)\r\n * - Default file configurations (`TFileConfig[]`)\r\n * - Initialization state\r\n * \r\n * @remarks\r\n * This object encapsulates configuration state and exposes methods to mutate and retrieve configuration values.\r\n * \r\n * @example\r\n * ```typescript\r\n * AppConfig.setLogFilePath('/var/log/app.log');\r\n * const logPath = AppConfig.getLogFilePath();\r\n * ```\r\n */\r\nexport const AppConfig = (<F extends Framework>() => {\r\n let logFilePath: string = '';\r\n let fileConfig: TFileConfig | undefined;\r\n let auditOption: TAuditOptions<F> | undefined;\r\n let isInitialized = false;\r\n let captureSystemErrors = true;\r\n let useUI = false;\r\n let defaultFileConfigs: TFileConfig[] | undefined;\r\n let framework: Framework = \"express\";\r\n\r\n return {\r\n setAuditOption(options: TAuditOptions<F>) {\r\n auditOption = options;\r\n },\r\n getAuditOption() {\r\n return auditOption;\r\n },\r\n\r\n setLogFilePath(value: string) {\r\n logFilePath = value;\r\n },\r\n getLogFilePath() {\r\n return logFilePath;\r\n },\r\n\r\n setFileConfig(config: TFileConfig) {\r\n fileConfig = config;\r\n },\r\n getFileConfig() {\r\n return fileConfig;\r\n },\r\n\r\n setDefaultFileConfig(config: TFileConfig[]) {\r\n defaultFileConfigs = config;\r\n },\r\n getDefaultFileConfig() {\r\n return defaultFileConfigs;\r\n },\r\n\r\n setIsInitialized(value: boolean) {\r\n isInitialized = value;\r\n },\r\n getIsInitialized() {\r\n return isInitialized;\r\n },\r\n setCaptureSystemErrors(value: boolean) {\r\n captureSystemErrors = value;\r\n },\r\n getCaptureSystemErrors() {\r\n return captureSystemErrors;\r\n },\r\n setFrameWork(value: Framework) {\r\n framework = value;\r\n },\r\n getFrameWork() {\r\n return framework;\r\n },\r\n setUseUI(value: boolean) {\r\n useUI = value;\r\n },\r\n getUseUI() {\r\n return useUI;\r\n },\r\n };\r\n})();","import chalk from 'chalk';\r\nimport { Request } from 'express';\r\nimport fs from \"fs\";\r\nimport path from \"path\";\r\nimport { AppConfig } from '../core/AppConfigs';\r\nimport { AuditContentParams, TFileConfig } from '../types';\r\nimport crypto from \"crypto\";\r\nimport { createRequire } from 'module';\r\n\r\nexport const getTimeStamp = () => new Date().toISOString();\r\nexport const getUserId = (req: Request) => {\r\n try {\r\n if (\"user\" in req) {\r\n const user = req?.user as { id: string; } | { _id: string; };\r\n if (!user) return \"unknown\";\r\n if (\"id\" in user) {\r\n return user;\r\n } else return \"unknown\";\r\n }\r\n else \"unknown\";\r\n return \"unknown\";\r\n } catch (error) {\r\n return \"unknown\";\r\n }\r\n};\r\n\r\nexport const handleLog = (fileConfig: TFileConfig, content: any) => {\r\n const config = AppConfig.getAuditOption()!;\r\n\r\n if (config.destinations?.includes(\"console\"))\r\n logAuditEvent(content);\r\n if (config.destinations?.includes(\"file\"))\r\n logAuditEvent(content, fileConfig);\r\n};\r\n\r\nexport const saveToFile = (file: TFileConfig, content: any) => {\r\n const config = AppConfig.getAuditOption()!;\r\n\r\n try {\r\n fs.appendFileSync(file.fullPath, `${JSON.stringify(content)}\\n`, { encoding: \"utf-8\" });\r\n\r\n } catch (error) {\r\n config?.logger?.error(chalk.red(\"Failed to save log to file\"));\r\n }\r\n};\r\n\r\nexport const createFile = (config: TFileConfig) => {\r\n const fullPath = path.join(process.cwd(), config.folderName);\r\n if (!fs.existsSync(fullPath)) {\r\n fs.mkdirSync(fullPath, { recursive: true });\r\n }\r\n\r\n const dir = path.join(fullPath, config.fileName);\r\n\r\n return dir;\r\n};\r\n\r\nexport const logAuditEvent = (content: any, file?: TFileConfig) => {\r\n const config = AppConfig.getAuditOption()!;\r\n if (file) {\r\n saveToFile(file, content);\r\n return;\r\n }\r\n\r\n const cleanContent = { ...content };\r\n delete cleanContent.fullStack;\r\n\r\n config?.logger?.info(cleanContent);\r\n};\r\n\r\nexport const getFileLocation = (location: string) => {\r\n const config = AppConfig.getAuditOption()!;\r\n const defaultFileConfigs = AppConfig.getDefaultFileConfig()!;\r\n\r\n if (config.splitFiles) {\r\n const dbFile = defaultFileConfigs.find(x => x.fileName === location) as TFileConfig;\r\n return dbFile;\r\n }\r\n\r\n return AppConfig.getFileConfig()!;\r\n};\r\n\r\nexport const generateAuditContent = ({\r\n type,\r\n action,\r\n message,\r\n ...rest\r\n}: AuditContentParams) => {\r\n return {\r\n id: generateId(),\r\n type,\r\n action,\r\n message,\r\n ...rest,\r\n ...(AppConfig.getAuditOption()?.useTimeStamp ? { timeStamp: getTimeStamp() } : {}),\r\n };\r\n};\r\n\r\nexport const generateId = () => {\r\n const time = Date.now().toString(36);\r\n const rand = crypto.randomBytes(3).toString('hex');\r\n return `${time}-${rand}`;\r\n};\r\n\r\nexport const checkForModule = (item: string) => {\r\n const requireFromUserProject = createRequire(path.join(process.cwd(), 'index.js'));\r\n\r\n try {\r\n AppConfig.getAuditOption()?.logger?.info(\r\n chalk.blueBright(`Checking for ${item}`),\r\n );\r\n requireFromUserProject.resolve(item);\r\n AppConfig.getAuditOption()?.logger?.info(\r\n chalk.greenBright(`${item} found.`),\r\n );\r\n return true;\r\n } catch (error) {\r\n AppConfig.getAuditOption()?.logger?.info(\r\n chalk.redBright(`Failed to find \"${item}\"`),\r\n );\r\n return false;\r\n }\r\n};\r\n\r\n\r\n","/**\r\n * Represents a user profile containing identifying and network information.\r\n *\r\n * @remarks\r\n * The `UserProfile` class encapsulates a user's ID, endpoint URL, and IP address.\r\n * It provides methods to build and retrieve these properties.\r\n *\r\n * @example\r\n * ```typescript\r\n * const profile = new UserProfile();\r\n * profile.BuildProfile('123', 'https://api.example.com', '192.168.1.1');\r\n * console.log(profile.getUserId()); // '123'\r\n * ```\r\n */\r\nclass UserProfile {\r\n private userId: string;\r\n private endPoint: string;\r\n private ip: string;\r\n private userAgent: string;\r\n\r\n constructor() {\r\n this.userId = \"\";\r\n this.endPoint = \"\";\r\n this.ip = \"\";\r\n this.userAgent = \"\";\r\n }\r\n\r\n BuildProfile(id: string, url: string, ip: string, userAgent: string) {\r\n this.userId = id;\r\n this.endPoint = url;\r\n this.ip = ip;\r\n this.userAgent = userAgent;\r\n }\r\n\r\n getUserId() {\r\n return this.userId;\r\n }\r\n getEndPoint() {\r\n return this.endPoint;\r\n }\r\n getIp() {\r\n return this.ip;\r\n }\r\n getUserAgent() {\r\n return this.userAgent;\r\n }\r\n}\r\n\r\n\r\nconst userProfile = new UserProfile();\r\n\r\nexport { userProfile };","import { NextFunction, Request, Response } from 'express';\r\nimport { FastifyReply, HookHandlerDoneFunction } from \"fastify\";\r\nimport { Context, Next } from \"koa\";\r\nimport { SupportedLoggersRequest } from '../types';\r\nimport { generateAuditContent, getFileLocation, getUserId, handleLog } from '../utils';\r\nimport { ExtendedFastifyRequest } from '../utils/interface';\r\nimport { userProfile } from '../utils/user';\r\n\r\n\r\n/**\r\n* Middleware function for logging HTTP request details with express framework.\r\n* @example\r\n* expressLogger(req, res, next)\r\n* No return value as it is middleware.\r\n* @param {Request} req - Express request object containing HTTP request details.\r\n* @param {Response} res - Express response object to capture status and headers.\r\n* @param {NextFunction} next - Callback to pass control to the next middleware or handler.\r\n* @returns {void} This function does not return a value, it passes control to the next middleware.\r\n* @description\r\n* - The function initializes logging parameters such as start time, route, IP, user agent, and user ID.\r\n* - Utilizes helper methods like `getUserId`, `BuildProfile`, `getFileLocation`, and `handleLog`.\r\n* - Suppress audit feature checks the response object flag to alter logging behavior.\r\n* - Configurable timestamp inclusion based on application settings.\r\n*/\r\nconst expressLogger = (req: Request, res: Response, next: NextFunction) => {\r\n const file = getFileLocation(\"request.log\");\r\n const start = Date.now();\r\n const route = req.originalUrl;\r\n const ip = req.ip ?? \"unknown\";\r\n const userAgent = req.headers['user-agent'] || '';\r\n const user = getUserId(req);\r\n const id = (typeof user === \"string\") ? user : user ? user?.id : \"unknown\";\r\n\r\n userProfile.BuildProfile(id, route, ip, userAgent);\r\n\r\n res.on('finish', () => {\r\n const duration = Date.now() - start;\r\n\r\n if ((res as any)._suppressAudit) {\r\n const content = generateAuditContent({\r\n type: \"request\",\r\n action: \"incoming request\",\r\n message: req.statusMessage ?? res.statusMessage ?? `[${route}]||[${req.method}]||[${[res.statusCode]}]`,\r\n outcome: \"failure\",\r\n duration,\r\n method: req.method,\r\n statusCode: res.statusCode || 500,\r\n ip,\r\n route,\r\n userAgent,\r\n userId: id,\r\n });\r\n handleLog(file, content);\r\n return;\r\n }\r\n const content = generateAuditContent({\r\n type: \"request\",\r\n action: \"incoming request\",\r\n outcome: \"success\",\r\n duration,\r\n method: req.method,\r\n statusCode: res.statusCode || 200,\r\n message: req.statusMessage ?? res.statusMessage ?? `[${route}]||[${req.method}]||[${[res.statusCode]}]`,\r\n route: route,\r\n statusMessage: res.statusMessage || \"success\",\r\n ip,\r\n userAgent,\r\n userId: id,\r\n });\r\n handleLog(file, content);\r\n });\r\n next();\r\n};\r\n\r\n/**\r\n* Sets up Fastify request logging middleware functions.\r\n* @example\r\n* fastifyLogger()\r\n* No return value as it is middleware.\r\n* @returns {object} An object with `onRequest` and `OnResponse` methods for lifecycle event handling.\r\n* @description\r\n* - Initializes request start time and calculates duration for response.\r\n* - Incorporates failure condition for logging based on status code.\r\n* - Logs essential request details using `handleLog` function.\r\n* - Configurable timestamp inclusion via global settings.\r\n*/\r\nconst fastifyLogger = {\r\n onRequest: (request: ExtendedFastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => {\r\n request.startTime = Date.now();\r\n const id = request.userId ?? \"unknown\";\r\n const route = request.url;\r\n const { ip } = request;\r\n const userAgent = request.headers['user-agent'] || '';\r\n\r\n userProfile.BuildProfile(id, route, ip, userAgent);\r\n\r\n done();\r\n },\r\n onResponse: (request: ExtendedFastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => {\r\n const file = getFileLocation(\"request.log\");\r\n const duration = Date.now() - (request.startTime || Date.now());\r\n const route = userProfile.getEndPoint() ?? \"unknown\";\r\n const content = generateAuditContent({\r\n type: \"request\",\r\n action: \"incoming request\",\r\n duration,\r\n method: request.method,\r\n statusCode: reply.statusCode || 500,\r\n message: `[${route}]||[${request.method}]||[${[reply.statusCode]}]`,\r\n ip: userProfile.getIp() ?? \"unknown\",\r\n route,\r\n userAgent: userProfile.getUserAgent() ?? \"unknown\",\r\n userId: request.userId ?? \"unknown\",\r\n });\r\n handleLog(file, content);\r\n done();\r\n },\r\n};\r\n\r\n/**\r\n* Middleware function for Koa that logs HTTP request details and audits requests.\r\n* @example\r\n* async koaLogger(ctx, next)\r\n* No return value as it is middleware.\r\n* @param {Context} ctx - Koa context object containing HTTP request and response details.\r\n* @param {Next} next - Function to pass control to the next middleware or handler.\r\n* @returns {Promise<void>} This function does not return a value, it awaits the next middleware.\r\n* @description\r\n* - Extracts user ID from the Koa context state and defaults to \"unknown\" if absent.\r\n* - Calculates duration of request handling for performance auditing.\r\n* - Configures log content including request and response details.\r\n* - Utilizes helper methods like `getTimeStamp` and `handleLog`.\r\n*/\r\nconst koaLogger = async (ctx: Context, next: Next) => {\r\n const file = getFileLocation(\"request.log\");\r\n const start = Date.now();\r\n const userId = ctx.state.userId ?? \"unknown\";\r\n const route = ctx.request.url;\r\n const ip = ctx.request.ip ?? \"unknown\";\r\n const userAgent = ctx.request.headers[\"user-agent\"] || \"\";\r\n userProfile.BuildProfile(userId, route, ip, userAgent);\r\n\r\n try {\r\n await next();\r\n } finally {\r\n const duration = Date.now() - start;\r\n\r\n const content = generateAuditContent({\r\n type: \"request\",\r\n action: \"incoming request\",\r\n duration,\r\n method: ctx.request.method,\r\n statusCode: ctx.res.statusCode || 500,\r\n message: ctx.message ?? `[${route}]||[${ctx.request.method}]||[${[ctx.status]}]`,\r\n ip,\r\n route,\r\n userAgent,\r\n userId,\r\n });\r\n handleLog(file, content);\r\n }\r\n};\r\n\r\nexport const requestLogger: SupportedLoggersRequest = {\r\n express: expressLogger,\r\n fastify: fastifyLogger,\r\n koa: koaLogger,\r\n};","import { NextFunction, Request, Response } from 'express';\r\nimport { FastifyError, FastifyReply } from 'fastify';\r\nimport { Context, Next } from 'koa';\r\nimport { generateAuditContent, getFileLocation, getUserId, handleLog } from '../utils';\r\nimport { ExtendedFastifyRequest } from '../utils/interface';\r\n\r\n\r\nconst expressErrorLogger = (err: any, req: Request, res: Response, next: NextFunction) => {\r\n const file = getFileLocation(\"error.log\");\r\n\r\n (res as any)._suppressAudit = true;\r\n const user = getUserId(req);\r\n let stackLine = \"\";\r\n if (err.stack) {\r\n const lines = err.stack.split('\\n');\r\n stackLine = lines.length > 1 ? lines[1].trim() : lines[0]?.trim() ?? \"\";\r\n }\r\n const content = generateAuditContent({\r\n type: \"error\",\r\n action: \"request failed\",\r\n method: req.method,\r\n statusCode: res.statusCode >= 400 ? res.statusCode : 500,\r\n route: req.originalUrl,\r\n statusMessage: res.statusMessage || \"Internal Server Error\",\r\n ip: req.ip ?? \"unknown\",\r\n userAgent: req.headers['user-agent'],\r\n message: err.message,\r\n stack: stackLine,\r\n fullStack: err.stack ?? \"Invalid\",\r\n userId: (typeof user === \"string\") ? user : user ? user?.id : \"unknown\",\r\n });\r\n\r\n handleLog(file, content);\r\n\r\n next(err); // pass to default error handler\r\n};\r\n\r\nconst fastifyErrorLogger = (error: FastifyError, request: ExtendedFastifyRequest, reply: FastifyReply) => {\r\n const file = getFileLocation(\"error.log\");\r\n let stackLine = \"\";\r\n if (error.stack) {\r\n const lines = error.stack?.split('\\n');\r\n stackLine = lines ? lines.length > 1 ? lines[1].trim() : lines[0]?.trim() ?? \"\" : error.message;\r\n }\r\n\r\n const content = generateAuditContent({\r\n type: \"error\",\r\n action: \"request failed\",\r\n method: request.method,\r\n statusCode: reply.statusCode || 500,\r\n route: request.url,\r\n ip: request.ip ?? \"unknown\",\r\n userAgent: request.headers['user-agent'],\r\n message: error.message,\r\n stack: stackLine,\r\n fullStack: error.stack ?? \"Invalid\",\r\n userId: request.userId ?? \"unknown\",\r\n });\r\n\r\n handleLog(file, content);\r\n return reply.send(error);\r\n};\r\n\r\nconst koaErrorLogger = async (ctx: Context, next: Next) => {\r\n const file = getFileLocation(\"error.log\");\r\n const userId = ctx.state.userId ?? \"unknown\";\r\n\r\n try {\r\n await next();\r\n } catch (error: any) {\r\n\r\n if (ctx.status >= 400) {\r\n let stackLine = \"\";\r\n if ((error as any).stack) {\r\n const lines = (error as any).stack.split('\\n');\r\n stackLine = lines ? lines.length > 1 ? lines[1].trim() : lines[0]?.trim() ?? null : (error as any).message;\r\n }\r\n\r\n const content = generateAuditContent({\r\n type: \"error\",\r\n action: \"request failed\",\r\n method: ctx.method,\r\n statusCode: ctx.statusCode || 500,\r\n message: (error as any).message,\r\n route: ctx.url,\r\n ip: ctx.ip ?? \"unknown\",\r\n userAgent: ctx.headers['user-agent'],\r\n stack: stackLine,\r\n fullStack: error.stack ?? \"Invalid\",\r\n userId,\r\n });\r\n\r\n handleLog(file, content);\r\n }\r\n }\r\n};\r\n\r\n\r\nexport const errorLogger = {\r\n express: expressErrorLogger,\r\n fastify: fastifyErrorLogger,\r\n koa: koaErrorLogger,\r\n};\r\n\r\n\r\n","import chalk from \"chalk\";\r\nimport fs, { createWriteStream, existsSync } from 'fs';\r\nimport path from \"path\";\r\nimport { Readable } from 'stream';\r\nimport { pipeline } from 'stream/promises';\r\nimport { fileURLToPath } from \"url\";\r\nimport { AppConfig } from \"../core/AppConfigs\";\r\nimport { checkForModule } from \"../utils\";\r\n\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\nconst uiPath = path.join(__dirname, \"ui\");\r\n\r\n\r\nconst getLogs = () => {\r\n const hasSplitFiles = AppConfig.getAuditOption()?.splitFiles;\r\n const file = AppConfig.getFileConfig();\r\n\r\n if (hasSplitFiles) {\r\n const files = AppConfig.getDefaultFileConfig();\r\n if (!files) return [];\r\n\r\n const data = files.flatMap((item) => {\r\n const logData = fs.readFileSync(item.fullPath, \"utf-8\");\r\n return logData.trim().split(\"\\n\").filter(Boolean).map((line, i) => ({\r\n id: i,\r\n ...JSON.parse(line),\r\n }));\r\n });\r\n\r\n return data.sort((a, b) => b.timeStamp.localeCompare(a.timeStamp));\r\n }\r\n\r\n if (!file) return [];\r\n\r\n const logData = fs.readFileSync(file.fullPath, \"utf-8\");\r\n const item = logData.trim().split(\"\\n\").filter(Boolean).map((line, i) => ({\r\n id: i,\r\n ...JSON.parse(line),\r\n }));\r\n\r\n return item.sort((a, b) => b.timeStamp.localeCompare(a.timeStamp));\r\n};\r\n\r\n\r\n/**\r\n * Asynchronously creates and configures an Express router for serving the audit UI and logs.\r\n *\r\n * - Dynamically imports the `express` module. If `express` is not installed, logs a message and returns a no-op middleware.\r\n * - Serves static files from the `uiPath` directory.\r\n * - Handles GET requests to `/audit-ui` by serving the main UI HTML file.\r\n * - Handles GET requests to `/audit-log` by returning audit logs as JSON.\r\n *\r\n * @returns {Promise<import(\"express\").Router | import(\"express\").RequestHandler>} \r\n * A Promise that resolves to an Express router instance, or a no-op middleware if `express` is not available.\r\n */\r\nconst expressRouter = async () => {\r\n let express;\r\n try {\r\n express = await import(\"express\");\r\n } catch (error) {\r\n AppConfig.getAuditOption()?.logger?.info(chalk.redBright(\"Please install express in order to use this module\"));\r\n return (req: any, res: any, next: any) => next();\r\n }\r\n\r\n const router = express.Router();\r\n\r\n router.use(express.static(uiPath));\r\n\r\n router.get('/audit-ui', (_req, res) => {\r\n res.sendFile(path.join(uiPath, \"index.html\"));\r\n });\r\n\r\n router.get('/audit-log', (_req, res) => {\r\n const logs = getLogs();\r\n res.status(200).json({ logs });\r\n });\r\n\r\n return router;\r\n};\r\n\r\n/**\r\n * Asynchronously creates a Fastify plugin for serving static files and API endpoints.\r\n *\r\n * This function attempts to dynamically import the `@fastify/static` module. If the import fails,\r\n * it returns a no-op async function. Otherwise, it returns an async function that registers the static\r\n * file handler and sets up two routes:\r\n * \r\n * - `/audit-ui`: Serves the `index.html` file as an HTML response.\r\n * - `/audit-log`: Returns audit logs as a JSON object.\r\n *\r\n * @returns {Promise<(fastify: any, opts: any) => Promise<void>>} A promise that resolves to a Fastify plugin function.\r\n */\r\nconst fastifyRouter = async () => {\r\n\r\n let fastifyStatic;\r\n try {\r\n fastifyStatic = await import('@fastify/static');\r\n } catch {\r\n return async () => { };\r\n }\r\n\r\n return async function (fastify: any, opts: any) {\r\n await fastify.register(fastifyStatic.default, {\r\n root: uiPath,\r\n prefix: '/',\r\n });\r\n\r\n /**\r\n * Sends an HTML file as the response for the '/audit-ui' route in Fastify.\r\n * @example\r\n * (_req, reply) => {\r\n * reply.type('text/html').sendFile('index.html');\r\n * }\r\n * @param {any} _ - The incoming request object (not used in this function).\r\n * @param {any} reply - The Fastify reply object used to send responses.\r\n * @description\r\n * - This function sets the response type to 'text/html'.\r\n * - It sends 'index.html' located in the static files directory specified in uiPath.\r\n */\r\n fastify.get('/audit-ui', (_: any, reply: any) => {\r\n reply.type('text/html').sendFile('index.html');\r\n });\r\n\r\n fastify.get('/audit-log', (_: any, reply: any) => {\r\n reply.send({ logs: getLogs() });\r\n });\r\n };\r\n};\r\n\r\n/**\r\n* Initializes a Koa router for serving audit-related endpoints\r\n* @example\r\n* koaRouter()\r\n* @returns {Function} Middleware function composed with Koa static server and router.\r\n* @description\r\n* - Dynamically imports necessary Koa modules and registers routes.\r\n* - Serves the audit UI from a static HTML file.\r\n* - Returns the audit logs in JSON format.\r\n* - Logs a message if required packages are not installed.\r\n*/\r\nconst koaRouter = async () => {\r\n let Router, serve, compose;\r\n\r\n try {\r\n Router = (await import('@koa/router')).default;\r\n serve = (await import('koa-static')).default;\r\n compose = (await import('koa-compose')).default;\r\n } catch {\r\n AppConfig.getAuditOption()?.logger?.info(\r\n chalk.redBright(\"Please install koa, @koa/router, koa-static, and koa-compose to use the audit UI.\"),\r\n );\r\n return async (ctx: any, next: any) => await next();\r\n }\r\n\r\n const router = new Router();\r\n\r\n\r\n router.get('/audit-ui', (ctx: any) => {\r\n ctx.type = 'html';\r\n ctx.body = fs.createReadStream(path.join(uiPath, 'index.html'));\r\n });\r\n\r\n router.get('/audit-log', (ctx: any) => {\r\n ctx.body = { logs: getLogs() };\r\n });\r\n\r\n return compose([\r\n serve(uiPath),\r\n router.routes(),\r\n router.allowedMethods(),\r\n ]);\r\n};\r\n\r\n\r\nexport const checkForFramework = () => {\r\n const activeFramework = AppConfig.getFrameWork() as string;\r\n return checkForModule(activeFramework);\r\n};\r\n\r\n\r\n/**\r\n * An object that maps supported web frameworks to their corresponding router implementations.\r\n *\r\n * @property {Router} express - The router implementation for Express.js.\r\n * @property {Router} fastify - The