Files
library/api/src/app/services/audit.service.ts
T

143 lines
3.7 KiB
TypeScript

import type { FastifyRequest } from "fastify";
import { prisma } from "../lib/prisma";
import type { AuditAction, AuditCategory, AuditLogFilters } from "@library/shared-types";
interface AuditLogData {
action: AuditAction;
category: AuditCategory;
userId?: string;
targetUserId?: string;
resourceType?: string;
resourceId?: string;
details?: string;
success?: boolean;
}
export const AuditService = {
async log(data: AuditLogData, request?: FastifyRequest) {
const userAgent = request?.headers["user-agent"] ?? undefined;
return prisma.auditLog.create({
data: {
action: data.action,
category: data.category,
userId: data.userId,
targetUserId: data.targetUserId,
resourceType: data.resourceType,
resourceId: data.resourceId,
details: data.details,
userAgent,
success: data.success ?? true,
},
});
},
async logFromRequest(
request: FastifyRequest,
data: Omit<AuditLogData, "userId">
) {
const userId = (request.user as { id?: string } | undefined)?.id;
return this.log(
{
...data,
userId,
},
request
);
},
async getLogs(filters: AuditLogFilters = {}) {
const { action, category, userId, success, startDate, endDate, page = 1, limit = 50 } = filters;
const where: Record<string, unknown> = {};
if (action) {
where.action = action;
}
if (category) {
where.category = category;
}
if (userId) {
where.userId = userId;
}
if (success !== undefined) {
where.success = success;
}
if (startDate || endDate) {
where.createdAt = {};
if (startDate) {
(where.createdAt as Record<string, Date>).gte = startDate;
}
if (endDate) {
(where.createdAt as Record<string, Date>).lte = endDate;
}
}
const [rawLogs, total] = await Promise.all([
prisma.auditLog.findMany({
where,
orderBy: { createdAt: "desc" },
skip: (page - 1) * limit,
take: limit,
}),
prisma.auditLog.count({ where }),
]);
// Collect all unique user IDs to fetch
const userIds = new Set<string>();
for (const log of rawLogs) {
if (log.userId) {
userIds.add(log.userId);
}
if (log.targetUserId) {
userIds.add(log.targetUserId);
}
}
// Fetch all users in one query
const users = userIds.size > 0
? await prisma.user.findMany({
where: { id: { in: Array.from(userIds) } },
select: { id: true, username: true, avatar: true },
})
: [];
// Create a lookup map
const userMap = new Map(users.map(u => [u.id, { id: u.id, username: u.username, avatar: u.avatar ?? undefined }]));
// Map logs with user info
const logs = rawLogs.map(log => ({
id: log.id,
action: log.action,
category: log.category,
userId: log.userId ?? undefined,
user: log.userId ? userMap.get(log.userId) : undefined,
targetUserId: log.targetUserId ?? undefined,
targetUser: log.targetUserId ? userMap.get(log.targetUserId) : undefined,
resourceType: log.resourceType ?? undefined,
resourceId: log.resourceId ?? undefined,
details: log.details ?? undefined,
userAgent: log.userAgent ?? undefined,
success: log.success,
createdAt: log.createdAt,
}));
return {
logs,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
},
async getLogsByUser(userId: string, page = 1, limit = 50) {
return this.getLogs({ userId, page, limit });
},
async getSecurityLogs(page = 1, limit = 50) {
return this.getLogs({ category: "SECURITY" as AuditCategory, page, limit });
},
};