mirror of
https://github.com/documenso/documenso.git
synced 2025-11-19 11:12:06 +10:00
feat: audit logS
This commit is contained in:
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user