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:
Chop
2025-07-09 23:08:16 +02:00
parent 90875db28b
commit b1a78bf7a8
20 changed files with 2943 additions and 130 deletions

View 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;