feat(audit-logging): Implement Edge-compatible audit logging utility and safe logging module
- Added `auditLogEdge.js` for Edge Runtime compatible audit logging, including console logging and API fallback. - Introduced `auditLogSafe.js` for safe audit logging without direct database imports, ensuring compatibility across runtimes. - Enhanced `auth.js` to integrate safe audit logging for login actions, including success and failure cases. - Created middleware `auditLog.js` to facilitate audit logging for API routes with predefined configurations. - Updated `middleware.js` to allow API route access without authentication checks. - Added tests for audit logging functionality and Edge compatibility in `test-audit-logging.mjs` and `test-edge-compatibility.mjs`. - Implemented safe audit logging tests in `test-safe-audit-logging.mjs` to verify functionality across environments.
This commit is contained in:
235
src/lib/middleware/auditLog.js
Normal file
235
src/lib/middleware/auditLog.js
Normal file
@@ -0,0 +1,235 @@
|
||||
import { logApiAction, AUDIT_ACTIONS, RESOURCE_TYPES } from "@/lib/auditLog.js";
|
||||
|
||||
/**
|
||||
* Higher-order function to add audit logging to API routes
|
||||
* @param {Function} handler - The original API route handler
|
||||
* @param {Object} auditConfig - Audit logging configuration
|
||||
* @param {string} auditConfig.action - The audit action to log
|
||||
* @param {string} auditConfig.resourceType - The resource type being accessed
|
||||
* @param {Function} [auditConfig.getResourceId] - Function to extract resource ID from request/params
|
||||
* @param {Function} [auditConfig.getAdditionalDetails] - Function to get additional details to log
|
||||
* @returns {Function} Wrapped handler with audit logging
|
||||
*/
|
||||
export function withAuditLog(handler, auditConfig) {
|
||||
return async (request, context) => {
|
||||
try {
|
||||
// Execute the original handler first
|
||||
const response = await handler(request, context);
|
||||
|
||||
// Extract resource ID if function provided
|
||||
let resourceId = null;
|
||||
if (auditConfig.getResourceId) {
|
||||
resourceId = auditConfig.getResourceId(request, context, response);
|
||||
} else if (context?.params?.id) {
|
||||
resourceId = context.params.id;
|
||||
}
|
||||
|
||||
// Get additional details if function provided
|
||||
let additionalDetails = {};
|
||||
if (auditConfig.getAdditionalDetails) {
|
||||
additionalDetails = auditConfig.getAdditionalDetails(
|
||||
request,
|
||||
context,
|
||||
response
|
||||
);
|
||||
}
|
||||
|
||||
// Log the action
|
||||
logApiAction(
|
||||
request,
|
||||
auditConfig.action,
|
||||
auditConfig.resourceType,
|
||||
resourceId,
|
||||
request.session,
|
||||
additionalDetails
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Log failed actions
|
||||
const resourceId = auditConfig.getResourceId
|
||||
? auditConfig.getResourceId(request, context, null)
|
||||
: context?.params?.id || null;
|
||||
|
||||
logApiAction(
|
||||
request,
|
||||
`${auditConfig.action}_failed`,
|
||||
auditConfig.resourceType,
|
||||
resourceId,
|
||||
request.session,
|
||||
{
|
||||
error: error.message,
|
||||
...(auditConfig.getAdditionalDetails
|
||||
? auditConfig.getAdditionalDetails(request, context, null)
|
||||
: {}),
|
||||
}
|
||||
);
|
||||
|
||||
// Re-throw the error
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Predefined audit configurations for common actions
|
||||
*/
|
||||
export const AUDIT_CONFIGS = {
|
||||
// Project actions
|
||||
PROJECT_VIEW: {
|
||||
action: AUDIT_ACTIONS.PROJECT_VIEW,
|
||||
resourceType: RESOURCE_TYPES.PROJECT,
|
||||
},
|
||||
PROJECT_CREATE: {
|
||||
action: AUDIT_ACTIONS.PROJECT_CREATE,
|
||||
resourceType: RESOURCE_TYPES.PROJECT,
|
||||
getResourceId: (req, ctx, res) => res?.json?.projectId?.toString(),
|
||||
getAdditionalDetails: async (req) => {
|
||||
const data = await req.json();
|
||||
return { projectData: data };
|
||||
},
|
||||
},
|
||||
PROJECT_UPDATE: {
|
||||
action: AUDIT_ACTIONS.PROJECT_UPDATE,
|
||||
resourceType: RESOURCE_TYPES.PROJECT,
|
||||
getAdditionalDetails: async (req) => {
|
||||
const data = await req.json();
|
||||
return { updatedData: data };
|
||||
},
|
||||
},
|
||||
PROJECT_DELETE: {
|
||||
action: AUDIT_ACTIONS.PROJECT_DELETE,
|
||||
resourceType: RESOURCE_TYPES.PROJECT,
|
||||
},
|
||||
|
||||
// Task actions
|
||||
TASK_VIEW: {
|
||||
action: AUDIT_ACTIONS.TASK_VIEW,
|
||||
resourceType: RESOURCE_TYPES.TASK,
|
||||
},
|
||||
TASK_CREATE: {
|
||||
action: AUDIT_ACTIONS.TASK_CREATE,
|
||||
resourceType: RESOURCE_TYPES.TASK,
|
||||
getAdditionalDetails: async (req) => {
|
||||
const data = await req.json();
|
||||
return { taskData: data };
|
||||
},
|
||||
},
|
||||
TASK_UPDATE: {
|
||||
action: AUDIT_ACTIONS.TASK_UPDATE,
|
||||
resourceType: RESOURCE_TYPES.TASK,
|
||||
getAdditionalDetails: async (req) => {
|
||||
const data = await req.json();
|
||||
return { updatedData: data };
|
||||
},
|
||||
},
|
||||
TASK_DELETE: {
|
||||
action: AUDIT_ACTIONS.TASK_DELETE,
|
||||
resourceType: RESOURCE_TYPES.TASK,
|
||||
},
|
||||
|
||||
// Project Task actions
|
||||
PROJECT_TASK_VIEW: {
|
||||
action: AUDIT_ACTIONS.PROJECT_TASK_VIEW,
|
||||
resourceType: RESOURCE_TYPES.PROJECT_TASK,
|
||||
},
|
||||
PROJECT_TASK_CREATE: {
|
||||
action: AUDIT_ACTIONS.PROJECT_TASK_CREATE,
|
||||
resourceType: RESOURCE_TYPES.PROJECT_TASK,
|
||||
getAdditionalDetails: async (req) => {
|
||||
const data = await req.json();
|
||||
return { taskData: data };
|
||||
},
|
||||
},
|
||||
PROJECT_TASK_UPDATE: {
|
||||
action: AUDIT_ACTIONS.PROJECT_TASK_UPDATE,
|
||||
resourceType: RESOURCE_TYPES.PROJECT_TASK,
|
||||
getAdditionalDetails: async (req) => {
|
||||
const data = await req.json();
|
||||
return { updatedData: data };
|
||||
},
|
||||
},
|
||||
PROJECT_TASK_DELETE: {
|
||||
action: AUDIT_ACTIONS.PROJECT_TASK_DELETE,
|
||||
resourceType: RESOURCE_TYPES.PROJECT_TASK,
|
||||
},
|
||||
|
||||
// Contract actions
|
||||
CONTRACT_VIEW: {
|
||||
action: AUDIT_ACTIONS.CONTRACT_VIEW,
|
||||
resourceType: RESOURCE_TYPES.CONTRACT,
|
||||
},
|
||||
CONTRACT_CREATE: {
|
||||
action: AUDIT_ACTIONS.CONTRACT_CREATE,
|
||||
resourceType: RESOURCE_TYPES.CONTRACT,
|
||||
getAdditionalDetails: async (req) => {
|
||||
const data = await req.json();
|
||||
return { contractData: data };
|
||||
},
|
||||
},
|
||||
CONTRACT_UPDATE: {
|
||||
action: AUDIT_ACTIONS.CONTRACT_UPDATE,
|
||||
resourceType: RESOURCE_TYPES.CONTRACT,
|
||||
getAdditionalDetails: async (req) => {
|
||||
const data = await req.json();
|
||||
return { updatedData: data };
|
||||
},
|
||||
},
|
||||
CONTRACT_DELETE: {
|
||||
action: AUDIT_ACTIONS.CONTRACT_DELETE,
|
||||
resourceType: RESOURCE_TYPES.CONTRACT,
|
||||
},
|
||||
|
||||
// Note actions
|
||||
NOTE_VIEW: {
|
||||
action: AUDIT_ACTIONS.NOTE_VIEW,
|
||||
resourceType: RESOURCE_TYPES.NOTE,
|
||||
},
|
||||
NOTE_CREATE: {
|
||||
action: AUDIT_ACTIONS.NOTE_CREATE,
|
||||
resourceType: RESOURCE_TYPES.NOTE,
|
||||
getAdditionalDetails: async (req) => {
|
||||
const data = await req.json();
|
||||
return { noteData: data };
|
||||
},
|
||||
},
|
||||
NOTE_UPDATE: {
|
||||
action: AUDIT_ACTIONS.NOTE_UPDATE,
|
||||
resourceType: RESOURCE_TYPES.NOTE,
|
||||
getAdditionalDetails: async (req) => {
|
||||
const data = await req.json();
|
||||
return { updatedData: data };
|
||||
},
|
||||
},
|
||||
NOTE_DELETE: {
|
||||
action: AUDIT_ACTIONS.NOTE_DELETE,
|
||||
resourceType: RESOURCE_TYPES.NOTE,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to create audit-logged API handlers
|
||||
* @param {Object} handlers - Object with HTTP method handlers
|
||||
* @param {Object} auditConfig - Audit configuration for this route
|
||||
* @returns {Object} Object with audit-logged handlers
|
||||
*/
|
||||
export function createAuditedHandlers(handlers, auditConfig) {
|
||||
const auditedHandlers = {};
|
||||
|
||||
Object.entries(handlers).forEach(([method, handler]) => {
|
||||
// Get method-specific audit config or use default
|
||||
const config = auditConfig[method] || auditConfig.default || auditConfig;
|
||||
|
||||
auditedHandlers[method] = withAuditLog(handler, config);
|
||||
});
|
||||
|
||||
return auditedHandlers;
|
||||
}
|
||||
|
||||
const auditLogMiddleware = {
|
||||
withAuditLog,
|
||||
AUDIT_CONFIGS,
|
||||
createAuditedHandlers,
|
||||
};
|
||||
|
||||
export default auditLogMiddleware;
|
||||
Reference in New Issue
Block a user