Compare commits

...

3 Commits

16 changed files with 198 additions and 31 deletions

View File

@ -4,7 +4,7 @@ import { useMemo } from 'react';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { AlertTriangle, CheckCheckIcon, CheckIcon, Loader, MailOpen } from 'lucide-react'; import { AlertTriangle, CheckCheckIcon, CheckIcon, Copy, Loader, MailOpen } from 'lucide-react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -141,6 +141,11 @@ export const DocumentPageViewRecentActivity = ({
<MailOpen className="h-3 w-3" aria-hidden="true" /> <MailOpen className="h-3 w-3" aria-hidden="true" />
</div> </div>
)) ))
.with(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_LINK_COPIED, () => (
<div className="bg-widget rounded-full border border-gray-300 p-1 dark:border-neutral-600">
<Copy className="h-3 w-3" aria-hidden="true" />
</div>
))
.otherwise(() => ( .otherwise(() => (
<div className="bg-widget h-1.5 w-1.5 rounded-full ring-1 ring-gray-300 dark:ring-neutral-600" /> <div className="bg-widget h-1.5 w-1.5 rounded-full ring-1 ring-gray-300 dark:ring-neutral-600" />
))} ))}

View File

@ -17,8 +17,9 @@ import { match } from 'ts-pattern';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { formatSigningLink } from '@documenso/lib/utils/recipients'; import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
import type { Document, Recipient } 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 { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { SignatureIcon } from '@documenso/ui/icons/signature'; import { SignatureIcon } from '@documenso/ui/icons/signature';
import { AvatarWithText } from '@documenso/ui/primitives/avatar'; import { AvatarWithText } from '@documenso/ui/primitives/avatar';
@ -40,8 +41,29 @@ export const DocumentPageViewRecipients = ({
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { mutateAsync: createAuditLog } = trpc.document.createAuditLog.useMutation();
const recipients = document.recipients; 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 ( return (
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border"> <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"> <div className="flex flex-row items-center justify-between px-4 py-3">
@ -157,12 +179,7 @@ export const DocumentPageViewRecipients = ({
recipient.role !== RecipientRole.CC && ( recipient.role !== RecipientRole.CC && (
<CopyTextButton <CopyTextButton
value={formatSigningLink(recipient.token)} value={formatSigningLink(recipient.token)}
onCopySuccess={() => { onCopySuccess={() => void onCopyLink(recipient)}
toast({
title: _(msg`Copied to clipboard`),
description: _(msg`The signing link has been copied to your clipboard.`),
});
}}
/> />
)} )}
</div> </div>

View File

@ -15,9 +15,8 @@ import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/g
import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto'; import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentStatus } from '@documenso/prisma/client';
import type { Team, TeamEmail } 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 { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -137,7 +136,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
return ( return (
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8"> <div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
{document.status === DocumentStatus.PENDING && ( {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"> <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} recipients={recipients}
documentStatus={document.status} documentStatus={document.status}
position="bottom" position="bottom"
documentId={document.id}
> >
<span> <span>
<Trans>{recipients.length} Recipient(s)</Trans> <Trans>{recipients.length} Recipient(s)</Trans>

View File

@ -13,8 +13,7 @@ import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { symmetricDecrypt } from '@documenso/lib/universal/crypto'; import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Team } from '@documenso/prisma/client'; import type { Team } from '@documenso/prisma/client';
import { TeamMemberRole } from '@documenso/prisma/client'; import { DocumentStatus as InternalDocumentStatus, TeamMemberRole } from '@documenso/prisma/client';
import { DocumentStatus as InternalDocumentStatus } from '@documenso/prisma/client';
import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document'; import { EditDocumentForm } from '~/app/(dashboard)/documents/[id]/edit-document';
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip'; import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
@ -127,6 +126,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
recipients={recipients} recipients={recipients}
documentStatus={document.status} documentStatus={document.status}
position="bottom" position="bottom"
documentId={document.id}
> >
<span> <span>
<Plural one="1 Recipient" other="# Recipients" value={recipients.length} /> <Plural one="1 Recipient" other="# Recipients" value={recipients.length} />

View File

@ -23,8 +23,8 @@ import { useSession } from 'next-auth/react';
import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; 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 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 { trpc as trpcClient } from '@documenso/trpc/client';
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
import { import {
@ -190,6 +190,7 @@ export const DataTableActionDropdown = ({ row, team }: DataTableActionDropdownPr
{canManageDocument && ( {canManageDocument && (
<DocumentRecipientLinkCopyDialog <DocumentRecipientLinkCopyDialog
recipients={row.recipients} recipients={row.recipients}
documentId={row.id}
trigger={ trigger={
<DropdownMenuItem disabled={!isPending} asChild onSelect={(e) => e.preventDefault()}> <DropdownMenuItem disabled={!isPending} asChild onSelect={(e) => e.preventDefault()}>
<div> <div>

View File

@ -66,6 +66,7 @@ export const DocumentsDataTable = ({
<StackAvatarsWithTooltip <StackAvatarsWithTooltip
recipients={row.original.recipients} recipients={row.original.recipients}
documentStatus={row.original.status} documentStatus={row.original.status}
documentId={row.original.id}
/> />
), ),
}, },

View File

@ -117,6 +117,7 @@ export const TemplatePageViewDocumentsTable = ({
<StackAvatarsWithTooltip <StackAvatarsWithTooltip
recipients={row.original.recipients} recipients={row.original.recipients}
documentStatus={row.original.status} documentStatus={row.original.status}
documentId={row.original.id}
/> />
), ),
}, },

View File

@ -1,7 +1,5 @@
'use client'; 'use client';
import React from 'react';
import { msg } from '@lingui/macro'; import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; 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 { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import type { Recipient } from '@documenso/prisma/client'; import type { Recipient } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client'; import { DocumentStatus } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
@ -20,14 +19,21 @@ import { StackAvatar } from './stack-avatar';
export type AvatarWithRecipientProps = { export type AvatarWithRecipientProps = {
recipient: Recipient; recipient: Recipient;
documentStatus: DocumentStatus; documentStatus: DocumentStatus;
documentId: number;
}; };
export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRecipientProps) { export function AvatarWithRecipient({
recipient,
documentStatus,
documentId,
}: AvatarWithRecipientProps) {
const [, copy] = useCopyToClipboard(); const [, copy] = useCopyToClipboard();
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const { mutateAsync: createAuditLog } = trpc.document.createAuditLog.useMutation();
const signingToken = documentStatus === DocumentStatus.PENDING ? recipient.token : null; const signingToken = documentStatus === DocumentStatus.PENDING ? recipient.token : null;
const onRecipientClick = () => { const onRecipientClick = () => {
@ -40,6 +46,18 @@ export function AvatarWithRecipient({ recipient, documentStatus }: AvatarWithRec
title: _(msg`Copied to clipboard`), title: _(msg`Copied to clipboard`),
description: _(msg`The signing link has been copied to your 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,
},
});
}); });
}; };

View File

@ -20,6 +20,7 @@ export type StackAvatarsWithTooltipProps = {
recipients: Recipient[]; recipients: Recipient[];
position?: 'top' | 'bottom'; position?: 'top' | 'bottom';
children?: React.ReactNode; children?: React.ReactNode;
documentId: number;
}; };
export const StackAvatarsWithTooltip = ({ export const StackAvatarsWithTooltip = ({
@ -27,6 +28,7 @@ export const StackAvatarsWithTooltip = ({
recipients, recipients,
position, position,
children, children,
documentId,
}: StackAvatarsWithTooltipProps) => { }: StackAvatarsWithTooltipProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
@ -129,6 +131,7 @@ export const StackAvatarsWithTooltip = ({
key={recipient.id} key={recipient.id}
recipient={recipient} recipient={recipient}
documentStatus={documentStatus} documentStatus={documentStatus}
documentId={documentId}
/> />
))} ))}
</div> </div>
@ -144,6 +147,7 @@ export const StackAvatarsWithTooltip = ({
key={recipient.id} key={recipient.id}
recipient={recipient} recipient={recipient}
documentStatus={documentStatus} documentStatus={documentStatus}
documentId={documentId}
/> />
))} ))}
</div> </div>
@ -159,6 +163,7 @@ export const StackAvatarsWithTooltip = ({
key={recipient.id} key={recipient.id}
recipient={recipient} recipient={recipient}
documentStatus={documentStatus} documentStatus={documentStatus}
documentId={documentId}
/> />
))} ))}
</div> </div>

View File

@ -14,6 +14,7 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-
import { formatSigningLink } from '@documenso/lib/utils/recipients'; import { formatSigningLink } from '@documenso/lib/utils/recipients';
import type { Recipient } from '@documenso/prisma/client'; import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } 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 { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { AvatarWithText } from '@documenso/ui/primitives/avatar'; import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
@ -32,11 +33,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentRecipientLinkCopyDialogProps = { export type DocumentRecipientLinkCopyDialogProps = {
trigger?: React.ReactNode; trigger?: React.ReactNode;
recipients: Recipient[]; recipients: Recipient[];
documentId: number;
}; };
export const DocumentRecipientLinkCopyDialog = ({ export const DocumentRecipientLinkCopyDialog = ({
trigger, trigger,
recipients, recipients,
documentId,
}: DocumentRecipientLinkCopyDialogProps) => { }: DocumentRecipientLinkCopyDialogProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@ -50,6 +53,29 @@ export const DocumentRecipientLinkCopyDialog = ({
const actionSearchParam = searchParams?.get('action'); 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 onBulkCopy = async () => {
const generatedString = recipients const generatedString = recipients
.filter((recipient) => recipient.role !== RecipientRole.CC) .filter((recipient) => recipient.role !== RecipientRole.CC)
@ -61,6 +87,24 @@ export const DocumentRecipientLinkCopyDialog = ({
title: _(msg`Copied to clipboard`), title: _(msg`Copied to clipboard`),
description: _(msg`All signing links have been copied to your 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 && ( {recipient.role !== RecipientRole.CC && (
<CopyTextButton <CopyTextButton
value={formatSigningLink(recipient.token)} value={formatSigningLink(recipient.token)}
onCopySuccess={() => { onCopySuccess={async () => onCopyLink(recipient)}
toast({
title: _(msg`Copied to clipboard`),
description: _(msg`The signing link has been copied to your clipboard.`),
});
}}
badgeContentUncopied={ badgeContentUncopied={
<p className="ml-1 text-xs"> <p className="ml-1 text-xs">
<Trans>Copy</Trans> <Trans>Copy</Trans>

View File

@ -76,6 +76,7 @@ export const findDocumentAuditLogs = async ({
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT, DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM, DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_MOVED_TO_TEAM,
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_LINK_COPIED,
], ],
}, },
}, },

View File

@ -39,6 +39,7 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
'DOCUMENT_TITLE_UPDATED', // When the document title is updated. 'DOCUMENT_TITLE_UPDATED', // When the document title is updated.
'DOCUMENT_EXTERNAL_ID_UPDATED', // When the document external ID is updated. 'DOCUMENT_EXTERNAL_ID_UPDATED', // When the document external ID is updated.
'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team. 'DOCUMENT_MOVED_TO_TEAM', // When the document is moved to a team.
'DOCUMENT_SIGNING_LINK_COPIED', // When a signing link is copied.
]); ]);
export const ZDocumentAuditLogEmailTypeSchema = z.enum([ export const ZDocumentAuditLogEmailTypeSchema = z.enum([
@ -225,6 +226,16 @@ export const ZDocumentAuditLogEventDocumentDeletedSchema = z.object({
}), }),
}); });
/**
* Event: Document signing link copied.
*/
export const ZDocumentAuditLogEventDocumentSigningLinkCopiedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_LINK_COPIED),
data: ZBaseRecipientDataSchema.extend({
isBulkCopy: z.boolean(),
}),
});
/** /**
* Event: Document field inserted. * Event: Document field inserted.
*/ */
@ -490,6 +501,7 @@ export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
ZDocumentAuditLogEventDocumentCompletedSchema, ZDocumentAuditLogEventDocumentCompletedSchema,
ZDocumentAuditLogEventDocumentCreatedSchema, ZDocumentAuditLogEventDocumentCreatedSchema,
ZDocumentAuditLogEventDocumentDeletedSchema, ZDocumentAuditLogEventDocumentDeletedSchema,
ZDocumentAuditLogEventDocumentSigningLinkCopiedSchema,
ZDocumentAuditLogEventDocumentMovedToTeamSchema, ZDocumentAuditLogEventDocumentMovedToTeamSchema,
ZDocumentAuditLogEventDocumentFieldInsertedSchema, ZDocumentAuditLogEventDocumentFieldInsertedSchema,
ZDocumentAuditLogEventDocumentFieldUninsertedSchema, ZDocumentAuditLogEventDocumentFieldUninsertedSchema,

View File

@ -385,6 +385,10 @@ export const formatDocumentAuditLogAction = (
anonymous: msg`Document completed`, anonymous: msg`Document completed`,
identified: msg`Document completed`, identified: msg`Document completed`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SIGNING_LINK_COPIED }, () => ({
anonymous: msg`Document signing link copied`,
identified: msg`${prefix} copied the document signing link`,
}))
.exhaustive(); .exhaustive();
return { return {

View File

@ -25,10 +25,13 @@ import { sendDocument } from '@documenso/lib/server-only/document/send-document'
import { updateDocument } from '@documenso/lib/server-only/document/update-document'; import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { symmetricEncrypt } from '@documenso/lib/universal/crypto'; import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import { DocumentDataType, DocumentStatus } from '@documenso/prisma/client'; import { DocumentDataType, DocumentStatus } from '@documenso/prisma/client';
import { authenticatedProcedure, procedure, router } from '../trpc'; import { authenticatedProcedure, procedure, router } from '../trpc';
import { import {
ZCreateAuditLogMutationSchema,
ZCreateDocumentRequestSchema, ZCreateDocumentRequestSchema,
ZCreateDocumentV2RequestSchema, ZCreateDocumentV2RequestSchema,
ZCreateDocumentV2ResponseSchema, ZCreateDocumentV2ResponseSchema,
@ -626,4 +629,37 @@ export const documentRouter = router({
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`, url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
}; };
}), }),
/**
* @private
*/
createAuditLog: authenticatedProcedure
.input(ZCreateAuditLogMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, type, data } = input;
const document = await getDocumentById({
documentId,
userId: ctx.user.id,
teamId,
}).catch(() => null);
if (!document || (teamId && document.teamId !== teamId)) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You do not have access to this document.',
});
}
return await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({
type,
data,
documentId,
user: ctx.user,
metadata: ctx.metadata,
}),
});
}),
}); });

View File

@ -326,6 +326,18 @@ export const ZDownloadCertificateMutationSchema = z.object({
documentId: z.number(), documentId: z.number(),
}); });
export const ZCreateAuditLogMutationSchema = z.object({
documentId: z.number(),
type: z.literal('DOCUMENT_SIGNING_LINK_COPIED'),
data: z.object({
recipientEmail: z.string(),
recipientName: z.string(),
recipientId: z.number(),
recipientRole: z.string(),
isBulkCopy: z.boolean(),
}),
});
export const ZMoveDocumentToTeamSchema = z.object({ export const ZMoveDocumentToTeamSchema = z.object({
documentId: z.number().describe('The ID of the document to move to a team.'), documentId: z.number().describe('The ID of the document to move to a team.'),
teamId: z.number().describe('The ID of the team to move the document to.'), teamId: z.number().describe('The ID of the team to move the document to.'),

View File

@ -16,6 +16,7 @@ import {
DocumentStatus, DocumentStatus,
RecipientRole, RecipientRole,
} from '@documenso/prisma/client'; } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper'; import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs'; import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
@ -58,6 +59,8 @@ export const AddSubjectFormPartial = ({
}: AddSubjectFormProps) => { }: AddSubjectFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { mutateAsync: createAuditLog } = trpc.document.createAuditLog.useMutation();
const { const {
register, register,
handleSubmit, handleSubmit,
@ -98,6 +101,25 @@ export const AddSubjectFormPartial = ({
const onFormSubmit = handleSubmit(onSubmit); const onFormSubmit = handleSubmit(onSubmit);
const { currentStep, totalSteps, previousStep } = useStep(); const { currentStep, totalSteps, previousStep } = useStep();
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 ( return (
<> <>
<DocumentFlowFormContainerHeader <DocumentFlowFormContainerHeader
@ -236,14 +258,7 @@ export const AddSubjectFormPartial = ({
{recipient.role !== RecipientRole.CC && ( {recipient.role !== RecipientRole.CC && (
<CopyTextButton <CopyTextButton
value={formatSigningLink(recipient.token)} value={formatSigningLink(recipient.token)}
onCopySuccess={() => { onCopySuccess={() => void onCopyLink(recipient)}
toast({
title: _(msg`Copied to clipboard`),
description: _(
msg`The signing link has been copied to your clipboard.`,
),
});
}}
badgeContentUncopied={ badgeContentUncopied={
<p className="ml-1 text-xs"> <p className="ml-1 text-xs">
<Trans>Copy</Trans> <Trans>Copy</Trans>