mirror of
https://github.com/documenso/documenso.git
synced 2025-11-17 10:11:35 +10:00
feat: audit logs for copying links
This commit is contained in:
@ -17,8 +17,9 @@ import { match } from 'ts-pattern';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { Document, Recipient } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
||||
import { SignatureIcon } from '@documenso/ui/icons/signature';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
@ -40,8 +41,29 @@ export const DocumentPageViewRecipients = ({
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: createAuditLog } = trpc.document.createAuditLog.useMutation();
|
||||
|
||||
const recipients = document.recipients;
|
||||
|
||||
const onCopyLink = (recipient: Recipient) => {
|
||||
toast({
|
||||
title: _(msg`Copied to clipboard`),
|
||||
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||
});
|
||||
|
||||
void createAuditLog({
|
||||
documentId: document.id,
|
||||
type: 'DOCUMENT_SIGNING_LINK_COPIED',
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
isBulkCopy: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">
|
||||
<div className="flex flex-row items-center justify-between px-4 py-3">
|
||||
@ -157,12 +179,7 @@ export const DocumentPageViewRecipients = ({
|
||||
recipient.role !== RecipientRole.CC && (
|
||||
<CopyTextButton
|
||||
value={formatSigningLink(recipient.token)}
|
||||
onCopySuccess={() => {
|
||||
toast({
|
||||
title: _(msg`Copied to clipboard`),
|
||||
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||
});
|
||||
}}
|
||||
onCopySuccess={() => void onCopyLink(recipient)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -15,9 +15,8 @@ import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/g
|
||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import type { Team, TeamEmail } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { Badge } from '@documenso/ui/primitives/badge';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@ -137,7 +136,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
return (
|
||||
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
|
||||
{document.status === DocumentStatus.PENDING && (
|
||||
<DocumentRecipientLinkCopyDialog recipients={recipients} />
|
||||
<DocumentRecipientLinkCopyDialog recipients={recipients} documentId={document.id} />
|
||||
)}
|
||||
|
||||
<Link href={documentRootPath} className="flex items-center text-[#7AC455] hover:opacity-80">
|
||||
@ -169,6 +168,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
recipients={recipients}
|
||||
documentStatus={document.status}
|
||||
position="bottom"
|
||||
documentId={document.id}
|
||||
>
|
||||
<span>
|
||||
<Trans>{recipients.length} Recipient(s)</Trans>
|
||||
|
||||
@ -13,8 +13,7 @@ import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import type { Team } from '@documenso/prisma/client';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus as InternalDocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
|
||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||
@ -127,6 +126,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
|
||||
recipients={recipients}
|
||||
documentStatus={document.status}
|
||||
position="bottom"
|
||||
documentId={document.id}
|
||||
>
|
||||
<span>
|
||||
<Plural one="1 Recipient" other="# Recipients" value={recipients.length} />
|
||||
|
||||
@ -23,8 +23,8 @@ import { useSession } from 'next-auth/react';
|
||||
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
|
||||
import type { Document, Recipient, Team, User } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||
import {
|
||||
@ -190,6 +190,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
|
||||
{canManageDocument && (
|
||||
<DocumentRecipientLinkCopyDialog
|
||||
recipients={row.recipients}
|
||||
documentId={row.id}
|
||||
trigger={
|
||||
<DropdownMenuItem disabled={!isPending} asChild onSelect={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
|
||||
@ -66,6 +66,7 @@ export const DocumentsDataTable = ({
|
||||
<StackAvatarsWithTooltip
|
||||
recipients={row.original.recipients}
|
||||
documentStatus={row.original.status}
|
||||
documentId={row.original.id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@ -117,6 +117,7 @@ export const TemplatePageViewDocumentsTable = ({
|
||||
<StackAvatarsWithTooltip
|
||||
recipients={row.original.recipients}
|
||||
documentStatus={row.original.status}
|
||||
documentId={row.original.id}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
@ -12,6 +10,7 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-
|
||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
@ -20,14 +19,21 @@ import { StackAvatar } from './stack-avatar';
|
||||
export type AvatarWithRecipientProps = {
|
||||
recipient: Recipient;
|
||||
documentStatus: DocumentStatus;
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRecipientProps) {
|
||||
export function AvatarWithRecipient({
|
||||
recipient,
|
||||
documentStatus,
|
||||
documentId,
|
||||
}: AvatarWithRecipientProps) {
|
||||
const [, copy] = useCopyToClipboard();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: createAuditLog } = trpc.document.createAuditLog.useMutation();
|
||||
|
||||
const signingToken = documentStatus === DocumentStatus.PENDING ? recipient.token : null;
|
||||
|
||||
const onRecipientClick = () => {
|
||||
@ -40,6 +46,18 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
|
||||
title: _(msg`Copied to clipboard`),
|
||||
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||
});
|
||||
|
||||
void createAuditLog({
|
||||
documentId,
|
||||
type: 'DOCUMENT_SIGNING_LINK_COPIED',
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
isBulkCopy: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ export type StackAvatarsWithTooltipProps = {
|
||||
recipients: Recipient[];
|
||||
position?: 'top' | 'bottom';
|
||||
children?: React.ReactNode;
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const StackAvatarsWithTooltip = ({
|
||||
@ -27,6 +28,7 @@ export const StackAvatarsWithTooltip = ({
|
||||
recipients,
|
||||
position,
|
||||
children,
|
||||
documentId,
|
||||
}: StackAvatarsWithTooltipProps) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
@ -129,6 +131,7 @@ export const StackAvatarsWithTooltip = ({
|
||||
key={recipient.id}
|
||||
recipient={recipient}
|
||||
documentStatus={documentStatus}
|
||||
documentId={documentId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -144,6 +147,7 @@ export const StackAvatarsWithTooltip = ({
|
||||
key={recipient.id}
|
||||
recipient={recipient}
|
||||
documentStatus={documentStatus}
|
||||
documentId={documentId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -159,6 +163,7 @@ export const StackAvatarsWithTooltip = ({
|
||||
key={recipient.id}
|
||||
recipient={recipient}
|
||||
documentStatus={documentStatus}
|
||||
documentId={documentId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -14,6 +14,7 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -32,11 +33,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
export type DocumentRecipientLinkCopyDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
recipients: Recipient[];
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const DocumentRecipientLinkCopyDialog = ({
|
||||
trigger,
|
||||
recipients,
|
||||
documentId,
|
||||
}: DocumentRecipientLinkCopyDialogProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
@ -50,6 +53,29 @@ export const DocumentRecipientLinkCopyDialog = ({
|
||||
|
||||
const actionSearchParam = searchParams?.get('action');
|
||||
|
||||
const { mutateAsync: createAuditLog } = trpc.document.createAuditLog.useMutation();
|
||||
|
||||
const onCopyLink = async (recipient: Recipient) => {
|
||||
await copy(formatSigningLink(recipient.token)).then(() => {
|
||||
toast({
|
||||
title: _(msg`Copied to clipboard`),
|
||||
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||
});
|
||||
|
||||
void createAuditLog({
|
||||
documentId,
|
||||
type: 'DOCUMENT_SIGNING_LINK_COPIED',
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
isBulkCopy: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onBulkCopy = async () => {
|
||||
const generatedString = recipients
|
||||
.filter((recipient) => recipient.role !== RecipientRole.CC)
|
||||
@ -61,6 +87,24 @@ export const DocumentRecipientLinkCopyDialog = ({
|
||||
title: _(msg`Copied to clipboard`),
|
||||
description: _(msg`All signing links have been copied to your clipboard.`),
|
||||
});
|
||||
|
||||
void Promise.all(
|
||||
recipients
|
||||
.filter((recipient) => recipient.role !== RecipientRole.CC)
|
||||
.map(async (recipient) =>
|
||||
createAuditLog({
|
||||
documentId,
|
||||
type: 'DOCUMENT_SIGNING_LINK_COPIED',
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
isBulkCopy: true,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -112,12 +156,7 @@ export const DocumentRecipientLinkCopyDialog = ({
|
||||
{recipient.role !== RecipientRole.CC && (
|
||||
<CopyTextButton
|
||||
value={formatSigningLink(recipient.token)}
|
||||
onCopySuccess={() => {
|
||||
toast({
|
||||
title: _(msg`Copied to clipboard`),
|
||||
description: _(msg`The signing link has been copied to your clipboard.`),
|
||||
});
|
||||
}}
|
||||
onCopySuccess={async () => onCopyLink(recipient)}
|
||||
badgeContentUncopied={
|
||||
<p className="ml-1 text-xs">
|
||||
<Trans>Copy</Trans>
|
||||
|
||||
Reference in New Issue
Block a user