From 316dbee446bac616d94369ee411649f70c053f8a Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Sun, 17 Nov 2024 12:12:27 +0000 Subject: [PATCH] feat: prevent signing when expired --- .../(signing)/sign/[token]/expired/page.tsx | 99 +++++++++++++++++++ .../src/app/(signing)/sign/[token]/page.tsx | 17 ++-- .../(signing)/sign/[token]/rejected/page.tsx | 12 +-- .../recipient/is-recipient-expired.ts | 34 +++++++ .../document-flow/signer-action-dropdown.tsx | 1 + 5 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 apps/web/src/app/(signing)/sign/[token]/expired/page.tsx create mode 100644 packages/lib/server-only/recipient/is-recipient-expired.ts diff --git a/apps/web/src/app/(signing)/sign/[token]/expired/page.tsx b/apps/web/src/app/(signing)/sign/[token]/expired/page.tsx new file mode 100644 index 000000000..fc74507b0 --- /dev/null +++ b/apps/web/src/app/(signing)/sign/[token]/expired/page.tsx @@ -0,0 +1,99 @@ +import Link from 'next/link'; +import { notFound } from 'next/navigation'; + +import { Trans } from '@lingui/macro'; +import { Clock } from 'lucide-react'; + +import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; +import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; +import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; +import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized'; +import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; +import { Badge } from '@documenso/ui/primitives/badge'; +import { Button } from '@documenso/ui/primitives/button'; + +import { truncateTitle } from '~/helpers/truncate-title'; + +import { SigningAuthPageView } from '../signing-auth-page'; + +export type ExpiredSigningPageProps = { + params: { + token?: string; + }; +}; + +export default async function ExpiredSigningPage({ params: { token } }: ExpiredSigningPageProps) { + await setupI18nSSR(); + + if (!token) { + return notFound(); + } + + const { user } = await getServerComponentSession(); + + const document = await getDocumentAndSenderByToken({ + token, + requireAccessAuth: false, + }).catch(() => null); + + if (!document) { + return notFound(); + } + + const truncatedTitle = truncateTitle(document.title); + + const recipient = await getRecipientByToken({ token }).catch(() => null); + + if (!recipient) { + return notFound(); + } + + const isDocumentAccessValid = await isRecipientAuthorized({ + type: 'ACCESS', + documentAuthOptions: document.authOptions, + recipient, + userId: user?.id, + }); + + if (!isDocumentAccessValid) { + return ; + } + + return ( +
+ + {truncatedTitle} + + +
+
+ +

+ Document Expired +

+
+ +
+ This document has expired and is no longer available to sign +
+ +

+ + {/* TODO: send email to owner when a user tried to sign an expired document??? */} + The document owner has been notified. They may send you a new signing link if required. + +

+ +

+ No further action is required from you at this time. +

+ + {user && ( + + )} +
+
+ ); +} diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index ec32082db..a5d008e80 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -12,6 +12,7 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures'; +import { isRecipientExpired } from '@documenso/lib/server-only/recipient/is-recipient-expired'; import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email'; import { symmetricDecrypt } from '@documenso/lib/universal/crypto'; import { extractNextHeaderRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; @@ -43,6 +44,16 @@ export default async function SigningPage({ params: { token } }: SigningPageProp const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders); + const isExpired = await isRecipientExpired({ token }); + if (isExpired) { + return redirect(`/sign/${token}/expired`); + } + + const isRecipientsTurn = await getIsRecipientsTurnToSign({ token }); + if (!isRecipientsTurn) { + return redirect(`/sign/${token}/waiting`); + } + const [document, fields, recipient, completedFields] = await Promise.all([ getDocumentAndSenderByToken({ token, @@ -63,12 +74,6 @@ export default async function SigningPage({ params: { token } }: SigningPageProp return notFound(); } - const isRecipientsTurn = await getIsRecipientsTurnToSign({ token }); - - if (!isRecipientsTurn) { - return redirect(`/sign/${token}/waiting`); - } - const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({ documentAuth: document.authOptions, recipientAuth: recipient.authOptions, diff --git a/apps/web/src/app/(signing)/sign/[token]/rejected/page.tsx b/apps/web/src/app/(signing)/sign/[token]/rejected/page.tsx index e3e630d8c..45f06c228 100644 --- a/apps/web/src/app/(signing)/sign/[token]/rejected/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/rejected/page.tsx @@ -8,9 +8,7 @@ import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server'; import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized'; -import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; -import { FieldType } from '@documenso/prisma/client'; import { Badge } from '@documenso/ui/primitives/badge'; import { Button } from '@documenso/ui/primitives/button'; @@ -44,10 +42,7 @@ export default async function RejectedSigningPage({ params: { token } }: Rejecte const truncatedTitle = truncateTitle(document.title); - const [fields, recipient] = await Promise.all([ - getFieldsForToken({ token }), - getRecipientByToken({ token }).catch(() => null), - ]); + const recipient = await getRecipientByToken({ token }).catch(() => null); if (!recipient) { return notFound(); @@ -64,11 +59,6 @@ export default async function RejectedSigningPage({ params: { token } }: Rejecte return ; } - const recipientName = - recipient.name || - fields.find((field) => field.type === FieldType.NAME)?.customText || - recipient.email; - return (
diff --git a/packages/lib/server-only/recipient/is-recipient-expired.ts b/packages/lib/server-only/recipient/is-recipient-expired.ts new file mode 100644 index 000000000..617a72a83 --- /dev/null +++ b/packages/lib/server-only/recipient/is-recipient-expired.ts @@ -0,0 +1,34 @@ +import { prisma } from '@documenso/prisma'; +import { SigningStatus } from '@documenso/prisma/client'; + +export type IsRecipientExpiredOptions = { + token: string; +}; + +export const isRecipientExpired = async ({ token }: IsRecipientExpiredOptions) => { + const recipient = await prisma.recipient.findFirst({ + where: { + token, + }, + }); + + if (!recipient) { + throw new Error('Recipient not found'); + } + + const now = new Date(); + const hasExpired = recipient.expired && new Date(recipient.expired) <= now; + + if (hasExpired && recipient.signingStatus !== SigningStatus.EXPIRED) { + await prisma.recipient.update({ + where: { + id: recipient.id, + }, + data: { + signingStatus: SigningStatus.EXPIRED, + }, + }); + } + + return hasExpired; +}; diff --git a/packages/ui/primitives/document-flow/signer-action-dropdown.tsx b/packages/ui/primitives/document-flow/signer-action-dropdown.tsx index a8a3efd90..5d5b79f3b 100644 --- a/packages/ui/primitives/document-flow/signer-action-dropdown.tsx +++ b/packages/ui/primitives/document-flow/signer-action-dropdown.tsx @@ -53,6 +53,7 @@ export function SignerActionDropdown({
+