185 lines
4.9 KiB
TypeScript
185 lines
4.9 KiB
TypeScript
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,
|
|
})),
|
|
}
|
|
}),
|
|
})
|