MOPC-App/src/server/routers/audit.ts

185 lines
4.9 KiB
TypeScript
Raw Normal View History

import { z } from 'zod'
import { router, adminProcedure } from '../trpc'
export const auditRouter = router({
/**
* List audit logs with filtering and pagination
*/
list: adminProcedure
.input(
z.object({
userId: z.string().optional(),
action: z.string().optional(),
entityType: z.string().optional(),
entityId: z.string().optional(),
startDate: z.date().optional(),
endDate: z.date().optional(),
page: z.number().int().min(1).default(1),
perPage: z.number().int().min(1).max(100).default(50),
})
)
.query(async ({ ctx, input }) => {
const { userId, action, entityType, entityId, startDate, endDate, page, perPage } = input
const skip = (page - 1) * perPage
const where: Record<string, unknown> = {}
if (userId) where.userId = userId
if (action) where.action = action
if (entityType) where.entityType = entityType
if (entityId) where.entityId = entityId
if (startDate || endDate) {
where.timestamp = {}
if (startDate) (where.timestamp as Record<string, Date>).gte = startDate
if (endDate) (where.timestamp as Record<string, Date>).lte = endDate
}
const [logs, total] = await Promise.all([
ctx.prisma.auditLog.findMany({
where,
skip,
take: perPage,
orderBy: { timestamp: 'desc' },
include: {
user: { select: { name: true, email: true } },
},
}),
ctx.prisma.auditLog.count({ where }),
])
return {
logs,
total,
page,
perPage,
totalPages: Math.ceil(total / perPage),
}
}),
/**
* Get audit logs for a specific entity
*/
getByEntity: adminProcedure
.input(
z.object({
entityType: z.string(),
entityId: z.string(),
})
)
.query(async ({ ctx, input }) => {
return ctx.prisma.auditLog.findMany({
where: {
entityType: input.entityType,
entityId: input.entityId,
},
orderBy: { timestamp: 'desc' },
include: {
user: { select: { name: true, email: true } },
},
})
}),
/**
* Get audit logs for a specific user
*/
getByUser: adminProcedure
.input(
z.object({
userId: z.string(),
limit: z.number().int().min(1).max(100).default(50),
})
)
.query(async ({ ctx, input }) => {
return ctx.prisma.auditLog.findMany({
where: { userId: input.userId },
orderBy: { timestamp: 'desc' },
take: input.limit,
})
}),
/**
* Get recent activity summary
*/
getRecentActivity: adminProcedure
.input(z.object({ limit: z.number().int().min(1).max(50).default(20) }))
.query(async ({ ctx, input }) => {
return ctx.prisma.auditLog.findMany({
orderBy: { timestamp: 'desc' },
take: input.limit,
include: {
user: { select: { name: true, email: true } },
},
})
}),
/**
* Get action statistics
*/
getStats: adminProcedure
.input(
z.object({
startDate: z.date().optional(),
endDate: z.date().optional(),
})
)
.query(async ({ ctx, input }) => {
const where: Record<string, unknown> = {}
if (input.startDate || input.endDate) {
where.timestamp = {}
if (input.startDate) (where.timestamp as Record<string, Date>).gte = input.startDate
if (input.endDate) (where.timestamp as Record<string, Date>).lte = input.endDate
}
const [byAction, byEntity, byUser] = await Promise.all([
ctx.prisma.auditLog.groupBy({
by: ['action'],
where,
_count: true,
orderBy: { _count: { action: 'desc' } },
}),
ctx.prisma.auditLog.groupBy({
by: ['entityType'],
where,
_count: true,
orderBy: { _count: { entityType: 'desc' } },
}),
ctx.prisma.auditLog.groupBy({
by: ['userId'],
where,
_count: true,
orderBy: { _count: { userId: 'desc' } },
take: 10,
}),
])
// Get user names for top users
const userIds = byUser
.map((u) => u.userId)
.filter((id): id is string => id !== null)
const users = await ctx.prisma.user.findMany({
where: { id: { in: userIds } },
select: { id: true, name: true, email: true },
})
const userMap = new Map(users.map((u) => [u.id, u]))
return {
byAction: byAction.map((a) => ({
action: a.action,
count: a._count,
})),
byEntity: byEntity.map((e) => ({
entityType: e.entityType,
count: e._count,
})),
byUser: byUser.map((u) => ({
userId: u.userId,
user: u.userId ? userMap.get(u.userId) : null,
count: u._count,
})),
}
}),
})