Loading...
Loading...
> Track security-relevant events and user actions for compliance and debugging.
Audit logging creates an immutable record of security-relevant events. Track logins, payments, admin actions, and more for compliance requirements and security investigations.
The AuditLog model in Prisma
1// prisma/schema.prisma23model AuditLog {4 id String @id @default(cuid())5 createdAt DateTime @default(now())67 // Event details8 action String // e.g., "user.login", "payment.created"9 category String // e.g., "auth", "billing", "admin"10 severity String @default("info") // info, warning, error, critical1112 // Actor information13 userId String?14 user User? @relation(fields: [userId], references: [id])15 actorType String @default("user") // user, system, api1617 // Context18 ipAddress String?19 userAgent String?2021 // Event data22 targetType String? // e.g., "user", "organization", "payment"23 targetId String?24 metadata Json? // Additional event-specific data2526 // Result27 status String @default("success") // success, failure28 errorMessage String?2930 @@index([userId])31 @@index([action])32 @@index([category])33 @@index([createdAt])34}
Create a service to log events
1// src/lib/audit.ts23import { prisma } from "@/lib/db";4import { NextRequest } from "next/server";56export type AuditAction =7 | "user.login"8 | "user.logout"9 | "user.register"10 | "user.password_reset"11 | "user.email verified"12 | "user.updated"13 | "user.deleted"14 | "payment.created"15 | "payment.failed"16 | "subscription.created"17 | "subscription.cancelled"18 | "api key.created"19 | "api key.revoked"20 | "organization.created"21 | "organization.member_added"22 | "organization.member_removed"23 | "admin.user_impersonated"24 | "admin.settings_changed";2526export type AuditCategory = "auth" | "billing" | "admin" | "organization" | "api";27export type AuditSeverity = "info" | "warning" | "error" | "critical";2829interface AuditLogInput {30 action: AuditAction;31 category: AuditCategory;32 severity?: AuditSeverity;33 userId?: string;34 actorType?: "user" | "system" | "api";35 targetType?: string;36 targetId?: string;37 metadata?: Record<string, unknown>;38 status?: "success" | "failure";39 errorMessage?: string;40 request?: NextRequest;41}4243export async function createAuditLog(input: AuditLogInput) {44 const {45 action,46 category,47 severity = "info",48 userId,49 actorType = "user",50 targetType,51 targetId,52 metadata,53 status = "success",54 errorMessage,55 request,56 } = input;5758 // Extract request context59 let ipAddress: string | undefined;60 let userAgent: string | undefined;6162 if (request) {63 ipAddress = request.ip ??64 request.headers.get("x-forwarded-for")?.split(",")[0] ??65 undefined;66 userAgent = request.headers.get("user-agent") ?? undefined;67 }6869 return prisma.auditLog.create({70 data: {71 action,72 category,73 severity,74 userId,75 actorType,76 ipAddress,77 userAgent,78 targetType,79 targetId,80 metadata,81 status,82 errorMessage,83 },84 });85}
Log events throughout your application
1// Login event2// src/app/api/auth/login/route.ts34import { createAuditLog } from "@/lib/audit";56export async function POST(request: NextRequest) {7 const body = await request.json();89 try {10 const user = await authenticateUser(body.email, body.password);1112 await createAuditLog({13 action: "user.login",14 category: "auth",15 userId: user.id,16 metadata: { email: body.email },17 request,18 });1920 return NextResponse.json({ success: true });21 } catch (_) {22 await createAuditLog({23 action: "user.login",24 category: "auth",25 severity: "warning",26 status: "failure",27 metadata: { email: body.email },28 errorMessage: error instanceof Error ? error.message : "Unknown error",29 request,30 });3132 return NextResponse.json(33 { error: "Invalid credentials" },34 { status: 401 }35 );36 }37}3839// Payment event40// src/app/api/stripe/webhook/route.ts4142await createAuditLog({43 action: "payment.created",44 category: "billing",45 userId: payment.userId,46 targetType: "payment",47 targetId: payment.id,48 metadata: {49 amount: payment.amount,50 currency: payment.currency,51 stripePaymentId: payment.stripeId,52 },53});5455// Admin action56await createAuditLog({57 action: "admin.user_impersonated",58 category: "admin",59 severity: "warning",60 userId: adminUser.id,61 targetType: "user",62 targetId: impersonatedUser.id,63 metadata: {64 reason: "Customer support request",65 },66 request,67});
API endpoint to search and filter audit logs
1// src/app/api/admin/audit-logs/route.ts23import { NextRequest, NextResponse } from "next/server";4import { auth } from "@/lib/auth";5import { prisma } from "@/lib/db";67export async function GET(request: NextRequest) {8 const session = await auth();910 // Admin only11 if (session?.user?.role !== "admin") {12 return NextResponse.json({ error: "Forbidden" }, { status: 403 });13 }1415 const searchParams = request.nextUrl.searchParams;1617 const page = parseInt(searchParams.get("page") || "1");18 const limit = parseInt(searchParams.get("limit") || "50");19 const userId = searchParams.get("userId");20 const action = searchParams.get("action");21 const category = searchParams.get("category");22 const severity = searchParams.get("severity");23 const from = searchParams.get("from");24 const to = searchParams.get("to");2526 const where: any = {};2728 if (userId) where.userId = userId;29 if (action) where.action = action;30 if (category) where.category = category;31 if (severity) where.severity = severity;3233 if (from || to) {34 where.createdAt = {};35 if (from) where.createdAt.gte = new Date(from);36 if (to) where.createdAt.lte = new Date(to);37 }3839 const [logs, total] = await Promise.all([40 prisma.auditLog.findMany({41 where,42 orderBy: { createdAt: "desc" },43 skip: (page - 1) * limit,44 take: limit,45 include: {46 user: {47 select: { id: true, name: true, email: true },48 },49 },50 }),51 prisma.auditLog.count({ where }),52 ]);5354 return NextResponse.json({55 logs,56 pagination: {57 page,58 limit,59 total,60 pages: Math.ceil(total / limit),61 },62 });63}
Set up automatic cleanup of old logs
1// scripts/cleanup-audit-logs.ts23import { prisma } from "@/lib/db";45const RETENTION_DAYS = {6 info: 30, // Keep info logs for 30 days7 warning: 90, // Keep warning logs for 90 days8 error: 180, // Keep error logs for 180 days9 critical: 365, // Keep critical logs for 1 year10};1112async function cleanupAuditLogs() {13 const now = new Date();1415 for (const [severity, days] of Object.entries(RETENTION_DAYS)) {16 const cutoffDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);1718 const result = await prisma.auditLog.deleteMany({19 where: {20 severity,21 createdAt: { lt: cutoffDate },22 },23 });2425 console.log(`Deleted ${result.count} ${severity} logs older than ${days} days`);26 }27}2829cleanupAuditLogs()30 .catch(console.error)31 .finally(() => prisma.$disconnect());3233// Run as cron job:34// 0 0 * * * npx ts-node scripts/cleanup-audit-logs.ts
Send alerts for critical events
1// src/lib/audit.ts23import { sendEmail } from "@/lib/email";45const ALERT_ACTIONS = [6 "user.password_reset",7 "admin.user_impersonated",8 "api key.created",9];1011export async function createAuditLog(input: AuditLogInput) {12 const log = await prisma.auditLog.create({ /* ... */ });1314 // Send alerts for critical actions15 if (16 input.severity === "critical" ||17 ALERT_ACTIONS.includes(input.action)18 ) {19 await sendSecurityAlert(log);20 }2122 return log;23}2425async function sendSecurityAlert(log: AuditLog) {26 const adminEmails = process.env.SECURITY_ALERT_EMAILS?.split(",") || [];2728 for (const email of adminEmails) {29 await sendEmail({30 to: email,31 subject: `Security Alert: ${log.action}`,32 text: `33 Action: ${log.action}34 Category: ${log.category}35 Severity: ${log.severity}36 User ID: ${log.userId || "N/A"}37 IP: ${log.ipAddress || "N/A"}38 Time: ${log.createdAt.toISOString()}3940 Metadata: ${JSON.stringify(log.metadata, null, 2)}41 `,42 });43 }44}