feat: per-recipient envelope expiration (#2519)

This commit is contained in:
Lucas Smith
2026-02-20 11:36:20 +11:00
committed by GitHub
parent f3ec8ddc57
commit 006b1d0a57
70 changed files with 2705 additions and 93 deletions
@@ -25,6 +25,7 @@ import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settin
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { isRecipientExpired } from '@documenso/lib/utils/recipients';
import { prisma } from '@documenso/prisma';
import { SigningCard3D } from '@documenso/ui/components/signing-card';
@@ -140,6 +141,10 @@ const handleV1Loader = async ({ params, request }: Route.LoaderArgs) => {
throw redirect(`/sign/${token}/rejected`);
}
if (isRecipientExpired(recipient)) {
throw redirect(`/sign/${token}/expired`);
}
if (
document.status === DocumentStatus.COMPLETED ||
recipient.signingStatus === SigningStatus.SIGNED
@@ -201,7 +206,8 @@ const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
return envelopeForSigning;
}
const { envelope, recipient, isCompleted, isRejected, isRecipientsTurn } = envelopeForSigning;
const { envelope, recipient, isCompleted, isRejected, isExpired, isRecipientsTurn } =
envelopeForSigning;
if (!isRecipientsTurn) {
throw redirect(`/sign/${token}/waiting`);
@@ -233,12 +239,6 @@ const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
} as const;
}
await viewedDocument({
token,
requestMetadata,
recipientAccessAuth: derivedRecipientAccessAuth,
}).catch(() => null);
if (isRejected) {
throw redirect(`/sign/${token}/rejected`);
}
@@ -247,6 +247,16 @@ const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
throw redirect(envelope.documentMeta.redirectUrl || `/sign/${token}/complete`);
}
if (isExpired) {
throw redirect(`/sign/${token}/expired`);
}
await viewedDocument({
token,
requestMetadata,
recipientAccessAuth: derivedRecipientAccessAuth,
}).catch(() => null);
return {
isDocumentAccessValid: true,
envelopeForSigning,
@@ -0,0 +1,114 @@
import { Trans } from '@lingui/react/macro';
import { TimerOffIcon } from 'lucide-react';
import { Link } from 'react-router';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { useOptionalSession } from '@documenso/lib/client-only/providers/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 { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page';
import { truncateTitle } from '~/utils/truncate-title';
import type { Route } from './+types/expired';
export async function loader({ params, request }: Route.LoaderArgs) {
const { user } = await getOptionalSession(request);
const { token } = params;
if (!token) {
throw new Response('Not Found', { status: 404 });
}
const document = await getDocumentAndSenderByToken({
token,
requireAccessAuth: false,
}).catch(() => null);
if (!document) {
throw new Response('Not Found', { status: 404 });
}
const title = document.title;
const recipient = await getRecipientByToken({ token }).catch(() => null);
if (!recipient) {
throw new Response('Not Found', { status: 404 });
}
const isDocumentAccessValid = await isRecipientAuthorized({
type: 'ACCESS',
documentAuthOptions: document.authOptions,
recipient,
userId: user?.id,
});
const recipientEmail = recipient.email;
if (isDocumentAccessValid) {
return {
isDocumentAccessValid: true,
recipientEmail,
title,
};
}
return {
isDocumentAccessValid: false,
recipientEmail,
};
}
export default function ExpiredSigningPage({ loaderData }: Route.ComponentProps) {
const { sessionData } = useOptionalSession();
const user = sessionData?.user;
const { isDocumentAccessValid, recipientEmail, title } = loaderData;
if (!isDocumentAccessValid) {
return <DocumentSigningAuthPageView email={recipientEmail} />;
}
return (
<div className="flex flex-col items-center pt-24 lg:pt-36 xl:pt-44">
<Badge
variant="neutral"
size="default"
title={title}
className="mb-6 rounded-xl border bg-transparent"
>
{truncateTitle(title ?? '')}
</Badge>
<div className="flex flex-col items-center">
<div className="flex items-center gap-x-4">
<TimerOffIcon className="h-10 w-10 text-orange-500" />
<h2 className="max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
<Trans>Signing Deadline Expired</Trans>
</h2>
</div>
<p className="mt-6 max-w-[60ch] text-center text-sm text-muted-foreground">
<Trans>
The signing deadline for this document has passed. Please contact the document owner if
you need a new copy to sign.
</Trans>
</p>
{user && (
<Button className="mt-6" asChild>
<Link to={`/`}>
<Trans>Return Home</Trans>
</Link>
</Button>
)}
</div>
</div>
);
}