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

424
src/lib/auditLog.js Normal file
View File

@@ -0,0 +1,424 @@
/**
* Audit log actions - standardized action types
*/
export const AUDIT_ACTIONS = {
// Authentication
LOGIN: "login",
LOGOUT: "logout",
LOGIN_FAILED: "login_failed",
// Projects
PROJECT_CREATE: "project_create",
PROJECT_UPDATE: "project_update",
PROJECT_DELETE: "project_delete",
PROJECT_VIEW: "project_view",
// Tasks
TASK_CREATE: "task_create",
TASK_UPDATE: "task_update",
TASK_DELETE: "task_delete",
TASK_STATUS_CHANGE: "task_status_change",
// Project Tasks
PROJECT_TASK_CREATE: "project_task_create",
PROJECT_TASK_UPDATE: "project_task_update",
PROJECT_TASK_DELETE: "project_task_delete",
PROJECT_TASK_STATUS_CHANGE: "project_task_status_change",
// Contracts
CONTRACT_CREATE: "contract_create",
CONTRACT_UPDATE: "contract_update",
CONTRACT_DELETE: "contract_delete",
// Notes
NOTE_CREATE: "note_create",
NOTE_UPDATE: "note_update",
NOTE_DELETE: "note_delete",
// Admin actions
USER_CREATE: "user_create",
USER_UPDATE: "user_update",
USER_DELETE: "user_delete",
USER_ROLE_CHANGE: "user_role_change",
// System actions
DATA_EXPORT: "data_export",
BULK_OPERATION: "bulk_operation",
};
/**
* Resource types for audit logging
*/
export const RESOURCE_TYPES = {
PROJECT: "project",
TASK: "task",
PROJECT_TASK: "project_task",
CONTRACT: "contract",
NOTE: "note",
USER: "user",
SESSION: "session",
SYSTEM: "system",
};
/**
* Log an audit event
* @param {Object} params - Audit log parameters
* @param {string} params.action - Action performed (use AUDIT_ACTIONS constants)
* @param {string} [params.userId] - ID of user performing the action
* @param {string} [params.resourceType] - Type of resource affected (use RESOURCE_TYPES constants)
* @param {string} [params.resourceId] - ID of the affected resource
* @param {string} [params.ipAddress] - IP address of the user
* @param {string} [params.userAgent] - User agent string
* @param {Object} [params.details] - Additional details about the action
* @param {string} [params.timestamp] - Custom timestamp (defaults to current time)
*/
export async function logAuditEvent({
action,
userId = null,
resourceType = null,
resourceId = null,
ipAddress = null,
userAgent = null,
details = null,
timestamp = null,
}) {
try {
// Check if we're in Edge Runtime - if so, skip database operations
if (
typeof EdgeRuntime !== "undefined" ||
process.env.NEXT_RUNTIME === "edge"
) {
console.log(
`[Audit Log - Edge Runtime] ${action} by user ${
userId || "anonymous"
} on ${resourceType}:${resourceId}`
);
return;
}
// Dynamic import to avoid Edge Runtime issues
const { default: db } = await import("./db.js");
const auditTimestamp = timestamp || new Date().toISOString();
const detailsJson = details ? JSON.stringify(details) : null;
const stmt = db.prepare(`
INSERT INTO audit_logs (
user_id, action, resource_type, resource_id,
ip_address, user_agent, timestamp, details
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(
userId,
action,
resourceType,
resourceId,
ipAddress,
userAgent,
auditTimestamp,
detailsJson
);
console.log(
`Audit log: ${action} by user ${
userId || "anonymous"
} on ${resourceType}:${resourceId}`
);
} catch (error) {
console.error("Failed to log audit event:", error);
// Don't throw error to avoid breaking the main application flow
}
}
/**
* Get audit logs with filtering and pagination
* @param {Object} options - Query options
* @param {string} [options.userId] - Filter by user ID
* @param {string} [options.action] - Filter by action
* @param {string} [options.resourceType] - Filter by resource type
* @param {string} [options.resourceId] - Filter by resource ID
* @param {string} [options.startDate] - Filter from this date (ISO string)
* @param {string} [options.endDate] - Filter until this date (ISO string)
* @param {number} [options.limit] - Maximum number of records to return
* @param {number} [options.offset] - Number of records to skip
* @param {string} [options.orderBy] - Order by field (default: timestamp)
* @param {string} [options.orderDirection] - Order direction (ASC/DESC, default: DESC)
* @returns {Array} Array of audit log entries
*/
export async function getAuditLogs({
userId = null,
action = null,
resourceType = null,
resourceId = null,
startDate = null,
endDate = null,
limit = 100,
offset = 0,
orderBy = "timestamp",
orderDirection = "DESC",
} = {}) {
try {
// Check if we're in Edge Runtime - if so, return empty array
if (
typeof EdgeRuntime !== "undefined" ||
process.env.NEXT_RUNTIME === "edge"
) {
console.log(
"[Audit Log - Edge Runtime] Cannot query audit logs in Edge Runtime"
);
return [];
}
// Dynamic import to avoid Edge Runtime issues
const { default: db } = await import("./db.js");
let query = `
SELECT
al.*,
u.name as user_name,
u.email as user_email
FROM audit_logs al
LEFT JOIN users u ON al.user_id = u.id
WHERE 1=1
`;
const params = [];
if (userId) {
query += " AND al.user_id = ?";
params.push(userId);
}
if (action) {
query += " AND al.action = ?";
params.push(action);
}
if (resourceType) {
query += " AND al.resource_type = ?";
params.push(resourceType);
}
if (resourceId) {
query += " AND al.resource_id = ?";
params.push(resourceId);
}
if (startDate) {
query += " AND al.timestamp >= ?";
params.push(startDate);
}
if (endDate) {
query += " AND al.timestamp <= ?";
params.push(endDate);
}
// Validate order direction
const validOrderDirection = ["ASC", "DESC"].includes(
orderDirection.toUpperCase()
)
? orderDirection.toUpperCase()
: "DESC";
// Validate order by field
const validOrderFields = [
"timestamp",
"action",
"user_id",
"resource_type",
"resource_id",
];
const validOrderBy = validOrderFields.includes(orderBy)
? orderBy
: "timestamp";
query += ` ORDER BY al.${validOrderBy} ${validOrderDirection}`;
if (limit) {
query += " LIMIT ?";
params.push(limit);
}
if (offset) {
query += " OFFSET ?";
params.push(offset);
}
const stmt = db.prepare(query);
const results = stmt.all(...params);
// Parse details JSON for each result
return results.map((log) => ({
...log,
details: log.details ? JSON.parse(log.details) : null,
}));
} catch (error) {
console.error("Failed to get audit logs:", error);
return [];
}
}
/**
* Get audit log statistics
* @param {Object} options - Query options
* @param {string} [options.startDate] - Filter from this date (ISO string)
* @param {string} [options.endDate] - Filter until this date (ISO string)
* @returns {Object} Statistics object
*/
export async function getAuditLogStats({
startDate = null,
endDate = null,
} = {}) {
try {
// Check if we're in Edge Runtime - if so, return empty stats
if (
typeof EdgeRuntime !== "undefined" ||
process.env.NEXT_RUNTIME === "edge"
) {
console.log(
"[Audit Log - Edge Runtime] Cannot query audit log stats in Edge Runtime"
);
return {
total: 0,
actionBreakdown: [],
userBreakdown: [],
resourceBreakdown: [],
};
}
// Dynamic import to avoid Edge Runtime issues
const { default: db } = await import("./db.js");
let baseQuery = "FROM audit_logs WHERE 1=1";
const params = [];
if (startDate) {
baseQuery += " AND timestamp >= ?";
params.push(startDate);
}
if (endDate) {
baseQuery += " AND timestamp <= ?";
params.push(endDate);
}
// Total count
const totalStmt = db.prepare(`SELECT COUNT(*) as total ${baseQuery}`);
const totalResult = totalStmt.get(...params);
// Actions breakdown
const actionsStmt = db.prepare(`
SELECT action, COUNT(*) as count
${baseQuery}
GROUP BY action
ORDER BY count DESC
`);
const actionsResult = actionsStmt.all(...params);
// Users breakdown
const usersStmt = db.prepare(`
SELECT
al.user_id,
u.name as user_name,
u.email as user_email,
COUNT(*) as count
${baseQuery}
LEFT JOIN users u ON al.user_id = u.id
GROUP BY al.user_id, u.name, u.email
ORDER BY count DESC
LIMIT 10
`);
const usersResult = usersStmt.all(...params);
// Resource types breakdown
const resourcesStmt = db.prepare(`
SELECT resource_type, COUNT(*) as count
${baseQuery}
WHERE resource_type IS NOT NULL
GROUP BY resource_type
ORDER BY count DESC
`);
const resourcesResult = resourcesStmt.all(...params);
return {
total: totalResult.total,
actionBreakdown: actionsResult,
userBreakdown: usersResult,
resourceBreakdown: resourcesResult,
};
} catch (error) {
console.error("Failed to get audit log statistics:", error);
return {
total: 0,
actionBreakdown: [],
userBreakdown: [],
resourceBreakdown: [],
};
}
}
/**
* Helper function to extract client information from request
* @param {Request} req - The request object
* @returns {Object} Object containing IP address and user agent
*/
export function getClientInfo(req) {
const ipAddress =
req.headers.get("x-forwarded-for") ||
req.headers.get("x-real-ip") ||
req.headers.get("cf-connecting-ip") ||
req.ip ||
"unknown";
const userAgent = req.headers.get("user-agent") || "unknown";
return { ipAddress, userAgent };
}
/**
* Middleware helper to log API actions
* @param {Request} req - The request object
* @param {string} action - The action being performed
* @param {string} resourceType - The type of resource
* @param {string} resourceId - The ID of the resource
* @param {Object} session - The user session
* @param {Object} additionalDetails - Additional details to log
*/
export async function logApiAction(
req,
action,
resourceType,
resourceId,
session,
additionalDetails = {}
) {
const { ipAddress, userAgent } = getClientInfo(req);
await logAuditEvent({
action,
userId: session?.user?.id || null,
resourceType,
resourceId,
ipAddress,
userAgent,
details: {
method: req.method,
url: req.url,
...additionalDetails,
},
});
}
const auditLog = {
logAuditEvent,
getAuditLogs,
getAuditLogStats,
getClientInfo,
logApiAction,
AUDIT_ACTIONS,
RESOURCE_TYPES,
};
export default auditLog;

129
src/lib/auditLogEdge.js Normal file
View File

@@ -0,0 +1,129 @@
/**
* Edge-compatible audit logging utility
* This version avoids direct database imports and can be used in Edge Runtime
*/
import { AUDIT_ACTIONS, RESOURCE_TYPES } from "./auditLog.js";
/**
* Log an audit event in Edge Runtime compatible way
* @param {Object} params - Audit log parameters
*/
export async function logAuditEventAsync({
action,
userId = null,
resourceType = null,
resourceId = null,
ipAddress = null,
userAgent = null,
details = null,
timestamp = null,
}) {
try {
// In Edge Runtime or when database is not available, log to console
if (
typeof EdgeRuntime !== "undefined" ||
process.env.NEXT_RUNTIME === "edge"
) {
console.log(
`[Audit Log - Edge] ${action} by user ${
userId || "anonymous"
} on ${resourceType}:${resourceId}`,
{
details,
ipAddress,
userAgent,
timestamp: timestamp || new Date().toISOString(),
}
);
return;
}
// Try to make an API call to log the event
try {
const response = await fetch("/api/audit-logs/log", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
action,
userId,
resourceType,
resourceId,
ipAddress,
userAgent,
details,
timestamp,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
} catch (fetchError) {
// Fallback to console logging if API call fails
console.log(
`[Audit Log - Fallback] ${action} by user ${
userId || "anonymous"
} on ${resourceType}:${resourceId}`,
{
details,
ipAddress,
userAgent,
timestamp: timestamp || new Date().toISOString(),
error: fetchError.message,
}
);
}
} catch (error) {
console.error("Failed to log audit event:", error);
}
}
/**
* Helper function to extract client information from request (Edge compatible)
* @param {Request} req - The request object
* @returns {Object} Object containing IP address and user agent
*/
export function getClientInfoEdgeCompatible(req) {
const ipAddress =
req.headers.get("x-forwarded-for") ||
req.headers.get("x-real-ip") ||
req.headers.get("cf-connecting-ip") ||
"unknown";
const userAgent = req.headers.get("user-agent") || "unknown";
return { ipAddress, userAgent };
}
/**
* Middleware helper to log API actions (Edge compatible)
*/
export async function logApiActionAsync(
req,
action,
resourceType,
resourceId,
session,
additionalDetails = {}
) {
const { ipAddress, userAgent } = getClientInfoEdgeCompatible(req);
await logAuditEventAsync({
action,
userId: session?.user?.id || null,
resourceType,
resourceId,
ipAddress,
userAgent,
details: {
method: req.method,
url: req.url,
...additionalDetails,
},
});
}
export { AUDIT_ACTIONS, RESOURCE_TYPES };

159
src/lib/auditLogSafe.js Normal file
View File

@@ -0,0 +1,159 @@
/**
* Safe audit logging that doesn't cause Edge Runtime issues
* This module can be safely imported anywhere without causing database issues
*/
// Constants that can be safely exported
export const AUDIT_ACTIONS = {
// Authentication
LOGIN: "login",
LOGOUT: "logout",
LOGIN_FAILED: "login_failed",
// Projects
PROJECT_CREATE: "project_create",
PROJECT_UPDATE: "project_update",
PROJECT_DELETE: "project_delete",
PROJECT_VIEW: "project_view",
// Tasks
TASK_CREATE: "task_create",
TASK_UPDATE: "task_update",
TASK_DELETE: "task_delete",
TASK_STATUS_CHANGE: "task_status_change",
// Project Tasks
PROJECT_TASK_CREATE: "project_task_create",
PROJECT_TASK_UPDATE: "project_task_update",
PROJECT_TASK_DELETE: "project_task_delete",
PROJECT_TASK_STATUS_CHANGE: "project_task_status_change",
// Contracts
CONTRACT_CREATE: "contract_create",
CONTRACT_UPDATE: "contract_update",
CONTRACT_DELETE: "contract_delete",
// Notes
NOTE_CREATE: "note_create",
NOTE_UPDATE: "note_update",
NOTE_DELETE: "note_delete",
// Admin actions
USER_CREATE: "user_create",
USER_UPDATE: "user_update",
USER_DELETE: "user_delete",
USER_ROLE_CHANGE: "user_role_change",
// System actions
DATA_EXPORT: "data_export",
BULK_OPERATION: "bulk_operation",
};
export const RESOURCE_TYPES = {
PROJECT: "project",
TASK: "task",
PROJECT_TASK: "project_task",
CONTRACT: "contract",
NOTE: "note",
USER: "user",
SESSION: "session",
SYSTEM: "system",
};
/**
* Safe audit logging function that works in any runtime
*/
export async function logAuditEventSafe({
action,
userId = null,
resourceType = null,
resourceId = null,
ipAddress = null,
userAgent = null,
details = null,
timestamp = null,
}) {
try {
// Always log to console first
console.log(
`[Audit] ${action} by user ${
userId || "anonymous"
} on ${resourceType}:${resourceId}`
);
// Check if we're in Edge Runtime
if (
typeof EdgeRuntime !== "undefined" ||
process.env.NEXT_RUNTIME === "edge"
) {
console.log("[Audit] Edge Runtime detected - console logging only");
return;
}
// Try to get the database-enabled audit function
try {
const auditModule = await import("./auditLog.js");
await auditModule.logAuditEvent({
action,
userId,
resourceType,
resourceId,
ipAddress,
userAgent,
details,
timestamp,
});
} catch (dbError) {
console.log(
"[Audit] Database logging failed, using console fallback:",
dbError.message
);
}
} catch (error) {
console.error("[Audit] Failed to log audit event:", error);
}
}
/**
* Helper function to extract client information from request
*/
export function getClientInfo(req) {
const ipAddress =
req.headers?.get?.("x-forwarded-for") ||
req.headers?.get?.("x-real-ip") ||
req.headers?.get?.("cf-connecting-ip") ||
req.ip ||
"unknown";
const userAgent = req.headers?.get?.("user-agent") || "unknown";
return { ipAddress, userAgent };
}
/**
* Safe API action logging
*/
export async function logApiActionSafe(
req,
action,
resourceType,
resourceId,
session,
additionalDetails = {}
) {
const { ipAddress, userAgent } = getClientInfo(req);
await logAuditEventSafe({
action,
userId: session?.user?.id || null,
resourceType,
resourceId,
ipAddress,
userAgent,
details: {
method: req.method,
url: req.url,
...additionalDetails,
},
});
}

View File

@@ -1,52 +1,60 @@
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import bcrypt from "bcryptjs"
import { z } from "zod"
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email("Invalid email format"),
password: z.string().min(6, "Password must be at least 6 characters")
})
email: z.string().email("Invalid email format"),
password: z.string().min(6, "Password must be at least 6 characters"),
});
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
try {
// Import database here to avoid edge runtime issues
const { default: db } = await import("./db.js")
// Validate input
const validatedFields = loginSchema.parse(credentials)
// Check if user exists and is active
const user = db.prepare(`
providers: [
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
try {
// Import database here to avoid edge runtime issues
const { default: db } = await import("./db.js");
// Validate input
const validatedFields = loginSchema.parse(credentials);
// Check if user exists and is active
const user = db
.prepare(
`
SELECT id, email, name, password_hash, role, is_active,
failed_login_attempts, locked_until
FROM users
WHERE email = ? AND is_active = 1
`).get(validatedFields.email)
if (!user) {
throw new Error("Invalid credentials")
}
// Check if account is locked
if (user.locked_until && new Date(user.locked_until) > new Date()) {
throw new Error("Account temporarily locked")
}
// Verify password
const isValidPassword = await bcrypt.compare(validatedFields.password, user.password_hash)
if (!isValidPassword) {
// Increment failed attempts
db.prepare(`
`
)
.get(validatedFields.email);
if (!user) {
throw new Error("Invalid credentials");
}
// Check if account is locked
if (user.locked_until && new Date(user.locked_until) > new Date()) {
throw new Error("Account temporarily locked");
}
// Verify password
const isValidPassword = await bcrypt.compare(
validatedFields.password,
user.password_hash
);
if (!isValidPassword) {
// Increment failed attempts
db.prepare(
`
UPDATE users
SET failed_login_attempts = failed_login_attempts + 1,
locked_until = CASE
@@ -55,57 +63,95 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
ELSE locked_until
END
WHERE id = ?
`).run(user.id)
throw new Error("Invalid credentials")
}
// Reset failed attempts and update last login
db.prepare(`
`
).run(user.id);
// Log failed login attempt (only in Node.js runtime)
try {
const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } =
await import("./auditLogSafe.js");
await logAuditEventSafe({
action: AUDIT_ACTIONS.LOGIN_FAILED,
userId: user.id,
resourceType: RESOURCE_TYPES.SESSION,
details: {
email: validatedFields.email,
reason: "invalid_password",
failed_attempts: user.failed_login_attempts + 1,
},
});
} catch (auditError) {
console.error("Failed to log audit event:", auditError);
}
throw new Error("Invalid credentials");
}
// Reset failed attempts and update last login
db.prepare(
`
UPDATE users
SET failed_login_attempts = 0,
locked_until = NULL,
last_login = CURRENT_TIMESTAMP
WHERE id = ?
`).run(user.id)
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role
}
} catch (error) {
console.error("Login error:", error)
return null
}
}
})
],
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
token.userId = user.id
}
return token
},
async session({ session, token }) {
if (token) {
session.user.id = token.userId
session.user.role = token.role
}
return session
}
},
pages: {
signIn: '/auth/signin',
signOut: '/auth/signout',
error: '/auth/error'
},
debug: process.env.NODE_ENV === 'development'
})
`
).run(user.id);
// Log successful login (only in Node.js runtime)
try {
const { logAuditEventSafe, AUDIT_ACTIONS, RESOURCE_TYPES } =
await import("./auditLogSafe.js");
await logAuditEventSafe({
action: AUDIT_ACTIONS.LOGIN,
userId: user.id,
resourceType: RESOURCE_TYPES.SESSION,
details: {
email: user.email,
role: user.role,
},
});
} catch (auditError) {
console.error("Failed to log audit event:", auditError);
}
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
};
} catch (error) {
console.error("Login error:", error);
return null;
}
},
}),
],
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role;
token.userId = user.id;
}
return token;
},
async session({ session, token }) {
if (token) {
session.user.id = token.userId;
session.user.role = token.role;
}
return session;
},
},
pages: {
signIn: "/auth/signin",
signOut: "/auth/signout",
error: "/auth/error",
},
debug: process.env.NODE_ENV === "development",
});

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;