From 6e9d17f8ea8e92cfcdd5e2157b3dc6fb55d9981c Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Sun, 17 Nov 2024 16:29:47 +0000 Subject: [PATCH] feat: audit logS --- .../recipient/set-recipient-expiry.ts | 49 ++++++++++--------- packages/lib/types/document-audit-logs.ts | 18 ++++++- packages/lib/utils/document-audit-logs.ts | 29 +++++++++-- .../document-flow/document-expiry-dialog.tsx | 12 ++--- .../document-flow/signer-action-dropdown.tsx | 5 +- 5 files changed, 78 insertions(+), 35 deletions(-) diff --git a/packages/lib/server-only/recipient/set-recipient-expiry.ts b/packages/lib/server-only/recipient/set-recipient-expiry.ts index fd11cf16b..a7d4cf9a6 100644 --- a/packages/lib/server-only/recipient/set-recipient-expiry.ts +++ b/packages/lib/server-only/recipient/set-recipient-expiry.ts @@ -1,7 +1,9 @@ import { prisma } from '@documenso/prisma'; import type { Team } from '@documenso/prisma/client'; +import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; import type { RequestMetadata } from '../../universal/extract-request-metadata'; +import { createDocumentAuditLogData, diffRecipientChanges } from '../../utils/document-audit-logs'; export type SetRecipientExpiryOptions = { documentId: number; @@ -70,7 +72,7 @@ export const setRecipientExpiry = async ({ } const updatedRecipient = await prisma.$transaction(async (tx) => { - const updated = await tx.recipient.update({ + const persisted = await tx.recipient.update({ where: { id: recipient.id, }, @@ -79,28 +81,31 @@ export const setRecipientExpiry = async ({ }, }); - // TODO: fix the audit logs - // await tx.documentAuditLog.create({ - // data: createDocumentAuditLogData({ - // type: 'RECIPIENT_EXPIRY_UPDATED', - // documentId, - // user: { - // id: team?.id ?? user.id, - // email: team?.name ?? user.email, - // name: team ? '' : user.name, - // }, - // data: { - // recipientEmail: recipient.email, - // recipientName: recipient.name, - // recipientId: recipient.id, - // recipientRole: recipient.role, - // expiry, - // }, - // requestMetadata, - // }), - // }); + const changes = diffRecipientChanges(recipient, persisted); - return updated; + if (changes.length > 0) { + await tx.documentAuditLog.create({ + data: createDocumentAuditLogData({ + type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED, + documentId: documentId, + user: { + id: team?.id ?? user.id, + name: team?.name ?? user.name, + email: team ? '' : user.email, + }, + requestMetadata, + data: { + changes, + recipientId, + recipientEmail: persisted.email, + recipientName: persisted.name, + recipientRole: persisted.role, + }, + }), + }); + + return persisted; + } }); return updatedRecipient; diff --git a/packages/lib/types/document-audit-logs.ts b/packages/lib/types/document-audit-logs.ts index 689ca4a78..0f5129913 100644 --- a/packages/lib/types/document-audit-logs.ts +++ b/packages/lib/types/document-audit-logs.ts @@ -34,6 +34,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([ 'DOCUMENT_META_UPDATED', // When the document meta data is updated. 'DOCUMENT_OPENED', // When the document is opened by a recipient. 'DOCUMENT_RECIPIENT_REJECTED', // When a recipient rejects the document. + 'DOCUMENT_RECIPIENT_EXPIRED', // When the recipient cannot access the document anymore. 'DOCUMENT_RECIPIENT_COMPLETED', // When a recipient completes all their required tasks for the document. 'DOCUMENT_SENT', // When the document transitions from DRAFT to PENDING. 'DOCUMENT_TITLE_UPDATED', // When the document title is updated. @@ -65,6 +66,7 @@ export const ZRecipientDiffTypeSchema = z.enum([ 'EMAIL', 'ACCESS_AUTH', 'ACTION_AUTH', + 'EXPIRY', ]); export const DOCUMENT_AUDIT_LOG_TYPE = ZDocumentAuditLogTypeSchema.Enum; @@ -146,12 +148,17 @@ export const ZRecipientDiffEmailSchema = ZGenericFromToSchema.extend({ type: z.literal(RECIPIENT_DIFF_TYPE.EMAIL), }); +export const ZRecipientDiffExpirySchema = ZGenericFromToSchema.extend({ + type: z.literal(RECIPIENT_DIFF_TYPE.EXPIRY), +}); + export const ZDocumentAuditLogRecipientDiffSchema = z.discriminatedUnion('type', [ ZRecipientDiffActionAuthSchema, ZRecipientDiffAccessAuthSchema, ZRecipientDiffNameSchema, ZRecipientDiffRoleSchema, ZRecipientDiffEmailSchema, + ZRecipientDiffExpirySchema, ]); const ZBaseFieldEventDataSchema = z.object({ @@ -365,7 +372,7 @@ export const ZDocumentAuditLogEventDocumentRecipientCompleteSchema = z.object({ }); /** - * Event: Document recipient completed the document (the recipient has fully actioned and completed their required steps for the document). + * Event: Document recipient rejected the document */ export const ZDocumentAuditLogEventDocumentRecipientRejectedSchema = z.object({ type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED), @@ -374,6 +381,14 @@ export const ZDocumentAuditLogEventDocumentRecipientRejectedSchema = z.object({ }), }); +/** + * Event: Recipient expired + */ +export const ZDocumentAuditLogEventDocumentRecipientExpiredSchema = z.object({ + type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_EXPIRED), + data: ZBaseRecipientDataSchema, +}); + /** * Event: Document sent. */ @@ -499,6 +514,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and( ZDocumentAuditLogEventDocumentOpenedSchema, ZDocumentAuditLogEventDocumentRecipientCompleteSchema, ZDocumentAuditLogEventDocumentRecipientRejectedSchema, + ZDocumentAuditLogEventDocumentRecipientExpiredSchema, ZDocumentAuditLogEventDocumentSentSchema, ZDocumentAuditLogEventDocumentTitleUpdatedSchema, ZDocumentAuditLogEventDocumentExternalIdUpdatedSchema, diff --git a/packages/lib/utils/document-audit-logs.ts b/packages/lib/utils/document-audit-logs.ts index bb4f9bc8e..1987e21d9 100644 --- a/packages/lib/utils/document-audit-logs.ts +++ b/packages/lib/utils/document-audit-logs.ts @@ -1,5 +1,6 @@ -import type { I18n } from '@lingui/core'; +import { type I18n, i18n } from '@lingui/core'; import { msg } from '@lingui/macro'; +import { DateTime } from 'luxon'; import { match } from 'ts-pattern'; import type { DocumentAuditLog, DocumentMeta, Field, Recipient } from '@documenso/prisma/client'; @@ -73,7 +74,7 @@ export const parseDocumentAuditLogData = (auditLog: DocumentAuditLog): TDocument return data.data; }; -type PartialRecipient = Pick; +type PartialRecipient = Pick; export const diffRecipientChanges = ( oldRecipient: PartialRecipient, @@ -131,6 +132,18 @@ export const diffRecipientChanges = ( }); } + if (oldRecipient.expired !== newRecipient.expired) { + diffs.push({ + type: RECIPIENT_DIFF_TYPE.EXPIRY, + from: DateTime.fromJSDate(oldRecipient.expired!) + .setLocale(i18n.locales?.[0] || i18n.locale) + .toLocaleString(DateTime.DATETIME_FULL), + to: DateTime.fromJSDate(newRecipient.expired!) + .setLocale(i18n.locales?.[0] || i18n.locale) + .toLocaleString(DateTime.DATETIME_FULL), + }); + } + return diffs; }; @@ -349,7 +362,7 @@ export const formatDocumentAuditLogAction = ( identified: result, }; }) - .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED }, ({ data }) => { + .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED }, () => { const userName = prefix || _(msg`Recipient`); const result = msg`${userName} rejected the document`; @@ -359,6 +372,16 @@ export const formatDocumentAuditLogAction = ( identified: result, }; }) + .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_EXPIRED }, () => { + const userName = prefix || _(msg`Recipient`); + + const result = msg`${userName} expired`; + + return { + anonymous: result, + identified: result, + }; + }) .with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => ({ anonymous: data.isResending ? msg`Email resent` : msg`Email sent`, identified: data.isResending diff --git a/packages/ui/primitives/document-flow/document-expiry-dialog.tsx b/packages/ui/primitives/document-flow/document-expiry-dialog.tsx index 0a752c0b2..326ceabaf 100644 --- a/packages/ui/primitives/document-flow/document-expiry-dialog.tsx +++ b/packages/ui/primitives/document-flow/document-expiry-dialog.tsx @@ -66,7 +66,7 @@ type DocumentExpiryDialogProps = { documentId: number; }; -export default function DocumentExpiryDialog({ +export function DocumentExpiryDialog({ open, onOpenChange, signer, @@ -98,12 +98,12 @@ export default function DocumentExpiryDialog({ const watchUnit = periodForm.watch('unit'); const { mutateAsync: setSignerExpiry, isLoading } = trpc.recipient.setSignerExpiry.useMutation({ - onSuccess: ({ expired }) => { + onSuccess: (updatedRecipient) => { router.refresh(); periodForm.reset( - expired - ? calculatePeriod(expired) + updatedRecipient?.expired + ? calculatePeriod(updatedRecipient.expired) : { amount: undefined, unit: undefined, @@ -112,7 +112,7 @@ export default function DocumentExpiryDialog({ dateForm.reset( { - expiry: expired ?? undefined, + expiry: updatedRecipient?.expired ?? undefined, }, { keepValues: false, @@ -167,8 +167,6 @@ export default function DocumentExpiryDialog({ } } - console.log('finalll expiry date', expiryDate); - await setSignerExpiry({ documentId, signerId: signer.nativeId, diff --git a/packages/ui/primitives/document-flow/signer-action-dropdown.tsx b/packages/ui/primitives/document-flow/signer-action-dropdown.tsx index 5d5b79f3b..1fdfb4225 100644 --- a/packages/ui/primitives/document-flow/signer-action-dropdown.tsx +++ b/packages/ui/primitives/document-flow/signer-action-dropdown.tsx @@ -14,7 +14,7 @@ import { import { cn } from '../../lib/utils'; import type { TAddSignerSchema as Signer } from './add-signers.types'; -import DocumentExpiryDialog from './document-expiry-dialog'; +import { DocumentExpiryDialog } from './document-expiry-dialog'; type SignerActionDropdownProps = { onDelete: () => void; @@ -29,6 +29,7 @@ export function SignerActionDropdown({ className, signer, documentId, + onDelete, }: SignerActionDropdownProps) { const [isExpiryDialogOpen, setExpiryDialogOpen] = useState(false); @@ -45,7 +46,7 @@ export function SignerActionDropdown({ Expiry - + Delete