feat: audit logS

This commit is contained in:
Ephraim Atta-Duncan
2024-11-17 16:29:47 +00:00
parent 8491c69e8c
commit 6e9d17f8ea
5 changed files with 78 additions and 35 deletions

View File

@ -1,7 +1,9 @@
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import type { Team } from '@documenso/prisma/client'; 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 type { RequestMetadata } from '../../universal/extract-request-metadata';
import { createDocumentAuditLogData, diffRecipientChanges } from '../../utils/document-audit-logs';
export type SetRecipientExpiryOptions = { export type SetRecipientExpiryOptions = {
documentId: number; documentId: number;
@ -70,7 +72,7 @@ export const setRecipientExpiry = async ({
} }
const updatedRecipient = await prisma.$transaction(async (tx) => { const updatedRecipient = await prisma.$transaction(async (tx) => {
const updated = await tx.recipient.update({ const persisted = await tx.recipient.update({
where: { where: {
id: recipient.id, id: recipient.id,
}, },
@ -79,28 +81,31 @@ export const setRecipientExpiry = async ({
}, },
}); });
// TODO: fix the audit logs const changes = diffRecipientChanges(recipient, persisted);
// 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,
// }),
// });
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; return updatedRecipient;

View File

@ -34,6 +34,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
'DOCUMENT_META_UPDATED', // When the document meta data is updated. 'DOCUMENT_META_UPDATED', // When the document meta data is updated.
'DOCUMENT_OPENED', // When the document is opened by a recipient. 'DOCUMENT_OPENED', // When the document is opened by a recipient.
'DOCUMENT_RECIPIENT_REJECTED', // When a recipient rejects the document. '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_RECIPIENT_COMPLETED', // When a recipient completes all their required tasks for the document.
'DOCUMENT_SENT', // When the document transitions from DRAFT to PENDING. 'DOCUMENT_SENT', // When the document transitions from DRAFT to PENDING.
'DOCUMENT_TITLE_UPDATED', // When the document title is updated. 'DOCUMENT_TITLE_UPDATED', // When the document title is updated.
@ -65,6 +66,7 @@ export const ZRecipientDiffTypeSchema = z.enum([
'EMAIL', 'EMAIL',
'ACCESS_AUTH', 'ACCESS_AUTH',
'ACTION_AUTH', 'ACTION_AUTH',
'EXPIRY',
]); ]);
export const DOCUMENT_AUDIT_LOG_TYPE = ZDocumentAuditLogTypeSchema.Enum; export const DOCUMENT_AUDIT_LOG_TYPE = ZDocumentAuditLogTypeSchema.Enum;
@ -146,12 +148,17 @@ export const ZRecipientDiffEmailSchema = ZGenericFromToSchema.extend({
type: z.literal(RECIPIENT_DIFF_TYPE.EMAIL), 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', [ export const ZDocumentAuditLogRecipientDiffSchema = z.discriminatedUnion('type', [
ZRecipientDiffActionAuthSchema, ZRecipientDiffActionAuthSchema,
ZRecipientDiffAccessAuthSchema, ZRecipientDiffAccessAuthSchema,
ZRecipientDiffNameSchema, ZRecipientDiffNameSchema,
ZRecipientDiffRoleSchema, ZRecipientDiffRoleSchema,
ZRecipientDiffEmailSchema, ZRecipientDiffEmailSchema,
ZRecipientDiffExpirySchema,
]); ]);
const ZBaseFieldEventDataSchema = z.object({ 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({ export const ZDocumentAuditLogEventDocumentRecipientRejectedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED), 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. * Event: Document sent.
*/ */
@ -499,6 +514,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
ZDocumentAuditLogEventDocumentOpenedSchema, ZDocumentAuditLogEventDocumentOpenedSchema,
ZDocumentAuditLogEventDocumentRecipientCompleteSchema, ZDocumentAuditLogEventDocumentRecipientCompleteSchema,
ZDocumentAuditLogEventDocumentRecipientRejectedSchema, ZDocumentAuditLogEventDocumentRecipientRejectedSchema,
ZDocumentAuditLogEventDocumentRecipientExpiredSchema,
ZDocumentAuditLogEventDocumentSentSchema, ZDocumentAuditLogEventDocumentSentSchema,
ZDocumentAuditLogEventDocumentTitleUpdatedSchema, ZDocumentAuditLogEventDocumentTitleUpdatedSchema,
ZDocumentAuditLogEventDocumentExternalIdUpdatedSchema, ZDocumentAuditLogEventDocumentExternalIdUpdatedSchema,

View File

@ -1,5 +1,6 @@
import type { I18n } from '@lingui/core'; import { type I18n, i18n } from '@lingui/core';
import { msg } from '@lingui/macro'; import { msg } from '@lingui/macro';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import type { DocumentAuditLog, DocumentMeta, Field, Recipient } from '@documenso/prisma/client'; import type { DocumentAuditLog, DocumentMeta, Field, Recipient } from '@documenso/prisma/client';
@ -73,7 +74,7 @@ export const parseDocumentAuditLogData = (auditLog: DocumentAuditLog): TDocument
return data.data; return data.data;
}; };
type PartialRecipient = Pick<Recipient, 'email' | 'name' | 'role' | 'authOptions'>; type PartialRecipient = Pick<Recipient, 'email' | 'name' | 'role' | 'authOptions' | 'expired'>;
export const diffRecipientChanges = ( export const diffRecipientChanges = (
oldRecipient: PartialRecipient, 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; return diffs;
}; };
@ -349,7 +362,7 @@ export const formatDocumentAuditLogAction = (
identified: result, 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 userName = prefix || _(msg`Recipient`);
const result = msg`${userName} rejected the document`; const result = msg`${userName} rejected the document`;
@ -359,6 +372,16 @@ export const formatDocumentAuditLogAction = (
identified: result, 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 }) => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT }, ({ data }) => ({
anonymous: data.isResending ? msg`Email resent` : msg`Email sent`, anonymous: data.isResending ? msg`Email resent` : msg`Email sent`,
identified: data.isResending identified: data.isResending

View File

@ -66,7 +66,7 @@ type DocumentExpiryDialogProps = {
documentId: number; documentId: number;
}; };
export default function DocumentExpiryDialog({ export function DocumentExpiryDialog({
open, open,
onOpenChange, onOpenChange,
signer, signer,
@ -98,12 +98,12 @@ export default function DocumentExpiryDialog({
const watchUnit = periodForm.watch('unit'); const watchUnit = periodForm.watch('unit');
const { mutateAsync: setSignerExpiry, isLoading } = trpc.recipient.setSignerExpiry.useMutation({ const { mutateAsync: setSignerExpiry, isLoading } = trpc.recipient.setSignerExpiry.useMutation({
onSuccess: ({ expired }) => { onSuccess: (updatedRecipient) => {
router.refresh(); router.refresh();
periodForm.reset( periodForm.reset(
expired updatedRecipient?.expired
? calculatePeriod(expired) ? calculatePeriod(updatedRecipient.expired)
: { : {
amount: undefined, amount: undefined,
unit: undefined, unit: undefined,
@ -112,7 +112,7 @@ export default function DocumentExpiryDialog({
dateForm.reset( dateForm.reset(
{ {
expiry: expired ?? undefined, expiry: updatedRecipient?.expired ?? undefined,
}, },
{ {
keepValues: false, keepValues: false,
@ -167,8 +167,6 @@ export default function DocumentExpiryDialog({
} }
} }
console.log('finalll expiry date', expiryDate);
await setSignerExpiry({ await setSignerExpiry({
documentId, documentId,
signerId: signer.nativeId, signerId: signer.nativeId,

View File

@ -14,7 +14,7 @@ import {
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import type { TAddSignerSchema as Signer } from './add-signers.types'; import type { TAddSignerSchema as Signer } from './add-signers.types';
import DocumentExpiryDialog from './document-expiry-dialog'; import { DocumentExpiryDialog } from './document-expiry-dialog';
type SignerActionDropdownProps = { type SignerActionDropdownProps = {
onDelete: () => void; onDelete: () => void;
@ -29,6 +29,7 @@ export function SignerActionDropdown({
className, className,
signer, signer,
documentId, documentId,
onDelete,
}: SignerActionDropdownProps) { }: SignerActionDropdownProps) {
const [isExpiryDialogOpen, setExpiryDialogOpen] = useState(false); const [isExpiryDialogOpen, setExpiryDialogOpen] = useState(false);
@ -45,7 +46,7 @@ export function SignerActionDropdown({
<Timer className="h-4 w-4" /> <Timer className="h-4 w-4" />
Expiry Expiry
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem disabled={deleteDisabled} className="gap-x-2"> <DropdownMenuItem disabled={deleteDisabled} className="gap-x-2" onClick={onDelete}>
<Trash className="h-4 w-4" /> <Trash className="h-4 w-4" />
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>