From 1a577e55a9f475dc50b0e9f3fdb0d844ac5ab476 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Sat, 22 Nov 2025 01:12:56 +0000 Subject: [PATCH] feat: add envelope audit logs endpoint --- .../document/find-document-audit-logs.ts | 118 ++++++++++++++++++ .../find-envelope-audit-logs.ts | 43 +++++++ .../find-envelope-audit-logs.types.ts | 35 ++++++ .../trpc/server/envelope-router/router.ts | 4 + 4 files changed, 200 insertions(+) create mode 100644 packages/trpc/server/envelope-router/find-envelope-audit-logs.ts create mode 100644 packages/trpc/server/envelope-router/find-envelope-audit-logs.types.ts diff --git a/packages/lib/server-only/document/find-document-audit-logs.ts b/packages/lib/server-only/document/find-document-audit-logs.ts index 6706efe21..33f9de25f 100644 --- a/packages/lib/server-only/document/find-document-audit-logs.ts +++ b/packages/lib/server-only/document/find-document-audit-logs.ts @@ -117,3 +117,121 @@ export const findDocumentAuditLogs = async ({ nextCursor, } satisfies FindResultResponse & { nextCursor?: string }; }; + +export interface FindEnvelopeAuditLogsOptions { + userId: number; + teamId: number; + envelopeId: string; + page?: number; + perPage?: number; + orderBy?: { + column: keyof DocumentAuditLog; + direction: 'asc' | 'desc'; + }; + cursor?: string; + filterForRecentActivity?: boolean; +} + +export const findEnvelopeAuditLogs = async ({ + userId, + teamId, + envelopeId, + page = 1, + perPage = 30, + orderBy, + cursor, + filterForRecentActivity, +}: FindEnvelopeAuditLogsOptions) => { + const orderByColumn = orderBy?.column ?? 'createdAt'; + const orderByDirection = orderBy?.direction ?? 'desc'; + + // Auto-detect ID type: if it's a numeric string, treat as documentId + const isNumericId = /^\d+$/.test(envelopeId); + + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: isNumericId + ? { + type: 'documentId', + id: Number(envelopeId), + } + : { + type: 'envelopeId', + id: envelopeId, + }, + type: isNumericId ? EnvelopeType.DOCUMENT : null, + userId, + teamId, + }); + + const envelope = await prisma.envelope.findUnique({ + where: envelopeWhereInput, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND); + } + + const whereClause: Prisma.DocumentAuditLogWhereInput = { + envelopeId: envelope.id, + }; + + // Filter events down to what we consider recent activity. + if (filterForRecentActivity) { + whereClause.OR = [ + { + type: { + in: [ + DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED, + DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED, + DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED, + DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED, + DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, + DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, + DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT, + DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM, + ], + }, + }, + { + type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT, + data: { + path: ['isResending'], + equals: true, + }, + }, + ]; + } + + const [data, count] = await Promise.all([ + prisma.documentAuditLog.findMany({ + where: whereClause, + skip: Math.max(page - 1, 0) * perPage, + take: perPage + 1, + orderBy: { + [orderByColumn]: orderByDirection, + }, + cursor: cursor ? { id: cursor } : undefined, + }), + prisma.documentAuditLog.count({ + where: whereClause, + }), + ]); + + let nextCursor: string | undefined = undefined; + + const parsedData = data.map((auditLog) => parseDocumentAuditLogData(auditLog)); + + if (parsedData.length > perPage) { + const nextItem = parsedData.pop(); + nextCursor = nextItem!.id; + } + + return { + data: parsedData, + count, + currentPage: Math.max(page, 1), + perPage, + totalPages: Math.ceil(count / perPage), + nextCursor, + } satisfies FindResultResponse & { nextCursor?: string }; +}; diff --git a/packages/trpc/server/envelope-router/find-envelope-audit-logs.ts b/packages/trpc/server/envelope-router/find-envelope-audit-logs.ts new file mode 100644 index 000000000..13427ac7e --- /dev/null +++ b/packages/trpc/server/envelope-router/find-envelope-audit-logs.ts @@ -0,0 +1,43 @@ +import { findEnvelopeAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs'; + +import { authenticatedProcedure } from '../trpc'; +import { + ZFindEnvelopeAuditLogsRequestSchema, + ZFindEnvelopeAuditLogsResponseSchema, + findEnvelopeAuditLogsMeta, +} from './find-envelope-audit-logs.types'; + +export const findEnvelopeAuditLogsRoute = authenticatedProcedure + .meta(findEnvelopeAuditLogsMeta) + .input(ZFindEnvelopeAuditLogsRequestSchema) + .output(ZFindEnvelopeAuditLogsResponseSchema) + .query(async ({ input, ctx }) => { + const { teamId } = ctx; + + const { + page, + perPage, + envelopeId, + cursor, + filterForRecentActivity, + orderByColumn, + orderByDirection, + } = input; + + ctx.logger.info({ + input: { + envelopeId, + }, + }); + + return await findEnvelopeAuditLogs({ + userId: ctx.user.id, + teamId, + page, + perPage, + envelopeId, + cursor, + filterForRecentActivity, + orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined, + }); + }); diff --git a/packages/trpc/server/envelope-router/find-envelope-audit-logs.types.ts b/packages/trpc/server/envelope-router/find-envelope-audit-logs.types.ts new file mode 100644 index 000000000..732cf74d3 --- /dev/null +++ b/packages/trpc/server/envelope-router/find-envelope-audit-logs.types.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; + +import { ZDocumentAuditLogSchema } from '@documenso/lib/types/document-audit-logs'; +import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params'; + +import type { TrpcRouteMeta } from '../trpc'; + +export const findEnvelopeAuditLogsMeta: TrpcRouteMeta = { + openapi: { + method: 'GET', + path: '/envelope/{envelopeId}/audit-log', + summary: 'Get envelope audit logs', + description: + 'Returns paginated audit logs for an envelope given an ID. Accepts both envelope IDs (string) and legacy document IDs (number).', + tags: ['Envelope'], + }, +}; + +export const ZFindEnvelopeAuditLogsRequestSchema = ZFindSearchParamsSchema.extend({ + envelopeId: z + .string() + .describe('Envelope ID (e.g., envelope_xxx) or legacy document ID (e.g., 12345)'), + cursor: z.string().optional(), + filterForRecentActivity: z.boolean().optional(), + orderByColumn: z.enum(['createdAt', 'type']).optional(), + orderByDirection: z.enum(['asc', 'desc']).default('desc'), +}); + +export const ZFindEnvelopeAuditLogsResponseSchema = ZFindResultResponse.extend({ + data: ZDocumentAuditLogSchema.array(), + nextCursor: z.string().optional(), +}); + +export type TFindEnvelopeAuditLogsRequest = z.infer; +export type TFindEnvelopeAuditLogsResponse = z.infer; diff --git a/packages/trpc/server/envelope-router/router.ts b/packages/trpc/server/envelope-router/router.ts index e11709eb2..f7aad1850 100644 --- a/packages/trpc/server/envelope-router/router.ts +++ b/packages/trpc/server/envelope-router/router.ts @@ -18,6 +18,7 @@ import { createEnvelopeRecipientsRoute } from './envelope-recipients/create-enve import { deleteEnvelopeRecipientRoute } from './envelope-recipients/delete-envelope-recipient'; import { getEnvelopeRecipientRoute } from './envelope-recipients/get-envelope-recipient'; import { updateEnvelopeRecipientsRoute } from './envelope-recipients/update-envelope-recipients'; +import { findEnvelopeAuditLogsRoute } from './find-envelope-audit-logs'; import { getEnvelopeRoute } from './get-envelope'; import { getEnvelopeItemsRoute } from './get-envelope-items'; import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token'; @@ -65,6 +66,9 @@ export const envelopeRouter = router({ set: setEnvelopeFieldsRoute, sign: signEnvelopeFieldRoute, }, + auditLog: { + find: findEnvelopeAuditLogsRoute, + }, get: getEnvelopeRoute, create: createEnvelopeRoute, use: useEnvelopeRoute,