feat: signing order (#1290)

Adds the ability to specify an optional signing order for documents.
When specified a document will be considered sequential with recipients
only being allowed to sign in the order that they were specified in.
This commit is contained in:
Ephraim Duncan
2024-09-16 12:36:45 +00:00
committed by GitHub
parent 357bdd374f
commit 3d644db286
66 changed files with 1999 additions and 606 deletions

View File

@ -85,6 +85,20 @@ export const EditDocumentForm = ({
},
});
const { mutateAsync: setSigningOrderForDocument } =
trpc.document.setSigningOrderForDocument.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
{
id: initialDocument.id,
teamId: team?.id,
},
(oldData) => ({ ...(oldData || initialDocument), ...newData, id: Number(newData.id) }),
);
},
});
const { mutateAsync: addFields } = trpc.field.addFields.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newFields) => {
@ -204,15 +218,22 @@ export const EditDocumentForm = ({
const onAddSignersFormSubmit = async (data: TAddSignersFormSchema) => {
try {
await addSigners({
documentId: document.id,
teamId: team?.id,
signers: data.signers.map((signer) => ({
...signer,
// Explicitly set to null to indicate we want to remove auth if required.
actionAuth: signer.actionAuth || null,
})),
});
await Promise.all([
setSigningOrderForDocument({
documentId: document.id,
signingOrder: data.signingOrder,
}),
addSigners({
documentId: document.id,
teamId: team?.id,
signers: data.signers.map((signer) => ({
...signer,
// Explicitly set to null to indicate we want to remove auth if required.
actionAuth: signer.actionAuth || null,
})),
}),
]);
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
@ -350,6 +371,7 @@ export const EditDocumentForm = ({
key={recipients.length}
documentFlow={documentFlow.signers}
recipients={recipients}
signingOrder={document.documentMeta?.signingOrder}
fields={fields}
isDocumentEnterprise={isDocumentEnterprise}
onSubmit={onAddSignersFormSubmit}

View File

@ -103,6 +103,19 @@ export const EditTemplateForm = ({
},
});
const { mutateAsync: setSigningOrderForTemplate } =
trpc.template.setSigningOrderForTemplate.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateWithDetailsById.setData(
{
id: initialTemplate.id,
},
(oldData) => ({ ...(oldData || initialTemplate), ...newData }),
);
},
});
const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
@ -160,11 +173,19 @@ export const EditTemplateForm = ({
data: TAddTemplatePlacholderRecipientsFormSchema,
) => {
try {
await addTemplateSigners({
templateId: template.id,
teamId: team?.id,
signers: data.signers,
});
await Promise.all([
setSigningOrderForTemplate({
templateId: template.id,
teamId: team?.id,
signingOrder: data.signingOrder,
}),
addTemplateSigners({
templateId: template.id,
teamId: team?.id,
signers: data.signers,
}),
]);
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
@ -262,6 +283,7 @@ export const EditTemplateForm = ({
documentFlow={documentFlow.signers}
recipients={recipients}
fields={fields}
signingOrder={template.templateMeta?.signingOrder}
templateDirectLink={template.directLink}
onSubmit={onAddTemplatePlaceholderFormSubmit}
isEnterprise={isEnterprise}

View File

@ -29,9 +29,16 @@ export type SigningFormProps = {
recipient: Recipient;
fields: Field[];
redirectUrl?: string | null;
isRecipientsTurn: boolean;
};
export const SigningForm = ({ document, recipient, fields, redirectUrl }: SigningFormProps) => {
export const SigningForm = ({
document,
recipient,
fields,
redirectUrl,
isRecipientsTurn,
}: SigningFormProps) => {
const router = useRouter();
const analytics = useAnalytics();
const { data: session } = useSession();
@ -150,6 +157,7 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
fields={fields}
fieldsValidated={fieldsValidated}
role={recipient.role}
disabled={!isRecipientsTurn}
/>
</div>
</div>
@ -213,6 +221,7 @@ export const SigningForm = ({ document, recipient, fields, redirectUrl }: Signin
fields={fields}
fieldsValidated={fieldsValidated}
role={recipient.role}
disabled={!isRecipientsTurn}
/>
</div>
</div>

View File

@ -9,6 +9,7 @@ import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-re
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
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 { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email';
@ -42,6 +43,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
if (!isRecipientsTurn) {
return redirect(`/sign/${token}/waiting`);
}
const [document, fields, recipient, completedFields] = await Promise.all([
getDocumentAndSenderByToken({
token,
@ -146,6 +153,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
document={document}
fields={fields}
completedFields={completedFields}
isRecipientsTurn={isRecipientsTurn}
/>
</DocumentAuthProvider>
</SigningProvider>

View File

@ -23,6 +23,7 @@ export type SignDialogProps = {
fieldsValidated: () => void | Promise<void>;
onSignatureComplete: () => void | Promise<void>;
role: RecipientRole;
disabled?: boolean;
};
export const SignDialog = ({
@ -32,6 +33,7 @@ export const SignDialog = ({
fieldsValidated,
onSignatureComplete,
role,
disabled = false,
}: SignDialogProps) => {
const [showDialog, setShowDialog] = useState(false);
const truncatedTitle = truncateTitle(documentTitle);
@ -54,6 +56,7 @@ export const SignDialog = ({
size="lg"
onClick={fieldsValidated}
loading={isSubmitting}
disabled={disabled}
>
{isComplete ? <Trans>Complete</Trans> : <Trans>Next field</Trans>}
</Button>

View File

@ -39,6 +39,7 @@ export type SigningPageViewProps = {
recipient: Recipient;
fields: Field[];
completedFields: CompletedField[];
isRecipientsTurn: boolean;
};
export const SigningPageView = ({
@ -46,6 +47,7 @@ export const SigningPageView = ({
recipient,
fields,
completedFields,
isRecipientsTurn,
}: SigningPageViewProps) => {
const { documentData, documentMeta } = document;
@ -99,6 +101,7 @@ export const SigningPageView = ({
recipient={recipient}
fields={fields}
redirectUrl={documentMeta?.redirectUrl}
isRecipientsTurn={isRecipientsTurn}
/>
</div>
</div>

View File

@ -0,0 +1,100 @@
import Link from 'next/link';
import { notFound, redirect } from 'next/navigation';
import { Trans } from '@lingui/macro';
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Team } from '@documenso/prisma/client';
import { DocumentStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
type WaitingForTurnToSignPageProps = {
params: { token?: string };
};
export default async function WaitingForTurnToSignPage({
params: { token },
}: WaitingForTurnToSignPageProps) {
setupI18nSSR();
if (!token) {
return notFound();
}
const { user } = await getServerComponentSession();
const [document, recipient] = await Promise.all([
getDocumentAndSenderByToken({ token }).catch(() => null),
getRecipientByToken({ token }).catch(() => null),
]);
if (!document || !recipient) {
return notFound();
}
if (document.status === DocumentStatus.COMPLETED) {
return redirect(`/sign/${token}/complete`);
}
let isOwnerOrTeamMember = false;
let team: Team | null = null;
if (user) {
isOwnerOrTeamMember = await getDocumentById({
id: document.id,
userId: user.id,
teamId: document.teamId ?? undefined,
})
.then((document) => !!document)
.catch(() => false);
if (document.teamId) {
team = await getTeamById({
userId: user.id,
teamId: document.teamId,
});
}
}
return (
<div className="relative flex flex-col items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
<div className="w-full max-w-md text-center">
<h2 className="tracking-tigh text-3xl font-bold">
<Trans>Waiting for Your Turn</Trans>
</h2>
<p className="text-muted-foreground mt-2 text-sm">
<Trans>
It's currently not your turn to sign. You will receive an email with instructions once
it's your turn to sign the document.
</Trans>
</p>
<p className="text-muted-foreground mt-4 text-sm">
<Trans>Please check your email for updates.</Trans>
</p>
<div className="mt-4">
{isOwnerOrTeamMember ? (
<Button variant="link" asChild>
<Link href={`${formatDocumentsPath(team?.url)}/${document.id}`}>
<Trans>Were you trying to edit this document instead?</Trans>
</Link>
</Button>
) : (
<Button variant="link" asChild>
<Link href="/documents">Return Home</Link>
</Button>
)}
</div>
</div>
</div>
);
}

View File

@ -1,5 +1,8 @@
import { z } from 'zod';
export const ZBaseEmbedDataSchema = z.object({
css: z.string().optional().transform(value => value || undefined),
css: z
.string()
.optional()
.transform((value) => value || undefined),
});

View File

@ -18,15 +18,18 @@ export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentComplet
<div className="mt-8 w-full max-w-md">
<SigningCard3D
className='w-full mx-auto'
className="mx-auto w-full"
name={name || 'Documenso'}
signature={signature}
signingCelebrationImage={signingCelebration}
/>
</div>
<p className="mt-8 max-w-[50ch] text-center text-muted-foreground text-sm">
<Trans>The document is now completed, please follow any instructions provided within the parent application.</Trans>
<p className="text-muted-foreground mt-8 max-w-[50ch] text-center text-sm">
<Trans>
The document is now completed, please follow any instructions provided within the parent
application.
</Trans>
</p>
</div>
);

View File

@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { DateTime } from 'luxon';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
@ -14,7 +15,7 @@ import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import { FieldType, type DocumentData, type Field } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import type {
TRemovedSignedFieldWithTokenMutationSchema,
@ -34,7 +35,6 @@ import type { DirectTemplateLocalField } from '~/app/(recipient)/d/[token]/sign-
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
import { Logo } from '~/components/branding/logo';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { EmbedClientLoading } from '../../client-loading';
import { EmbedDocumentCompleted } from '../../completed';
import { EmbedDocumentFields } from '../../document-fields';
@ -307,7 +307,7 @@ export const EmbedDirectTemplateClientPage = ({
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
<div className="relative flex flex-col md:flex-row w-full gap-x-6 gap-y-12">
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="flex-1">
<LazyPDFViewer
@ -318,26 +318,26 @@ export const EmbedDirectTemplateClientPage = ({
{/* Widget */}
<div
className="group/document-widget fixed md:sticky md:top-4 left-0 w-full bottom-8 px-6 md:px-0 z-50 md:z-auto md:w-[350px] flex-shrink-0 h-fit"
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="w-full border-border bg-widget flex md:min-h-[min(calc(100dvh-2rem),48rem)] h-fit flex-col rounded-xl border px-4 py-4 md:py-6">
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
{/* Header */}
<div>
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl md:text-2xl font-semibold">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
<Trans>Sign document</Trans>
</h3>
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
{isExpanded ? (
<LucideChevronDown
className="h-5 w-5 text-muted-foreground"
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(false)}
/>
) : (
<LucideChevronUp
className="h-5 w-5 text-muted-foreground"
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(true)}
/>
)}
@ -354,7 +354,7 @@ export const EmbedDirectTemplateClientPage = ({
</div>
{/* Form */}
<div className="-mx-2 px-2 hidden group-data-[expanded]/document-widget:block md:block">
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">
@ -408,9 +408,9 @@ export const EmbedDirectTemplateClientPage = ({
</div>
</div>
<div className="flex-1 hidden group-data-[expanded]/document-widget:block md:block" />
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
<div className="w-full grid-cols-2 items-center mt-4 hidden group-data-[expanded]/document-widget:grid md:grid">
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
{pendingFields.length > 0 ? (
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
<Trans>Next</Trans>

View File

@ -1,3 +1,3 @@
export default function EmbedDirectTemplateNotFound() {
return <div>Not Found</div>
return <div>Not Found</div>;
}

View File

@ -73,11 +73,7 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
const fields = template.Field.filter((field) => field.recipientId === directTemplateRecipientId);
return (
<SigningProvider
email={user?.email}
fullName={user?.name}
signature={user?.signature}
>
<SigningProvider email={user?.email} fullName={user?.name} signature={user?.signature}>
<DocumentAuthProvider
documentAuthOptions={template.authOptions}
recipient={recipient}

View File

@ -13,7 +13,7 @@ import {
ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import { FieldType, type Field } from '@documenso/prisma/client';
import { type Field, FieldType } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import type {
TRemovedSignedFieldWithTokenMutationSchema,

View File

@ -1,5 +1,7 @@
export const EmbedPaywall = () => {
return <div>
<h1>Paywall</h1>
</div>
}
return (
<div>
<h1>Paywall</h1>
</div>
);
};

View File

@ -1,15 +1,17 @@
'use client';
import { useEffect, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useEffect, useState } from 'react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -20,9 +22,9 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
import { Logo } from '~/components/branding/logo';
import { EmbedClientLoading } from '../../client-loading';
import { EmbedDocumentCompleted } from '../../completed';
import { EmbedDocumentFields } from '../../document-fields';
@ -185,7 +187,7 @@ export const EmbedSignDocumentClientPage = ({
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
{(!hasFinishedInit || !hasDocumentLoaded) && <EmbedClientLoading />}
<div className="relative flex flex-col md:flex-row w-full gap-x-6 gap-y-12">
<div className="relative flex w-full flex-col gap-x-6 gap-y-12 md:flex-row">
{/* Viewer */}
<div className="flex-1">
<LazyPDFViewer
@ -196,26 +198,26 @@ export const EmbedSignDocumentClientPage = ({
{/* Widget */}
<div
className="group/document-widget fixed md:sticky md:top-4 left-0 w-full bottom-8 px-6 md:px-0 z-50 md:z-auto md:w-[350px] flex-shrink-0 h-fit"
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined}
>
<div className="w-full border-border bg-widget flex flex-col rounded-xl border px-4 py-4 md:py-6">
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
{/* Header */}
<div>
<div className="flex items-center justify-between gap-x-2">
<h3 className="text-foreground text-xl md:text-2xl font-semibold">
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
<Trans>Sign document</Trans>
</h3>
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
{isExpanded ? (
<LucideChevronDown
className="h-5 w-5 text-muted-foreground"
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(false)}
/>
) : (
<LucideChevronUp
className="h-5 w-5 text-muted-foreground"
className="text-muted-foreground h-5 w-5"
onClick={() => setIsExpanded(true)}
/>
)}
@ -232,7 +234,7 @@ export const EmbedSignDocumentClientPage = ({
</div>
{/* Form */}
<div className="-mx-2 px-2 hidden group-data-[expanded]/document-widget:block md:block">
<div className="-mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
<div className="flex flex-1 flex-col gap-y-4">
<div>
<Label htmlFor="full-name">
@ -285,9 +287,9 @@ export const EmbedSignDocumentClientPage = ({
</div>
</div>
<div className="flex-1 hidden group-data-[expanded]/document-widget:block md:block" />
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
<div className="w-full grid-cols-2 items-center mt-4 hidden group-data-[expanded]/document-widget:grid md:grid">
<div className="mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
{pendingFields.length > 0 ? (
<Button className="col-start-2" onClick={() => onNextFieldClick()}>
<Trans>Next</Trans>

View File

@ -1,3 +1,3 @@
export default function EmbedDirectTemplateNotFound() {
return <div>Not Found</div>
return <div>Not Found</div>;
}

View File

@ -4,8 +4,12 @@ import { match } from 'ts-pattern';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
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 { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentStatus } from '@documenso/prisma/client';
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
@ -13,10 +17,6 @@ import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
import { EmbedAuthenticateView } from '../../authenticate';
import { EmbedPaywall } from '../../paywall';
import { EmbedSignDocumentClientPage } from './client';
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 { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { DocumentStatus } from '@documenso/prisma/client';
export type EmbedSignDocumentPageProps = {
params: {
@ -66,7 +66,12 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
.exhaustive();
if (!isAccessAuthValid) {
return <EmbedAuthenticateView email={user?.email || recipient.email} returnTo={`/embed/direct/${token}`} />;
return (
<EmbedAuthenticateView
email={user?.email || recipient.email}
returnTo={`/embed/direct/${token}`}
/>
);
}
return (

View File

@ -81,7 +81,7 @@ async function middleware(req: NextRequest): Promise<NextResponse> {
// Allow third parties to iframe the document.
res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.headers.set('Access-Control-Allow-Origin', '*');
res.headers.set('Content-Security-Policy', "frame-ancestors *");
res.headers.set('Content-Security-Policy', 'frame-ancestors *');
res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
res.headers.set('X-Content-Type-Options', 'nosniff');
res.headers.set('X-Frame-Options', 'ALLOW-ALL');