import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { and, desc, eq, lt } from "drizzle-orm"; import { auditLog, users } from "@tasks/database/schema"; import { router, workspaceProcedure } from "@/server/trpc"; const DEFAULT_LIMIT = 50; const MAX_LIMIT = 200; /** * Audit log read API. Owner-only; this is a debugging / support surface, * not a feature stream. Kept narrow on purpose — pagination is keyset on * `(created_at, id)` so we can scale rows without LIMIT/OFFSET pain. */ export const auditRouter = router({ list: workspaceProcedure .input( z.object({ limit: z.number().int().min(1).max(MAX_LIMIT).optional(), // Keyset cursor: a row's `createdAt` ISO string. Rows older than this // are returned next. We use createdAt-only (no id tiebreaker) because // the index is `(workspace_id, created_at)`. In the rare event of // identical timestamps the user can refresh; we're not building a // realtime feed here. cursorCreatedAt: z.string().datetime().optional(), }), ) .query(async ({ ctx, input }) => { if (ctx.workspace.role !== "owner") { throw new TRPCError({ code: "FORBIDDEN", message: "Only the workspace owner can view the audit log.", }); } const limit = input.limit ?? DEFAULT_LIMIT; const cursorDate = input.cursorCreatedAt ? new Date(input.cursorCreatedAt) : null; const rows = await ctx.db .select({ id: auditLog.id, action: auditLog.action, targetType: auditLog.targetType, targetId: auditLog.targetId, metadata: auditLog.metadata, createdAt: auditLog.createdAt, actorUserId: auditLog.actorUserId, actorName: users.name, actorEmail: users.email, }) .from(auditLog) .leftJoin(users, eq(auditLog.actorUserId, users.id)) .where( cursorDate ? and( eq(auditLog.workspaceId, ctx.workspace.id), lt(auditLog.createdAt, cursorDate), ) : eq(auditLog.workspaceId, ctx.workspace.id), ) .orderBy(desc(auditLog.createdAt)) .limit(limit + 1); const hasMore = rows.length > limit; const page = hasMore ? rows.slice(0, limit) : rows; const nextCursor = hasMore ? page[page.length - 1]!.createdAt.toISOString() : null; return { rows: page, nextCursor }; }), }); export type AuditRouter = typeof auditRouter;