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 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;

View File

@ -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,

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 { 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<Recipient, 'email' | 'name' | 'role' | 'authOptions'>;
type PartialRecipient = Pick<Recipient, 'email' | 'name' | 'role' | 'authOptions' | 'expired'>;
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

View File

@ -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,

View File

@ -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({
<Timer className="h-4 w-4" />
Expiry
</DropdownMenuItem>
<DropdownMenuItem disabled={deleteDisabled} className="gap-x-2">
<DropdownMenuItem disabled={deleteDisabled} className="gap-x-2" onClick={onDelete}>
<Trash className="h-4 w-4" />
Delete
</DropdownMenuItem>