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,49 @@
// Force this API route to use Node.js runtime for database access
export const runtime = "nodejs";
import { NextResponse } from "next/server";
import { logAuditEvent } from "@/lib/auditLog";
export async function POST(request) {
try {
const data = await request.json();
const {
action,
userId,
resourceType,
resourceId,
ipAddress,
userAgent,
details,
timestamp,
} = data;
if (!action) {
return NextResponse.json(
{ error: "Action is required" },
{ status: 400 }
);
}
// Log the audit event
await logAuditEvent({
action,
userId,
resourceType,
resourceId,
ipAddress,
userAgent,
details,
timestamp,
});
return NextResponse.json({ success: true });
} catch (error) {
console.error("Audit log API error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,67 @@
// Force this API route to use Node.js runtime
export const runtime = "nodejs";
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { getAuditLogs, getAuditLogStats } from "@/lib/auditLog";
export async function GET(request) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Only admins and project managers can view audit logs
if (!["admin", "project_manager"].includes(session.user.role)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { searchParams } = new URL(request.url);
// Parse query parameters
const filters = {
userId: searchParams.get("userId") || null,
action: searchParams.get("action") || null,
resourceType: searchParams.get("resourceType") || null,
resourceId: searchParams.get("resourceId") || null,
startDate: searchParams.get("startDate") || null,
endDate: searchParams.get("endDate") || null,
limit: parseInt(searchParams.get("limit")) || 100,
offset: parseInt(searchParams.get("offset")) || 0,
orderBy: searchParams.get("orderBy") || "timestamp",
orderDirection: searchParams.get("orderDirection") || "DESC",
};
// Get audit logs
const logs = await getAuditLogs(filters);
// Get statistics if requested
const includeStats = searchParams.get("includeStats") === "true";
let stats = null;
if (includeStats) {
stats = await getAuditLogStats({
startDate: filters.startDate,
endDate: filters.endDate,
});
}
return NextResponse.json({
success: true,
data: logs,
stats,
filters: {
...filters,
total: logs.length,
},
});
} catch (error) {
console.error("Audit logs API error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,41 @@
// Force this API route to use Node.js runtime
export const runtime = "nodejs";
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { getAuditLogStats } from "@/lib/auditLog";
export async function GET(request) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Only admins and project managers can view audit log statistics
if (!["admin", "project_manager"].includes(session.user.role)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { searchParams } = new URL(request.url);
const filters = {
startDate: searchParams.get("startDate") || null,
endDate: searchParams.get("endDate") || null,
};
const stats = await getAuditLogStats(filters);
return NextResponse.json({
success: true,
data: stats,
});
} catch (error) {
console.error("Audit log stats API error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -1,6 +1,14 @@
// Force this API route to use Node.js runtime for database access
export const runtime = "nodejs";
import db from "@/lib/db";
import { NextResponse } from "next/server";
import { withUserAuth } from "@/lib/middleware/auth";
import {
logApiActionSafe,
AUDIT_ACTIONS,
RESOURCE_TYPES,
} from "@/lib/auditLogSafe.js";
async function createNoteHandler(req) {
const { project_id, task_id, note } = await req.json();
@@ -10,12 +18,26 @@ async function createNoteHandler(req) {
}
try {
db.prepare(
`
const result = db
.prepare(
`
INSERT INTO notes (project_id, task_id, note, created_by, note_date)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
`
).run(project_id || null, task_id || null, note, req.user?.id || null);
)
.run(project_id || null, task_id || null, note, req.user?.id || null);
// Log note creation
await logApiActionSafe(
req,
AUDIT_ACTIONS.NOTE_CREATE,
RESOURCE_TYPES.NOTE,
result.lastInsertRowid.toString(),
req.session,
{
noteData: { project_id, task_id, note_length: note.length },
}
);
return NextResponse.json({ success: true });
} catch (error) {
@@ -27,11 +49,30 @@ async function createNoteHandler(req) {
}
}
async function deleteNoteHandler(_, { params }) {
async function deleteNoteHandler(req, { params }) {
const { id } = params;
// Get note data before deletion for audit log
const note = db.prepare("SELECT * FROM notes WHERE note_id = ?").get(id);
db.prepare("DELETE FROM notes WHERE note_id = ?").run(id);
// Log note deletion
await logApiActionSafe(
req,
AUDIT_ACTIONS.NOTE_DELETE,
RESOURCE_TYPES.NOTE,
id,
req.session,
{
deletedNote: {
project_id: note?.project_id,
task_id: note?.task_id,
note_length: note?.note?.length || 0,
},
}
);
return NextResponse.json({ success: true });
}
@@ -43,12 +84,36 @@ async function updateNoteHandler(req, { params }) {
return NextResponse.json({ error: "Missing note or ID" }, { status: 400 });
}
// Get original note for audit log
const originalNote = db
.prepare("SELECT * FROM notes WHERE note_id = ?")
.get(noteId);
db.prepare(
`
UPDATE notes SET note = ? WHERE note_id = ?
`
).run(note, noteId);
// Log note update
await logApiActionSafe(
req,
AUDIT_ACTIONS.NOTE_UPDATE,
RESOURCE_TYPES.NOTE,
noteId,
req.session,
{
originalNote: {
note_length: originalNote?.note?.length || 0,
project_id: originalNote?.project_id,
task_id: originalNote?.task_id,
},
updatedNote: {
note_length: note.length,
},
}
);
return NextResponse.json({ success: true });
}

View File

@@ -1,3 +1,6 @@
// Force this API route to use Node.js runtime for database access
export const runtime = "nodejs";
import {
getProjectById,
updateProject,
@@ -6,6 +9,11 @@ import {
import initializeDatabase from "@/lib/init-db";
import { NextResponse } from "next/server";
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
import {
logApiActionSafe,
AUDIT_ACTIONS,
RESOURCE_TYPES,
} from "@/lib/auditLogSafe.js";
// Make sure the DB is initialized before queries run
initializeDatabase();
@@ -18,6 +26,16 @@ async function getProjectHandler(req, { params }) {
return NextResponse.json({ error: "Project not found" }, { status: 404 });
}
// Log project view
await logApiActionSafe(
req,
AUDIT_ACTIONS.PROJECT_VIEW,
RESOURCE_TYPES.PROJECT,
id,
req.session,
{ project_name: project.project_name }
);
return NextResponse.json(project);
}
@@ -28,16 +46,54 @@ async function updateProjectHandler(req, { params }) {
// Get user ID from authenticated request
const userId = req.user?.id;
// Get original project data for audit log
const originalProject = getProjectById(parseInt(id));
updateProject(parseInt(id), data, userId);
// Return the updated project
// Get updated project
const updatedProject = getProjectById(parseInt(id));
// Log project update
await logApiActionSafe(
req,
AUDIT_ACTIONS.PROJECT_UPDATE,
RESOURCE_TYPES.PROJECT,
id,
req.session,
{
originalData: originalProject,
updatedData: data,
changedFields: Object.keys(data),
}
);
return NextResponse.json(updatedProject);
}
async function deleteProjectHandler(req, { params }) {
const { id } = await params;
// Get project data before deletion for audit log
const project = getProjectById(parseInt(id));
deleteProject(parseInt(id));
// Log project deletion
await logApiActionSafe(
req,
AUDIT_ACTIONS.PROJECT_DELETE,
RESOURCE_TYPES.PROJECT,
id,
req.session,
{
deletedProject: {
project_name: project?.project_name,
project_number: project?.project_number,
},
}
);
return NextResponse.json({ success: true });
}

View File

@@ -1,3 +1,6 @@
// Force this API route to use Node.js runtime for database access
export const runtime = "nodejs";
import {
getAllProjects,
createProject,
@@ -6,6 +9,11 @@ import {
import initializeDatabase from "@/lib/init-db";
import { NextResponse } from "next/server";
import { withReadAuth, withUserAuth } from "@/lib/middleware/auth";
import {
logApiActionSafe,
AUDIT_ACTIONS,
RESOURCE_TYPES,
} from "@/lib/auditLogSafe.js";
// Make sure the DB is initialized before queries run
initializeDatabase();
@@ -30,6 +38,19 @@ async function getProjectsHandler(req) {
projects = getAllProjects(contractId);
}
// Log project list access
await logApiActionSafe(
req,
AUDIT_ACTIONS.PROJECT_VIEW,
RESOURCE_TYPES.PROJECT,
null, // No specific project ID for list view
req.session,
{
filters: { contractId, assignedTo, createdBy },
resultCount: projects.length,
}
);
return NextResponse.json(projects);
}
@@ -40,9 +61,27 @@ async function createProjectHandler(req) {
const userId = req.user?.id;
const result = createProject(data, userId);
const projectId = result.lastInsertRowid;
// Log project creation
await logApiActionSafe(
req,
AUDIT_ACTIONS.PROJECT_CREATE,
RESOURCE_TYPES.PROJECT,
projectId.toString(),
req.session,
{
projectData: {
project_name: data.project_name,
project_number: data.project_number,
contract_id: data.contract_id,
},
}
);
return NextResponse.json({
success: true,
projectId: result.lastInsertRowid,
projectId: projectId,
});
}