mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 17:35:05 +10:00
chore: merged main
This commit is contained in:
@@ -13,7 +13,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import { useRequiredDocumentSigningAuthContext } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||
import { useRequiredDocumentSigningContext } from '~/components/general/document-signing/document-signing-provider';
|
||||
@@ -37,7 +37,6 @@ export const DirectTemplatePageView = ({
|
||||
directTemplateRecipient,
|
||||
directTemplateToken,
|
||||
}: DirectTemplatePageViewProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { _ } = useLingui();
|
||||
@@ -119,7 +118,7 @@ export const DirectTemplatePageView = ({
|
||||
if (redirectUrl) {
|
||||
window.location.href = redirectUrl;
|
||||
} else {
|
||||
await navigate(`/sign/${token}/complete`);
|
||||
window.location.href = `/sign/${token}/complete`;
|
||||
}
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { AlertTriangleIcon } from 'lucide-react';
|
||||
|
||||
export type CscRecipientBlockedPageProps = {
|
||||
code: string;
|
||||
recipientToken: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Terminal page rendered when the service-scope CSC OAuth callback surfaces a
|
||||
* hard error the recipient can't resolve themselves (empty credential list,
|
||||
* invalid cert, refused algorithm). The blocking-error cookie is read +
|
||||
* cleared by the loader; this page only renders the message + retry CTA.
|
||||
*
|
||||
* The retry link kicks a fresh service-scope OAuth round-trip — useful when
|
||||
* the TSP-side issue is transient (e.g. the recipient's admin has since
|
||||
* provisioned a credential).
|
||||
*/
|
||||
export const CscRecipientBlockedPage = ({ code, recipientToken }: CscRecipientBlockedPageProps) => {
|
||||
const retryUrl = `/api/csc/oauth/authorize?scope=service&token=${encodeURIComponent(recipientToken)}`;
|
||||
|
||||
return (
|
||||
<div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-16 md:-mx-8 md:px-8 lg:pt-16 xl:pt-24">
|
||||
<AlertTriangleIcon className="h-12 w-12 text-destructive" />
|
||||
|
||||
<h2 className="mt-6 max-w-[35ch] text-center font-semibold text-2xl leading-normal md:text-3xl lg:text-4xl">
|
||||
{code === AppErrorCode.CSC_CREDENTIAL_LIST_EMPTY ? (
|
||||
<Trans>No signing credentials available</Trans>
|
||||
) : code === AppErrorCode.CSC_CERT_INVALID ? (
|
||||
<Trans>Signing certificate is invalid</Trans>
|
||||
) : code === AppErrorCode.CSC_ALGORITHM_REFUSED ? (
|
||||
<Trans>Signing algorithm is not supported</Trans>
|
||||
) : (
|
||||
<Trans>Unable to start the signing flow</Trans>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<p className="mt-2.5 max-w-[60ch] text-center font-medium text-muted-foreground/60 text-sm md:text-base">
|
||||
{code === AppErrorCode.CSC_CREDENTIAL_LIST_EMPTY ? (
|
||||
<Trans>
|
||||
Your signing provider returned no usable credentials for this account. Contact your administrator or signing
|
||||
provider for assistance.
|
||||
</Trans>
|
||||
) : code === AppErrorCode.CSC_CERT_INVALID ? (
|
||||
<Trans>
|
||||
Your signing certificate is invalid, expired, or missing a required key. Contact your administrator or
|
||||
signing provider for assistance.
|
||||
</Trans>
|
||||
) : code === AppErrorCode.CSC_ALGORITHM_REFUSED ? (
|
||||
<Trans>
|
||||
Your signing provider does not advertise a signing algorithm this document accepts. Contact your
|
||||
administrator or signing provider for assistance.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>Something went wrong while preparing the remote signature. Please try again.</Trans>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Button asChild className="mt-8">
|
||||
<a href={retryUrl}>
|
||||
<Trans>Try again</Trans>
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { AlertTriangleIcon, Loader2Icon } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export type CscRecipientSigningInProgressPageProps = {
|
||||
sessionId: string;
|
||||
recipientToken: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Rendered when the credential-scope OAuth callback has attached a SAD to the
|
||||
* server-side `CscSession` and set the `csc_sad_session` cookie. The page
|
||||
* auto-fires `enterprise.csc.signEnvelope` on mount and navigates to the
|
||||
* completion page on success. On failure, it surfaces an error message and
|
||||
* a retry CTA pointing at a fresh credential-scope OAuth round-trip.
|
||||
*/
|
||||
export const CscRecipientSigningInProgressPage = ({
|
||||
sessionId,
|
||||
recipientToken,
|
||||
}: CscRecipientSigningInProgressPageProps) => {
|
||||
const { mutateAsync: signEnvelope } = trpc.enterprise.csc.signEnvelope.useMutation();
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Ref rather than state for the fire-once guard. Refs mutate synchronously,
|
||||
// so React StrictMode's double-invoke of the effect sees the updated value
|
||||
// on the second pass and short-circuits. A useState guard would still let
|
||||
// the second effect fire because the queued setState from the first run
|
||||
// hasn't been committed yet when the second one reads it — that double-fire
|
||||
// races two signEnvelope calls; whichever loses sees the SAD already
|
||||
// consumed and flashes "Signing failed" before the winning call's
|
||||
// navigation kicks in.
|
||||
const hasFiredRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasFiredRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasFiredRef.current = true;
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
await signEnvelope({ sessionId, recipientToken });
|
||||
|
||||
window.location.href = `/sign/${recipientToken}/complete`;
|
||||
} catch (err) {
|
||||
const parsed = AppError.parseError(err);
|
||||
setError(parsed.code || AppErrorCode.UNKNOWN_ERROR);
|
||||
}
|
||||
};
|
||||
|
||||
void run();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const retryUrl = `/api/csc/oauth/authorize?scope=credential&session=${encodeURIComponent(sessionId)}`;
|
||||
|
||||
return (
|
||||
<div className="-mx-4 flex max-w-[100vw] flex-col items-center overflow-x-hidden px-4 pt-16 md:-mx-8 md:px-8 lg:pt-16 xl:pt-24">
|
||||
{error ? (
|
||||
<>
|
||||
<AlertTriangleIcon className="h-12 w-12 text-destructive" />
|
||||
|
||||
<h2 className="mt-6 max-w-[35ch] text-center font-semibold text-2xl leading-normal md:text-3xl lg:text-4xl">
|
||||
<Trans>Signing failed</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="mt-2.5 max-w-[60ch] text-center font-medium text-muted-foreground/60 text-sm md:text-base">
|
||||
{error === AppErrorCode.CSC_TSP_TIMEOUT ? (
|
||||
<Trans>The signing provider did not respond in time. Please retry.</Trans>
|
||||
) : error === AppErrorCode.CSC_SAD_EXPIRED_PRE_SIGN ? (
|
||||
<Trans>
|
||||
Your signing authorisation expired before the signature could be applied. Please reauthorise to retry.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>Something went wrong while applying your signature. Please retry.</Trans>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<Button asChild className="mt-8">
|
||||
<a href={retryUrl}>
|
||||
<Trans>Reauthorise and retry</Trans>
|
||||
</a>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Loader2Icon className="h-12 w-12 animate-spin text-primary" />
|
||||
|
||||
<h2 className="mt-6 max-w-[35ch] text-center font-semibold text-2xl leading-normal md:text-3xl lg:text-4xl">
|
||||
<Trans>Applying your signature</Trans>
|
||||
</h2>
|
||||
|
||||
<p className="mt-2.5 max-w-[60ch] text-center font-medium text-muted-foreground/60 text-sm md:text-base">
|
||||
<Trans>Please don't close this tab. The signing provider is finalising your signature.</Trans>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
-3
@@ -27,7 +27,6 @@ import type { Field } from '@prisma/client';
|
||||
import { FieldType, RecipientRole } from '@prisma/client';
|
||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { match, P } from 'ts-pattern';
|
||||
|
||||
import { DocumentSigningAttachmentsPopover } from '~/components/general/document-signing/document-signing-attachments-popover';
|
||||
@@ -84,7 +83,6 @@ export const DocumentSigningPageViewV1 = ({
|
||||
? authUser.twoFactorEnabled && authUser.email === recipient.email
|
||||
: false;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
|
||||
@@ -129,7 +127,7 @@ export const DocumentSigningPageViewV1 = ({
|
||||
if (documentMeta?.redirectUrl) {
|
||||
window.location.href = documentMeta.redirectUrl;
|
||||
} else {
|
||||
await navigate(`/sign/${recipient.token}/complete`);
|
||||
window.location.href = `/sign/${recipient.token}/complete`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+2
-3
@@ -17,7 +17,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate, useSearchParams } from 'react-router';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
const ZRejectDocumentFormSchema = z.object({
|
||||
@@ -41,7 +41,6 @@ export function DocumentSigningRejectDialog({
|
||||
}: DocumentSigningRejectDialogProps) {
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -74,7 +73,7 @@ export function DocumentSigningRejectDialog({
|
||||
if (onRejected) {
|
||||
await onRejected(reason);
|
||||
} else {
|
||||
await navigate(`/sign/${token}/rejected`);
|
||||
window.location.href = `/sign/${token}/rejected`;
|
||||
}
|
||||
} catch (err) {
|
||||
toast({
|
||||
|
||||
@@ -38,7 +38,7 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
|
||||
})
|
||||
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
|
||||
<Button className="w-full" asChild>
|
||||
<Link to={`/sign/${recipient?.token}`}>
|
||||
<a href={`/sign/${recipient?.token}`}>
|
||||
{match(role)
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<>
|
||||
@@ -58,7 +58,7 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
|
||||
<Trans>View</Trans>
|
||||
</>
|
||||
))}
|
||||
</Link>
|
||||
</a>
|
||||
</Button>
|
||||
))
|
||||
.with({ isComplete: false }, () => (
|
||||
|
||||
+12
-3
@@ -89,7 +89,7 @@ export const EnvelopeSignerCompleteDialog = () => {
|
||||
recipientDetails?: { name: string; email: string },
|
||||
) => {
|
||||
try {
|
||||
await completeDocument({
|
||||
const result = await completeDocument({
|
||||
token: recipient.token,
|
||||
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||
accessAuthOptions,
|
||||
@@ -97,6 +97,15 @@ export const EnvelopeSignerCompleteDialog = () => {
|
||||
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
||||
});
|
||||
|
||||
// TSP envelopes can't be completed via the SES path; the mutation returns
|
||||
// a credential-scope OAuth URL the recipient must follow to acquire a SAD
|
||||
// before the sync sign mutation can run. Short-circuit here so the
|
||||
// analytics / completion handlers don't run with a still-unsigned doc.
|
||||
if (result.status === 'REDIRECT') {
|
||||
window.location.href = result.redirectUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
analytics.capture('App: Recipient has completed signing', {
|
||||
signerId: recipient.id,
|
||||
documentId: envelope.id,
|
||||
@@ -119,7 +128,7 @@ export const EnvelopeSignerCompleteDialog = () => {
|
||||
if (envelope.documentMeta.redirectUrl) {
|
||||
window.location.href = envelope.documentMeta.redirectUrl;
|
||||
} else {
|
||||
await navigate(`/sign/${recipient.token}/complete`);
|
||||
window.location.href = `/sign/${recipient.token}/complete`;
|
||||
}
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
@@ -197,7 +206,7 @@ export const EnvelopeSignerCompleteDialog = () => {
|
||||
if (redirectUrl) {
|
||||
window.location.href = redirectUrl;
|
||||
} else {
|
||||
await navigate(`/sign/${token}/complete`);
|
||||
window.location.href = `/sign/${token}/complete`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err', err);
|
||||
|
||||
@@ -66,7 +66,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
|
||||
))
|
||||
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
|
||||
<Button className="w-32" asChild>
|
||||
<Link to={`/sign/${recipient?.token}`}>
|
||||
<a href={`/sign/${recipient?.token}`}>
|
||||
{match(role)
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<>
|
||||
@@ -86,7 +86,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
|
||||
<Trans>View</Trans>
|
||||
</>
|
||||
))}
|
||||
</Link>
|
||||
</a>
|
||||
</Button>
|
||||
))
|
||||
.with({ isPending: true, isSigned: true }, () => (
|
||||
|
||||
@@ -105,7 +105,7 @@ export const DocumentsTableActionDropdown = ({ row, onMoveDocument }: DocumentsT
|
||||
recipient?.role !== RecipientRole.CC &&
|
||||
recipient?.role !== RecipientRole.ASSISTANT && (
|
||||
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
|
||||
<Link to={`/sign/${recipient?.token}`}>
|
||||
<a href={`/sign/${recipient?.token}`}>
|
||||
{recipient?.role === RecipientRole.VIEWER && (
|
||||
<>
|
||||
<EyeIcon className="mr-2 h-4 w-4" />
|
||||
@@ -126,7 +126,7 @@ export const DocumentsTableActionDropdown = ({ row, onMoveDocument }: DocumentsT
|
||||
<Trans>Approve</Trans>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { DocumentStatus as DocumentStatusEnum, RecipientRole, SigningStatus } fr
|
||||
import { CheckCircleIcon, DownloadIcon, EyeIcon, Loader, PencilIcon } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useMemo, useTransition } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { DocumentStatus } from '~/components/general/document/document-status';
|
||||
@@ -200,7 +200,7 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
|
||||
})
|
||||
.with({ isPending: true, isSigned: false }, () => (
|
||||
<Button className="w-32" asChild>
|
||||
<Link to={`/sign/${recipient?.token}`}>
|
||||
<a href={`/sign/${recipient?.token}`}>
|
||||
{match(role)
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<>
|
||||
@@ -220,7 +220,7 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
|
||||
<Trans>View</Trans>
|
||||
</>
|
||||
))}
|
||||
</Link>
|
||||
</a>
|
||||
</Button>
|
||||
))
|
||||
.with({ isPending: true, isSigned: true }, () => (
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import {
|
||||
buildClearCscBlockingErrorCookieHeader,
|
||||
readCscBlockingErrorFromRequest,
|
||||
} from '@documenso/ee/server-only/signing/csc/cookies/blocking-error-cookie';
|
||||
import { readCscSadSessionFromRequest } from '@documenso/ee/server-only/signing/csc/cookies/sad-session-cookie';
|
||||
import { readCscServiceSessionFromRequest } from '@documenso/ee/server-only/signing/csc/cookies/service-session-cookie';
|
||||
import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { IS_INSTANCE_CSC_MODE } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { loadRecipientBrandingByTeamId } from '@documenso/lib/server-only/branding/load-recipient-branding';
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
@@ -18,6 +25,7 @@ import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/
|
||||
import { getTeamSettings } from '@documenso/lib/server-only/team/get-team-settings';
|
||||
import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
|
||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||
import { isTspEnvelope } from '@documenso/lib/types/signature-level';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { isRecipientExpired } from '@documenso/lib/utils/recipients';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@@ -30,6 +38,8 @@ import { getOptionalLoaderContext } from 'server/utils/get-loader-session';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
|
||||
import { CscRecipientBlockedPage } from '~/components/general/document-signing/csc-recipient-blocked-page';
|
||||
import { CscRecipientSigningInProgressPage } from '~/components/general/document-signing/csc-recipient-signing-in-progress-page';
|
||||
import { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page';
|
||||
import { DocumentSigningAuthProvider } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||
import { DocumentSigningPageViewV1 } from '~/components/general/document-signing/document-signing-page-view-v1';
|
||||
@@ -257,6 +267,58 @@ const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
|
||||
recipientAccessAuth: derivedRecipientAccessAuth,
|
||||
}).catch(() => null);
|
||||
|
||||
// CSC / TSP routing. TSP envelopes have three terminal recipient-page
|
||||
// states beyond the normal signing UI:
|
||||
// 1. `blocked` — service-scope OAuth returned a hard error (set by the
|
||||
// callback as a one-shot `csc_blocking_error` cookie).
|
||||
// 2. `signing-in-progress` — credential-scope OAuth completed, SAD is
|
||||
// attached server-side, page auto-fires the sync sign mutation.
|
||||
// 3. pre-auth — no service token yet, kick the recipient into
|
||||
// service-scope OAuth.
|
||||
// The fourth state (service session valid, no SAD, no blocking error) falls
|
||||
// through to the normal signing UI.
|
||||
if (IS_INSTANCE_CSC_MODE() && isTspEnvelope(envelope)) {
|
||||
const blockingError = await readCscBlockingErrorFromRequest(request);
|
||||
|
||||
if (blockingError && blockingError.recipientToken === token) {
|
||||
return {
|
||||
isDocumentAccessValid: true,
|
||||
envelopeForSigning,
|
||||
csc: { state: 'blocked', code: blockingError.code } as const,
|
||||
responseHeaders: { 'Set-Cookie': buildClearCscBlockingErrorCookieHeader() },
|
||||
} as const;
|
||||
}
|
||||
|
||||
const sadSessionId = await readCscSadSessionFromRequest(request);
|
||||
|
||||
if (sadSessionId) {
|
||||
const cscSession = await prisma.cscSession.findUnique({
|
||||
where: { id: sadSessionId },
|
||||
});
|
||||
|
||||
const isSadSessionValid =
|
||||
cscSession !== null &&
|
||||
cscSession.recipientId === recipient.id &&
|
||||
cscSession.encryptedSad !== null &&
|
||||
cscSession.sadExpiresAt !== null &&
|
||||
cscSession.sadExpiresAt > new Date();
|
||||
|
||||
if (isSadSessionValid) {
|
||||
return {
|
||||
isDocumentAccessValid: true,
|
||||
envelopeForSigning,
|
||||
csc: { state: 'signing-in-progress', sessionId: sadSessionId } as const,
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
|
||||
const serviceSessionToken = await readCscServiceSessionFromRequest(request);
|
||||
|
||||
if (serviceSessionToken !== token) {
|
||||
throw redirect(`/api/csc/oauth/authorize?scope=service&token=${encodeURIComponent(token)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isDocumentAccessValid: true,
|
||||
envelopeForSigning,
|
||||
@@ -296,11 +358,22 @@ export async function loader(loaderArgs: Route.LoaderArgs) {
|
||||
if (foundRecipient.envelope.internalVersion === 2) {
|
||||
const payloadV2 = await handleV2Loader(loaderArgs);
|
||||
|
||||
return superLoaderJson({
|
||||
version: 2,
|
||||
payload: payloadV2,
|
||||
branding,
|
||||
} as const);
|
||||
// V2 payload may carry a one-shot `Set-Cookie` header (used to clear the
|
||||
// CSC blocking-error cookie after the loader reads it). Forward it via
|
||||
// the `superLoaderJson` response init so the browser actually applies the
|
||||
// header. The field stays on the payload — it's just a `Max-Age=0` clear
|
||||
// directive, not sensitive — and isn't read by any consumer.
|
||||
const responseHeaders =
|
||||
'responseHeaders' in payloadV2 && payloadV2.responseHeaders ? payloadV2.responseHeaders : undefined;
|
||||
|
||||
return superLoaderJson(
|
||||
{
|
||||
version: 2,
|
||||
payload: payloadV2,
|
||||
branding,
|
||||
} as const,
|
||||
responseHeaders ? { headers: responseHeaders } : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
const payloadV1 = await handleV1Loader(loaderArgs);
|
||||
@@ -430,6 +503,19 @@ const SigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV2Loade
|
||||
return <DocumentSigningAuthPageView email={data.recipientEmail} emailHasAccount={!!data.recipientHasAccount} />;
|
||||
}
|
||||
|
||||
if ('csc' in data && data.csc?.state === 'blocked') {
|
||||
return <CscRecipientBlockedPage code={data.csc.code} recipientToken={data.envelopeForSigning.recipient.token} />;
|
||||
}
|
||||
|
||||
if ('csc' in data && data.csc?.state === 'signing-in-progress') {
|
||||
return (
|
||||
<CscRecipientSigningInProgressPage
|
||||
sessionId={data.csc.sessionId}
|
||||
recipientToken={data.envelopeForSigning.recipient.token}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { envelope, recipientSignature, recipient } = data.envelopeForSigning;
|
||||
|
||||
if (envelope.deletedAt || envelope.status === DocumentStatus.REJECTED) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { tsRestHonoApp } from '@documenso/api/hono';
|
||||
import { auth } from '@documenso/auth/server';
|
||||
import { csc } from '@documenso/ee/server-only/signing/csc/hono';
|
||||
import { jobsClient } from '@documenso/lib/jobs/client';
|
||||
import { LicenseClient } from '@documenso/lib/server-only/license/license-client';
|
||||
import { createRateLimitMiddleware } from '@documenso/lib/server-only/rate-limit/rate-limit-middleware';
|
||||
@@ -111,6 +112,9 @@ app.route('/api/files', filesRoute);
|
||||
app.use('/api/ai/*', aiRateLimitMiddleware);
|
||||
app.route('/api/ai', aiRoute);
|
||||
|
||||
// CSC OAuth routes (mounted from @documenso/ee).
|
||||
app.route('/api/csc', csc);
|
||||
|
||||
// API servers.
|
||||
app.route('/api/v1', tsRestHonoApp);
|
||||
app.use('/api/jobs/*', jobsClient.getApiHandler());
|
||||
|
||||
Reference in New Issue
Block a user