Initial commit: MOPC platform with Docker deployment setup
Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth. Includes production Dockerfile (multi-stage, port 7600), docker-compose with registry-based image pull, Gitea Actions CI workflow, nginx config for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
184
src/server/routers/audit.ts
Normal file
184
src/server/routers/audit.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
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 = { contains: action, mode: 'insensitive' }
|
||||
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,
|
||||
})),
|
||||
}
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user