From 44dc89083cd852204d5ee7300f276ec8a6f985b3 Mon Sep 17 00:00:00 2001 From: Ewan-Dkhar Date: Sun, 12 Apr 2026 22:00:39 +0530 Subject: [PATCH 1/3] Add audit log model and logAudit helper function --- app/api/admin/example/route.ts | 24 +++++++++++++++ models/AuditLog.ts | 55 ++++++++++++++++++++++++++++++++++ utils/auditLogger.ts | 50 +++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 app/api/admin/example/route.ts create mode 100644 models/AuditLog.ts create mode 100644 utils/auditLogger.ts diff --git a/app/api/admin/example/route.ts b/app/api/admin/example/route.ts new file mode 100644 index 0000000..81c827b --- /dev/null +++ b/app/api/admin/example/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from "next/server"; +import { logAudit } from "@/utils/auditLogger"; +import { connectDB } from "@/lib/db"; + +export async function POST(req: NextRequest) { + await connectDB(); + + // updating a user role + const targetUserId = "60c72b2f9b1d8e1a4c8e4d3a"; // Mock target ID + + // Call the audit logger at the end + await logAudit({ + req, + action: "UPDATE_USER_ROLE", + targetType: "User", + targetId: targetUserId, + diff: { + oldRole: "member", + newRole: "moderator" + }, + }); + + return NextResponse.json({ success: true }); +} diff --git a/models/AuditLog.ts b/models/AuditLog.ts new file mode 100644 index 0000000..23e0cba --- /dev/null +++ b/models/AuditLog.ts @@ -0,0 +1,55 @@ +import mongoose, { Schema, Document, Model } from 'mongoose'; + + +export interface IAuditLog extends Document { + actorId: mongoose.Types.ObjectId; + action: string; + targetType: string; + targetId: mongoose.Types.ObjectId; + diff?: Record; + ip?: string; + userAgent?: string; + createdAt: Date; +} + +const AuditLogSchema: Schema = new Schema( + { + actorId: { + type: Schema.Types.ObjectId, + ref: 'User', // Basically tells the ID belongs to a document from the User collection(for future implemetation) + required: true, + }, + action: { + type: String, + required: true, + }, + targetType: { + type: String, + required: true, + }, + targetId: { + type: Schema.Types.ObjectId, + required: true, + }, + diff: { + type: Schema.Types.Mixed, + }, + ip: { + type: String, + }, + userAgent: { + type: String, + }, + createdAt: { + type: Date, + default: Date.now, + index: true, + expires: '2y', + }, + } +); + +const AuditLog: Model = + mongoose.models.AuditLog || mongoose.model('AuditLog', AuditLogSchema); + +export default AuditLog; diff --git a/utils/auditLogger.ts b/utils/auditLogger.ts new file mode 100644 index 0000000..2e02082 --- /dev/null +++ b/utils/auditLogger.ts @@ -0,0 +1,50 @@ +import { NextRequest } from 'next/server'; +import { Types } from 'mongoose'; +import AuditLog from '@/models/AuditLog'; + +interface LogAuditParams { + req: NextRequest | Request; + action: string; + targetType: string; + targetId: Types.ObjectId | string; + diff?: Record; +} + +/** + * Helper function to create an Audit Log entry. + */ +export const logAudit = async ({ + req, + action, + targetType, + targetId, + diff, +}: LogAuditParams): Promise => { + try { + const rawIp = req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip') || ''; + const ip = rawIp.split(',')[0].trim(); + + const userAgent = req.headers.get('user-agent') || 'Unknown Device'; + + const rawActorId = req.headers.get('x-user-id'); + const actorId = rawActorId ? new Types.ObjectId(rawActorId) : new Types.ObjectId(); + + if (!rawActorId) { + console.warn(`[AuditLog] Missing actorId for action: ${action}. Is the user authenticated?`); + } + + // Save to database + await AuditLog.create({ + actorId, + action, + targetType, + targetId: new Types.ObjectId(targetId), + diff, + ip, + userAgent, + }); + + } catch (error) { + console.error('[AuditLog] Failed to create audit log:', error); + } +}; \ No newline at end of file From c1e1b4c1beed5f3a1468237f53ac9ab1baf76c5c Mon Sep 17 00:00:00 2001 From: Ewan-Dkhar Date: Sun, 12 Apr 2026 22:16:02 +0530 Subject: [PATCH 2/3] Add admin/audit read-only table and fix linting errors --- app/admin/audit/page.tsx | 115 +++++++++++++++++++++++++++++++++++++++ models/AuditLog.ts | 3 +- utils/auditLogger.ts | 2 +- 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 app/admin/audit/page.tsx diff --git a/app/admin/audit/page.tsx b/app/admin/audit/page.tsx new file mode 100644 index 0000000..c46bee4 --- /dev/null +++ b/app/admin/audit/page.tsx @@ -0,0 +1,115 @@ +import { connectDB } from "@/lib/db"; +import AuditLog from "@/models/AuditLog"; +import { headers } from "next/headers"; + +// Force Next.js to dynamically render this page on every request +export const dynamic = "force-dynamic"; + +export default async function AuditLogPage() { + // 1. Superadmin Authorization Check + // In a real application, you would use next-auth's `getServerSession` or similar here. + // For now, we mock the auth check by looking for a specific request header. + const headersList = await headers(); + const userRole = headersList.get("x-user-role"); + + // If the user isn't a superadmin, deny access + if (userRole !== "superadmin") { + return ( +
+
+

403 - Forbidden

+

+ You must be a superadmin to view the audit logs. +

+
+
+ ); + } + + // 2. Fetch Data + await connectDB(); + + // Fetch the latest 100 audit logs + const logs = await AuditLog.find({}) + .sort({ createdAt: -1 }) + .limit(100) + .lean(); + + // 3. Render Table UI + return ( +
+
+

System Audit Logs

+ + Superadmin Only + +
+ +
+ + + + + + + + + + + + + {logs.map((log: any) => ( + + + + + + + + + ))} + + {logs.length === 0 && ( + + + + )} + +
TimestampActionActor IDTargetDiff / ChangesIP & Device
+ {new Date(log.createdAt).toLocaleString()} + + {log.action} + + {log.actorId?.toString() || 'System'} + +
+ + {log.targetType} + + + {log.targetId?.toString()} + +
+
+ {log.diff && Object.keys(log.diff).length > 0 ? ( +
+                      {JSON.stringify(log.diff, null, 2)}
+                    
+ ) : ( + No changes recorded + )} +
+
{log.ip}
+
+ {log.userAgent} +
+
+ No audit logs found in the database. +
+
+
+ ); +} diff --git a/models/AuditLog.ts b/models/AuditLog.ts index 23e0cba..03e45fd 100644 --- a/models/AuditLog.ts +++ b/models/AuditLog.ts @@ -1,12 +1,11 @@ import mongoose, { Schema, Document, Model } from 'mongoose'; - export interface IAuditLog extends Document { actorId: mongoose.Types.ObjectId; action: string; targetType: string; targetId: mongoose.Types.ObjectId; - diff?: Record; + diff?: Record; ip?: string; userAgent?: string; createdAt: Date; diff --git a/utils/auditLogger.ts b/utils/auditLogger.ts index 2e02082..4df46c1 100644 --- a/utils/auditLogger.ts +++ b/utils/auditLogger.ts @@ -7,7 +7,7 @@ interface LogAuditParams { action: string; targetType: string; targetId: Types.ObjectId | string; - diff?: Record; + diff?: Record; } /** From ff5871da3a18ae42f09c23bf2011861b73ad16cc Mon Sep 17 00:00:00 2001 From: Ewan-Dkhar Date: Sun, 12 Apr 2026 22:18:43 +0530 Subject: [PATCH 3/3] Fix liniting errors in /admin/audit/page.tsx --- app/admin/audit/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/admin/audit/page.tsx b/app/admin/audit/page.tsx index c46bee4..0e8885b 100644 --- a/app/admin/audit/page.tsx +++ b/app/admin/audit/page.tsx @@ -58,7 +58,7 @@ export default async function AuditLogPage() { - {logs.map((log: any) => ( + {logs.map((log) => (