Compare commits
19 Commits
v1.5.4-rc.
...
feat/add-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c0918963d | ||
|
|
47916e3127 | ||
|
|
62dd737cf0 | ||
|
|
0d510cf280 | ||
|
|
f88faa43d9 | ||
|
|
074adccae2 | ||
|
|
844261c35c | ||
|
|
c0fb5caf9c | ||
|
|
b6c4cc9dc8 | ||
|
|
94da57704d | ||
|
|
1375946bc5 | ||
|
|
dc6aba58e6 | ||
|
|
364b9e03e1 | ||
|
|
5f3152b7db | ||
|
|
dc8d8433dd | ||
|
|
6781ff137e | ||
|
|
fa9099bc86 | ||
|
|
228ac90036 | ||
|
|
8d1b0adbb2 |
@@ -14,7 +14,6 @@ import { LocaleDate } from '~/components/formatter/locale-date';
|
|||||||
|
|
||||||
import { AdminActions } from './admin-actions';
|
import { AdminActions } from './admin-actions';
|
||||||
import { RecipientItem } from './recipient-item';
|
import { RecipientItem } from './recipient-item';
|
||||||
import { SuperDeleteDocumentDialog } from './super-delete-document-dialog';
|
|
||||||
|
|
||||||
type AdminDocumentDetailsPageProps = {
|
type AdminDocumentDetailsPageProps = {
|
||||||
params: {
|
params: {
|
||||||
@@ -82,10 +81,6 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
|
|||||||
))}
|
))}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="my-4" />
|
|
||||||
|
|
||||||
{document && <SuperDeleteDocumentDialog document={document} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import type { Document } from '@documenso/prisma/client';
|
|
||||||
import { TRPCClientError } from '@documenso/trpc/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@documenso/ui/primitives/dialog';
|
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
export type SuperDeleteDocumentDialogProps = {
|
|
||||||
document: Document;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SuperDeleteDocumentDialog = ({ document }: SuperDeleteDocumentDialogProps) => {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [reason, setReason] = useState('');
|
|
||||||
|
|
||||||
const { mutateAsync: deleteDocument, isLoading: isDeletingDocument } =
|
|
||||||
trpc.admin.deleteDocument.useMutation();
|
|
||||||
|
|
||||||
const handleDeleteDocument = async () => {
|
|
||||||
try {
|
|
||||||
if (!reason) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteDocument({ id: document.id, reason });
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Document deleted',
|
|
||||||
description: 'The Document has been deleted successfully.',
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
router.push('/admin/documents');
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
|
||||||
toast({
|
|
||||||
title: 'An error occurred',
|
|
||||||
description: err.message,
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast({
|
|
||||||
title: 'An unknown error occurred',
|
|
||||||
variant: 'destructive',
|
|
||||||
description:
|
|
||||||
err.message ??
|
|
||||||
'We encountered an unknown error while attempting to delete your document. Please try again later.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<Alert
|
|
||||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row "
|
|
||||||
variant="neutral"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<AlertTitle>Delete Document</AlertTitle>
|
|
||||||
<AlertDescription className="mr-2">
|
|
||||||
Delete the document. This action is irreversible so proceed with caution.
|
|
||||||
</AlertDescription>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="destructive">Delete Document</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader className="space-y-4">
|
|
||||||
<DialogTitle>Delete Document</DialogTitle>
|
|
||||||
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertDescription className="selection:bg-red-100">
|
|
||||||
This action is not reversible. Please be certain.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<DialogDescription>To confirm, please enter the reason</DialogDescription>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
className="mt-2"
|
|
||||||
type="text"
|
|
||||||
value={reason}
|
|
||||||
onChange={(e) => setReason(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
onClick={handleDeleteDocument}
|
|
||||||
loading={isDeletingDocument}
|
|
||||||
variant="destructive"
|
|
||||||
disabled={!reason}
|
|
||||||
>
|
|
||||||
{isDeletingDocument ? 'Deleting document...' : 'Delete Document'}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -58,7 +58,6 @@ export const UsersDataTable = ({
|
|||||||
perPage,
|
perPage,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [debouncedSearchString]);
|
}, [debouncedSearchString]);
|
||||||
|
|
||||||
const onPaginationChange = (page: number, perPage: number) => {
|
const onPaginationChange = (page: number, perPage: number) => {
|
||||||
|
|||||||
@@ -7,11 +7,9 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
|
||||||
import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
|
||||||
import { truncateTitle } from '~/helpers/truncate-title';
|
import { truncateTitle } from '~/helpers/truncate-title';
|
||||||
|
|
||||||
export type SignDialogProps = {
|
export type SignDialogProps = {
|
||||||
@@ -68,38 +66,22 @@ export const SignDialog = ({
|
|||||||
{isComplete ? 'Complete' : 'Next field'}
|
{isComplete ? 'Complete' : 'Next field'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>
|
<div className="text-center">
|
||||||
<div className="text-foreground text-xl font-semibold">
|
<div className="text-foreground text-xl font-semibold">
|
||||||
{role === RecipientRole.VIEWER && 'Complete Viewing'}
|
{role === RecipientRole.VIEWER && 'Mark Document as Viewed'}
|
||||||
{role === RecipientRole.SIGNER && 'Complete Signing'}
|
{role === RecipientRole.SIGNER && 'Sign Document'}
|
||||||
{role === RecipientRole.APPROVER && 'Complete Approval'}
|
{role === RecipientRole.APPROVER && 'Approve Document'}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground mx-auto w-4/5 py-2 text-center">
|
||||||
|
{role === RecipientRole.VIEWER &&
|
||||||
|
`You are about to finish viewing "${truncatedTitle}". Are you sure?`}
|
||||||
|
{role === RecipientRole.SIGNER &&
|
||||||
|
`You are about to finish signing "${truncatedTitle}". Are you sure?`}
|
||||||
|
{role === RecipientRole.APPROVER &&
|
||||||
|
`You are about to finish approving "${truncatedTitle}". Are you sure?`}
|
||||||
</div>
|
</div>
|
||||||
</DialogTitle>
|
|
||||||
|
|
||||||
<div className="text-muted-foreground max-w-[50ch]">
|
|
||||||
{role === RecipientRole.VIEWER && (
|
|
||||||
<span>
|
|
||||||
You are about to complete viewing "{truncatedTitle}".
|
|
||||||
<br /> Are you sure?
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{role === RecipientRole.SIGNER && (
|
|
||||||
<span>
|
|
||||||
You are about to complete signing "{truncatedTitle}".
|
|
||||||
<br /> Are you sure?
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{role === RecipientRole.APPROVER && (
|
|
||||||
<span>
|
|
||||||
You are about to complete approving "{truncatedTitle}".
|
|
||||||
<br /> Are you sure?
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SigningDisclosure className="mt-4" />
|
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import { Label } from '@documenso/ui/primitives/label';
|
|||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { SigningDisclosure } from '~/components/general/signing-disclosure';
|
|
||||||
|
|
||||||
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
import { useRequiredDocumentAuthContext } from './document-auth-provider';
|
||||||
import { useRequiredSigningContext } from './provider';
|
import { useRequiredSigningContext } from './provider';
|
||||||
import { SigningFieldContainer } from './signing-field-container';
|
import { SigningFieldContainer } from './signing-field-container';
|
||||||
@@ -202,8 +200,6 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SigningDisclosure />
|
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
export default function SignatureDisclosure() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<article className="prose">
|
|
||||||
<h1>Electronic Signature Disclosure</h1>
|
|
||||||
|
|
||||||
<h2>Welcome</h2>
|
|
||||||
<p>
|
|
||||||
Thank you for using Documenso to perform your electronic document signing. The purpose of
|
|
||||||
this disclosure is to inform you about the process, legality, and your rights regarding
|
|
||||||
the use of electronic signatures on our platform. By opting to use an electronic
|
|
||||||
signature, you are agreeing to the terms and conditions outlined below.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Acceptance and Consent</h2>
|
|
||||||
<p>
|
|
||||||
When you use our platform to affix your electronic signature to documents, you are
|
|
||||||
consenting to do so under the Electronic Signatures in Global and National Commerce Act
|
|
||||||
(E-Sign Act) and other applicable laws. This action indicates your agreement to use
|
|
||||||
electronic means to sign documents and receive notifications.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Legality of Electronic Signatures</h2>
|
|
||||||
<p>
|
|
||||||
An electronic signature provided by you on our platform, achieved through clicking through
|
|
||||||
to a document and entering your name, or any other electronic signing method we provide,
|
|
||||||
is legally binding. It carries the same weight and enforceability as a manual signature
|
|
||||||
written with ink on paper.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>System Requirements</h2>
|
|
||||||
<p>To use our electronic signature service, you must have access to:</p>
|
|
||||||
<ul>
|
|
||||||
<li>A stable internet connection</li>
|
|
||||||
<li>An email account</li>
|
|
||||||
<li>A device capable of accessing, opening, and reading documents</li>
|
|
||||||
<li>A means to print or download documents for your records</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Electronic Delivery of Documents</h2>
|
|
||||||
<p>
|
|
||||||
All documents related to the electronic signing process will be provided to you
|
|
||||||
electronically through our platform or via email. It is your responsibility to ensure that
|
|
||||||
your email address is current and that you can receive and open our emails.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Consent to Electronic Transactions</h2>
|
|
||||||
<p>
|
|
||||||
By using the electronic signature feature, you are consenting to conduct transactions and
|
|
||||||
receive disclosures electronically. You acknowledge that your electronic signature on
|
|
||||||
documents is binding and that you accept the terms outlined in the documents you are
|
|
||||||
signing.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Withdrawing Consent</h2>
|
|
||||||
<p>
|
|
||||||
You have the right to withdraw your consent to use electronic signatures at any time
|
|
||||||
before completing the signing process. To withdraw your consent, please contact the sender
|
|
||||||
of the document. In failing to contact the sender you may reach out to{' '}
|
|
||||||
<a href="mailto:support@documenso.com">support@documenso.com</a> for assistance. Be aware
|
|
||||||
that withdrawing consent may delay or halt the completion of the related transaction or
|
|
||||||
service.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Updating Your Information</h2>
|
|
||||||
<p>
|
|
||||||
It is crucial to keep your contact information, especially your email address, up to date
|
|
||||||
with us. Please notify us immediately of any changes to ensure that you continue to
|
|
||||||
receive all necessary communications.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Retention of Documents</h2>
|
|
||||||
<p>
|
|
||||||
After signing a document electronically, you will be provided the opportunity to view,
|
|
||||||
download, and print the document for your records. It is highly recommended that you
|
|
||||||
retain a copy of all electronically signed documents for your personal records. We will
|
|
||||||
also retain a copy of the signed document for our records however we may not be able to
|
|
||||||
provide you with a copy of the signed document after a certain period of time.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Acknowledgment</h2>
|
|
||||||
<p>
|
|
||||||
By proceeding to use the electronic signature service provided by Documenso, you affirm
|
|
||||||
that you have read and understood this disclosure. You agree to all terms and conditions
|
|
||||||
related to the use of electronic signatures and electronic transactions as outlined
|
|
||||||
herein.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>Contact Information</h2>
|
|
||||||
<p>
|
|
||||||
For any questions regarding this disclosure, electronic signatures, or any related
|
|
||||||
process, please contact us at:{' '}
|
|
||||||
<a href="mailto:support@documenso.com">support@documenso.com</a>
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<div className="mt-8">
|
|
||||||
<Button asChild>
|
|
||||||
<Link href="/documents">Back to Documents</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import { Recipient } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { StackAvatar } from './stack-avatar';
|
import { StackAvatar } from './stack-avatar';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SVGAttributes } from 'react';
|
import { SVGAttributes } from 'react';
|
||||||
|
|
||||||
export type LogoProps = SVGAttributes<SVGSVGElement>;
|
export type LogoProps = SVGAttributes<SVGSVGElement>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
import { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import { Globe, Lock } from 'lucide-react';
|
import { Globe, Lock } from 'lucide-react';
|
||||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||||
|
|
||||||
import type { TemplateType as TemplateTypePrisma } from '@documenso/prisma/client';
|
import { TemplateType as TemplateTypePrisma } from '@documenso/prisma/client';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
type TemplateTypeIcon = {
|
type TemplateTypeIcon = {
|
||||||
|
|||||||
@@ -47,9 +47,12 @@ export const ViewRecoveryCodesDialog = () => {
|
|||||||
data: recoveryCodes,
|
data: recoveryCodes,
|
||||||
mutate,
|
mutate,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isError,
|
||||||
error,
|
error,
|
||||||
} = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
|
} = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
|
||||||
|
|
||||||
|
// error?.data?.code
|
||||||
|
|
||||||
const viewRecoveryCodesForm = useForm<TViewRecoveryCodesForm>({
|
const viewRecoveryCodesForm = useForm<TViewRecoveryCodesForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
token: '',
|
token: '',
|
||||||
|
|||||||
@@ -55,8 +55,11 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
const hasTwoFactorAuthentication = user.twoFactorEnabled;
|
||||||
|
|
||||||
const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation();
|
const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation();
|
||||||
|
const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
|
||||||
|
trpc.profile.deleteAccount.useMutation();
|
||||||
|
|
||||||
const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => {
|
const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { HTMLAttributes } from 'react';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
|
|
||||||
export type SigningDisclosureProps = HTMLAttributes<HTMLParagraphElement>;
|
|
||||||
|
|
||||||
export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProps) => {
|
|
||||||
return (
|
|
||||||
<p className={cn('text-muted-foreground text-xs', className)} {...props}>
|
|
||||||
By proceeding with your electronic signature, you acknowledge and consent that it will be used
|
|
||||||
to sign the given document and holds the same legal validity as a handwritten signature. By
|
|
||||||
completing the electronic signing process, you affirm your understanding and acceptance of
|
|
||||||
these conditions.
|
|
||||||
<span className="mt-2 block">
|
|
||||||
Read the full{' '}
|
|
||||||
<Link
|
|
||||||
className="text-documenso-700 underline"
|
|
||||||
href="/articles/signature-disclosure"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
signature disclosure
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SVGAttributes } from 'react';
|
import { SVGAttributes } from 'react';
|
||||||
|
|
||||||
export type BackgroundProps = Omit<SVGAttributes<SVGElement>, 'viewBox'>;
|
export type BackgroundProps = Omit<SVGAttributes<SVGElement>, 'viewBox'>;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="border-border bg-background text-muted-foreground inline-block max-w-full truncate rounded-md border px-2.5 py-1.5 text-sm lowercase">
|
<div className="border-border bg-background text-muted-foreground inline-block max-w-full truncate rounded-md border px-2.5 py-1.5 text-sm">
|
||||||
{baseUrl.host}/u/{user.url}
|
{baseUrl.host}/u/{user.url}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||||
import type { ThemeProviderProps } from 'next-themes/dist/types';
|
import { ThemeProviderProps } from 'next-themes/dist/types';
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
|
import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email';
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin } from './fixtures/authentication';
|
import { apiSignin } from './fixtures/authentication';
|
||||||
|
|
||||||
const getDocumentByToken = async (token: string) => {
|
|
||||||
return await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
Recipient: {
|
|
||||||
some: {
|
|
||||||
token,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
test(`[PR-718]: should be able to create a document`, async ({ page }) => {
|
test(`[PR-718]: should be able to create a document`, async ({ page }) => {
|
||||||
await page.goto('/signin');
|
await page.goto('/signin');
|
||||||
|
|
||||||
@@ -258,7 +246,7 @@ test('should be able to create, send and sign a document', async ({ page }) => {
|
|||||||
await page.waitForURL(`/sign/${token}`);
|
await page.waitForURL(`/sign/${token}`);
|
||||||
|
|
||||||
// Check if document has been viewed
|
// Check if document has been viewed
|
||||||
const { status } = await getDocumentByToken(token);
|
const { status } = await getDocumentByToken({ token });
|
||||||
expect(status).toBe(DocumentStatus.PENDING);
|
expect(status).toBe(DocumentStatus.PENDING);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Complete' }).click();
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
@@ -269,7 +257,7 @@ test('should be able to create, send and sign a document', async ({ page }) => {
|
|||||||
await expect(page.getByText('You have signed')).toBeVisible();
|
await expect(page.getByText('You have signed')).toBeVisible();
|
||||||
|
|
||||||
// Check if document has been signed
|
// Check if document has been signed
|
||||||
const { status: completedStatus } = await getDocumentByToken(token);
|
const { status: completedStatus } = await getDocumentByToken({ token });
|
||||||
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
|
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -343,7 +331,7 @@ test('should be able to create, send with redirect url, sign a document and redi
|
|||||||
await page.waitForURL(`/sign/${token}`);
|
await page.waitForURL(`/sign/${token}`);
|
||||||
|
|
||||||
// Check if document has been viewed
|
// Check if document has been viewed
|
||||||
const { status } = await getDocumentByToken(token);
|
const { status } = await getDocumentByToken({ token });
|
||||||
expect(status).toBe(DocumentStatus.PENDING);
|
expect(status).toBe(DocumentStatus.PENDING);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Complete' }).click();
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
@@ -353,6 +341,6 @@ test('should be able to create, send with redirect url, sign a document and redi
|
|||||||
await page.waitForURL('https://documenso.com');
|
await page.waitForURL('https://documenso.com');
|
||||||
|
|
||||||
// Check if document has been signed
|
// Check if document has been signed
|
||||||
const { status: completedStatus } = await getDocumentByToken(token);
|
const { status: completedStatus } = await getDocumentByToken({ token });
|
||||||
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
|
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import { Section, Text } from '../components';
|
|
||||||
import { TemplateDocumentImage } from './template-document-image';
|
|
||||||
|
|
||||||
export interface TemplateDocumentDeleteProps {
|
|
||||||
reason: string;
|
|
||||||
documentName: string;
|
|
||||||
assetBaseUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TemplateDocumentDelete = ({
|
|
||||||
reason,
|
|
||||||
documentName,
|
|
||||||
assetBaseUrl,
|
|
||||||
}: TemplateDocumentDeleteProps) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
|
||||||
|
|
||||||
<Section>
|
|
||||||
<Text className="text-primary mb-0 mt-6 text-left text-lg font-semibold">
|
|
||||||
Your document has been deleted by an admin!
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="mx-auto mb-6 mt-1 text-left text-base text-slate-400">
|
|
||||||
"{documentName}" has been deleted by an admin.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="mx-auto mb-6 mt-1 text-left text-base text-slate-400">
|
|
||||||
This document can not be recovered, if you would like to dispute the reason for future
|
|
||||||
documents please contact support.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="mx-auto mt-1 text-left text-base text-slate-400">
|
|
||||||
The reason provided for deletion is the following:
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text className="mx-auto mb-6 mt-1 text-left text-base italic text-slate-400">
|
|
||||||
{reason}
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TemplateDocumentDelete;
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import config from '@documenso/tailwind-config';
|
|
||||||
|
|
||||||
import { Body, Container, Head, Hr, Html, Img, Preview, Section, Tailwind } from '../components';
|
|
||||||
import {
|
|
||||||
TemplateDocumentDelete,
|
|
||||||
type TemplateDocumentDeleteProps,
|
|
||||||
} from '../template-components/template-document-super-delete';
|
|
||||||
import { TemplateFooter } from '../template-components/template-footer';
|
|
||||||
|
|
||||||
export type DocumentDeleteEmailTemplateProps = Partial<TemplateDocumentDeleteProps>;
|
|
||||||
|
|
||||||
export const DocumentSuperDeleteEmailTemplate = ({
|
|
||||||
documentName = 'Open Source Pledge.pdf',
|
|
||||||
assetBaseUrl = 'http://localhost:3002',
|
|
||||||
reason = 'Unknown',
|
|
||||||
}: DocumentDeleteEmailTemplateProps) => {
|
|
||||||
const previewText = `An admin has deleted your document "${documentName}".`;
|
|
||||||
|
|
||||||
const getAssetUrl = (path: string) => {
|
|
||||||
return new URL(path, assetBaseUrl).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Html>
|
|
||||||
<Head />
|
|
||||||
<Preview>{previewText}</Preview>
|
|
||||||
<Tailwind
|
|
||||||
config={{
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: config.theme.extend.colors,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Body className="mx-auto my-auto bg-white font-sans">
|
|
||||||
<Section>
|
|
||||||
<Container className="mx-auto mb-2 mt-8 max-w-xl rounded-lg border border-solid border-slate-200 p-4 backdrop-blur-sm">
|
|
||||||
<Section>
|
|
||||||
<Img
|
|
||||||
src={getAssetUrl('/static/logo.png')}
|
|
||||||
alt="Documenso Logo"
|
|
||||||
className="mb-4 h-6"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TemplateDocumentDelete
|
|
||||||
reason={reason}
|
|
||||||
documentName={documentName}
|
|
||||||
assetBaseUrl={assetBaseUrl}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
</Container>
|
|
||||||
|
|
||||||
<Hr className="mx-auto mt-12 max-w-xl" />
|
|
||||||
|
|
||||||
<Container className="mx-auto max-w-xl">
|
|
||||||
<TemplateFooter />
|
|
||||||
</Container>
|
|
||||||
</Section>
|
|
||||||
</Body>
|
|
||||||
</Tailwind>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DocumentSuperDeleteEmailTemplate;
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument, PDFSignature, rectangle } from 'pdf-lib';
|
||||||
|
|
||||||
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
@@ -15,9 +15,7 @@ import { signPdf } from '@documenso/signing';
|
|||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { getFile } from '../../universal/upload/get-file';
|
import { getFile } from '../../universal/upload/get-file';
|
||||||
import { putFile } from '../../universal/upload/put-file';
|
import { putFile } from '../../universal/upload/put-file';
|
||||||
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
|
||||||
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
|
||||||
import { normalizeSignatureAppearances } from '../pdf/normalize-signature-appearances';
|
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
import { sendCompletedEmail } from './send-completed-email';
|
import { sendCompletedEmail } from './send-completed-email';
|
||||||
|
|
||||||
@@ -93,10 +91,31 @@ export const sealDocument = async ({
|
|||||||
|
|
||||||
const doc = await PDFDocument.load(pdfData);
|
const doc = await PDFDocument.load(pdfData);
|
||||||
|
|
||||||
// Normalize and flatten layers that could cause issues with the signature
|
const form = doc.getForm();
|
||||||
normalizeSignatureAppearances(doc);
|
|
||||||
doc.getForm().flatten();
|
// Remove old signatures
|
||||||
flattenAnnotations(doc);
|
for (const field of form.getFields()) {
|
||||||
|
if (field instanceof PDFSignature) {
|
||||||
|
field.acroField.getWidgets().forEach((widget) => {
|
||||||
|
widget.ensureAP();
|
||||||
|
|
||||||
|
try {
|
||||||
|
widget.getNormalAppearance();
|
||||||
|
} catch (e) {
|
||||||
|
const { context } = widget.dict;
|
||||||
|
|
||||||
|
const xobj = context.formXObject([rectangle(0, 0, 0, 0)]);
|
||||||
|
|
||||||
|
const streamRef = context.register(xobj);
|
||||||
|
|
||||||
|
widget.setNormalAppearance(streamRef);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten the form to stop annotation layers from appearing above documenso fields
|
||||||
|
form.flatten();
|
||||||
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
await insertFieldInPDF(doc, field);
|
await insertFieldInPDF(doc, field);
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
|
||||||
import { render } from '@documenso/email/render';
|
|
||||||
import { DocumentSuperDeleteEmailTemplate } from '@documenso/email/templates/document-super-delete';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|
||||||
|
|
||||||
export interface SendDeleteEmailOptions {
|
|
||||||
documentId: number;
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOptions) => {
|
|
||||||
const document = await prisma.document.findFirst({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
User: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new Error('Document not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { email, name } = document.User;
|
|
||||||
|
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
|
||||||
|
|
||||||
const template = createElement(DocumentSuperDeleteEmailTemplate, {
|
|
||||||
documentName: document.title,
|
|
||||||
reason,
|
|
||||||
assetBaseUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
await mailer.sendMail({
|
|
||||||
to: {
|
|
||||||
address: email,
|
|
||||||
name: name || '',
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
|
|
||||||
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
|
|
||||||
},
|
|
||||||
subject: 'Document Deleted!',
|
|
||||||
html: render(template),
|
|
||||||
text: render(template, { plainText: true }),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { mailer } from '@documenso/email/mailer';
|
|
||||||
import { render } from '@documenso/email/render';
|
|
||||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|
||||||
import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
|
||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
|
||||||
|
|
||||||
export type SuperDeleteDocumentOptions = {
|
|
||||||
id: number;
|
|
||||||
requestMetadata?: RequestMetadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDocumentOptions) => {
|
|
||||||
const document = await prisma.document.findUnique({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Recipient: true,
|
|
||||||
documentMeta: true,
|
|
||||||
User: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!document) {
|
|
||||||
throw new Error('Document not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const { status, User: user } = document;
|
|
||||||
|
|
||||||
// if the document is pending, send cancellation emails to all recipients
|
|
||||||
if (status === DocumentStatus.PENDING && document.Recipient.length > 0) {
|
|
||||||
await Promise.all(
|
|
||||||
document.Recipient.map(async (recipient) => {
|
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
|
||||||
const template = createElement(DocumentCancelTemplate, {
|
|
||||||
documentName: document.title,
|
|
||||||
inviterName: user.name || undefined,
|
|
||||||
inviterEmail: user.email,
|
|
||||||
assetBaseUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
await mailer.sendMail({
|
|
||||||
to: {
|
|
||||||
address: recipient.email,
|
|
||||||
name: recipient.name,
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
name: FROM_NAME,
|
|
||||||
address: FROM_ADDRESS,
|
|
||||||
},
|
|
||||||
subject: 'Document Cancelled',
|
|
||||||
html: render(template),
|
|
||||||
text: render(template, { plainText: true }),
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// always hard delete if deleted from admin
|
|
||||||
return await prisma.$transaction(async (tx) => {
|
|
||||||
await tx.documentAuditLog.create({
|
|
||||||
data: createDocumentAuditLogData({
|
|
||||||
documentId: id,
|
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
|
|
||||||
user,
|
|
||||||
requestMetadata,
|
|
||||||
data: {
|
|
||||||
type: 'HARD',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
return await tx.document.delete({ where: { id } });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { PDFAnnotation, PDFRef } from 'pdf-lib';
|
|
||||||
import {
|
|
||||||
PDFDict,
|
|
||||||
type PDFDocument,
|
|
||||||
PDFName,
|
|
||||||
drawObject,
|
|
||||||
popGraphicsState,
|
|
||||||
pushGraphicsState,
|
|
||||||
rotateInPlace,
|
|
||||||
translate,
|
|
||||||
} from 'pdf-lib';
|
|
||||||
|
|
||||||
export const flattenAnnotations = (document: PDFDocument) => {
|
|
||||||
const pages = document.getPages();
|
|
||||||
|
|
||||||
for (const page of pages) {
|
|
||||||
const annotations = page.node.Annots()?.asArray() ?? [];
|
|
||||||
|
|
||||||
annotations.forEach((annotation) => {
|
|
||||||
if (!(annotation instanceof PDFRef)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualAnnotation = page.node.context.lookup(annotation);
|
|
||||||
|
|
||||||
if (!(actualAnnotation instanceof PDFDict)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pdfAnnot = PDFAnnotation.fromDict(actualAnnotation);
|
|
||||||
|
|
||||||
const appearance = pdfAnnot.ensureAP();
|
|
||||||
|
|
||||||
// Skip annotations without a normal appearance
|
|
||||||
if (!appearance.has(PDFName.of('N'))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalAppearance = pdfAnnot.getNormalAppearance();
|
|
||||||
const rectangle = pdfAnnot.getRectangle();
|
|
||||||
|
|
||||||
if (!(normalAppearance instanceof PDFRef)) {
|
|
||||||
// Not sure how to get the reference to the normal appearance yet
|
|
||||||
// so we should skip this annotation for now
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const xobj = page.node.newXObject('FlatAnnot', normalAppearance);
|
|
||||||
|
|
||||||
const operators = [
|
|
||||||
pushGraphicsState(),
|
|
||||||
translate(rectangle.x, rectangle.y),
|
|
||||||
...rotateInPlace({ ...rectangle, rotation: 0 }),
|
|
||||||
drawObject(xobj),
|
|
||||||
popGraphicsState(),
|
|
||||||
].filter((op) => !!op);
|
|
||||||
|
|
||||||
page.pushOperators(...operators);
|
|
||||||
|
|
||||||
page.node.removeAnnot(annotation);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { PDFDocument } from 'pdf-lib';
|
|
||||||
import { PDFSignature, rectangle } from 'pdf-lib';
|
|
||||||
|
|
||||||
export const normalizeSignatureAppearances = (document: PDFDocument) => {
|
|
||||||
const form = document.getForm();
|
|
||||||
|
|
||||||
for (const field of form.getFields()) {
|
|
||||||
if (field instanceof PDFSignature) {
|
|
||||||
field.acroField.getWidgets().forEach((widget) => {
|
|
||||||
widget.ensureAP();
|
|
||||||
|
|
||||||
try {
|
|
||||||
widget.getNormalAppearance();
|
|
||||||
} catch {
|
|
||||||
const { context } = widget.dict;
|
|
||||||
|
|
||||||
const xobj = context.formXObject([rectangle(0, 0, 0, 0)]);
|
|
||||||
|
|
||||||
const streamRef = context.register(xobj);
|
|
||||||
|
|
||||||
widget.setNormalAppearance(streamRef);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -6,13 +6,7 @@
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "VerificationToken" ADD COLUMN "secondaryId" TEXT;
|
ALTER TABLE "VerificationToken" ADD COLUMN "secondaryId" TEXT NOT NULL;
|
||||||
|
|
||||||
-- Set all null secondaryId fields to a uuid
|
|
||||||
UPDATE "VerificationToken" SET "secondaryId" = gen_random_uuid()::text WHERE "secondaryId" IS NULL;
|
|
||||||
|
|
||||||
-- Restrict the VerificationToken to required
|
|
||||||
ALTER TABLE "VerificationToken" ALTER COLUMN "secondaryId" SET NOT NULL;
|
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "VerificationToken_secondaryId_key" ON "VerificationToken"("secondaryId");
|
CREATE UNIQUE INDEX "VerificationToken_secondaryId_key" ON "VerificationToken"("secondaryId");
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
PDFArray,
|
PDFArray,
|
||||||
PDFDict,
|
|
||||||
PDFDocument,
|
PDFDocument,
|
||||||
PDFHexString,
|
PDFHexString,
|
||||||
PDFName,
|
PDFName,
|
||||||
@@ -17,7 +16,7 @@ export type AddSigningPlaceholderOptions = {
|
|||||||
|
|
||||||
export const addSigningPlaceholder = async ({ pdf }: AddSigningPlaceholderOptions) => {
|
export const addSigningPlaceholder = async ({ pdf }: AddSigningPlaceholderOptions) => {
|
||||||
const doc = await PDFDocument.load(pdf);
|
const doc = await PDFDocument.load(pdf);
|
||||||
const [firstPage] = doc.getPages();
|
const pages = doc.getPages();
|
||||||
|
|
||||||
const byteRange = PDFArray.withContext(doc.context);
|
const byteRange = PDFArray.withContext(doc.context);
|
||||||
|
|
||||||
@@ -26,8 +25,7 @@ export const addSigningPlaceholder = async ({ pdf }: AddSigningPlaceholderOption
|
|||||||
byteRange.push(PDFName.of(BYTE_RANGE_PLACEHOLDER));
|
byteRange.push(PDFName.of(BYTE_RANGE_PLACEHOLDER));
|
||||||
byteRange.push(PDFName.of(BYTE_RANGE_PLACEHOLDER));
|
byteRange.push(PDFName.of(BYTE_RANGE_PLACEHOLDER));
|
||||||
|
|
||||||
const signature = doc.context.register(
|
const signature = doc.context.obj({
|
||||||
doc.context.obj({
|
|
||||||
Type: 'Sig',
|
Type: 'Sig',
|
||||||
Filter: 'Adobe.PPKLite',
|
Filter: 'Adobe.PPKLite',
|
||||||
SubFilter: 'adbe.pkcs7.detached',
|
SubFilter: 'adbe.pkcs7.detached',
|
||||||
@@ -35,62 +33,56 @@ export const addSigningPlaceholder = async ({ pdf }: AddSigningPlaceholderOption
|
|||||||
Contents: PDFHexString.fromText(' '.repeat(8192)),
|
Contents: PDFHexString.fromText(' '.repeat(8192)),
|
||||||
Reason: PDFString.of('Signed by Documenso'),
|
Reason: PDFString.of('Signed by Documenso'),
|
||||||
M: PDFString.fromDate(new Date()),
|
M: PDFString.fromDate(new Date()),
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const widget = doc.context.register(
|
const signatureRef = doc.context.register(signature);
|
||||||
doc.context.obj({
|
|
||||||
|
const widget = doc.context.obj({
|
||||||
Type: 'Annot',
|
Type: 'Annot',
|
||||||
Subtype: 'Widget',
|
Subtype: 'Widget',
|
||||||
FT: 'Sig',
|
FT: 'Sig',
|
||||||
Rect: [0, 0, 0, 0],
|
Rect: [0, 0, 0, 0],
|
||||||
V: signature,
|
V: signatureRef,
|
||||||
T: PDFString.of('Signature1'),
|
T: PDFString.of('Signature1'),
|
||||||
F: 4,
|
F: 4,
|
||||||
P: firstPage.ref,
|
P: pages[0].ref,
|
||||||
AP: doc.context.obj({
|
});
|
||||||
N: doc.context.register(doc.context.formXObject([rectangle(0, 0, 0, 0)])),
|
|
||||||
}),
|
const xobj = widget.context.formXObject([rectangle(0, 0, 0, 0)]);
|
||||||
|
|
||||||
|
const streamRef = widget.context.register(xobj);
|
||||||
|
|
||||||
|
widget.set(PDFName.of('AP'), widget.context.obj({ N: streamRef }));
|
||||||
|
|
||||||
|
const widgetRef = doc.context.register(widget);
|
||||||
|
|
||||||
|
let widgets = pages[0].node.get(PDFName.of('Annots'));
|
||||||
|
|
||||||
|
if (widgets instanceof PDFArray) {
|
||||||
|
widgets.push(widgetRef);
|
||||||
|
} else {
|
||||||
|
const newWidgets = PDFArray.withContext(doc.context);
|
||||||
|
|
||||||
|
newWidgets.push(widgetRef);
|
||||||
|
|
||||||
|
pages[0].node.set(PDFName.of('Annots'), newWidgets);
|
||||||
|
|
||||||
|
widgets = pages[0].node.get(PDFName.of('Annots'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widgets) {
|
||||||
|
throw new Error('No widgets');
|
||||||
|
}
|
||||||
|
|
||||||
|
pages[0].node.set(PDFName.of('Annots'), widgets);
|
||||||
|
|
||||||
|
doc.catalog.set(
|
||||||
|
PDFName.of('AcroForm'),
|
||||||
|
doc.context.obj({
|
||||||
|
SigFlags: 3,
|
||||||
|
Fields: [widgetRef],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let widgets: PDFArray;
|
|
||||||
|
|
||||||
try {
|
|
||||||
widgets = firstPage.node.lookup(PDFName.of('Annots'), PDFArray);
|
|
||||||
} catch {
|
|
||||||
widgets = PDFArray.withContext(doc.context);
|
|
||||||
|
|
||||||
firstPage.node.set(PDFName.of('Annots'), widgets);
|
|
||||||
}
|
|
||||||
|
|
||||||
widgets.push(widget);
|
|
||||||
|
|
||||||
let arcoForm: PDFDict;
|
|
||||||
|
|
||||||
try {
|
|
||||||
arcoForm = doc.catalog.lookup(PDFName.of('AcroForm'), PDFDict);
|
|
||||||
} catch {
|
|
||||||
arcoForm = doc.context.obj({
|
|
||||||
Fields: PDFArray.withContext(doc.context),
|
|
||||||
});
|
|
||||||
|
|
||||||
doc.catalog.set(PDFName.of('AcroForm'), arcoForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fields: PDFArray;
|
|
||||||
|
|
||||||
try {
|
|
||||||
fields = arcoForm.lookup(PDFName.of('Fields'), PDFArray);
|
|
||||||
} catch {
|
|
||||||
fields = PDFArray.withContext(doc.context);
|
|
||||||
|
|
||||||
arcoForm.set(PDFName.of('Fields'), fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
fields.push(widget);
|
|
||||||
|
|
||||||
arcoForm.set(PDFName.of('SigFlags'), PDFNumber.of(3));
|
|
||||||
|
|
||||||
return Buffer.from(await doc.save({ useObjectStreams: false }));
|
return Buffer.from(await doc.save({ useObjectStreams: false }));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,16 +4,12 @@ import { findDocuments } from '@documenso/lib/server-only/admin/get-all-document
|
|||||||
import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient';
|
import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient';
|
||||||
import { updateUser } from '@documenso/lib/server-only/admin/update-user';
|
import { updateUser } from '@documenso/lib/server-only/admin/update-user';
|
||||||
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
|
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
|
||||||
import { sendDeleteEmail } from '@documenso/lib/server-only/document/send-delete-email';
|
|
||||||
import { superDeleteDocument } from '@documenso/lib/server-only/document/super-delete-document';
|
|
||||||
import { upsertSiteSetting } from '@documenso/lib/server-only/site-settings/upsert-site-setting';
|
import { upsertSiteSetting } from '@documenso/lib/server-only/site-settings/upsert-site-setting';
|
||||||
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
||||||
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
||||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
||||||
|
|
||||||
import { adminProcedure, router } from '../trpc';
|
import { adminProcedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
ZAdminDeleteDocumentMutationSchema,
|
|
||||||
ZAdminDeleteUserMutationSchema,
|
ZAdminDeleteUserMutationSchema,
|
||||||
ZAdminFindDocumentsQuerySchema,
|
ZAdminFindDocumentsQuerySchema,
|
||||||
ZAdminResealDocumentMutationSchema,
|
ZAdminResealDocumentMutationSchema,
|
||||||
@@ -122,25 +118,4 @@ export const adminRouter = router({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
deleteDocument: adminProcedure
|
|
||||||
.input(ZAdminDeleteDocumentMutationSchema)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { id, reason } = input;
|
|
||||||
try {
|
|
||||||
await sendDeleteEmail({ documentId: id, reason });
|
|
||||||
|
|
||||||
return await superDeleteDocument({
|
|
||||||
id,
|
|
||||||
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'BAD_REQUEST',
|
|
||||||
message: 'We were unable to delete the specified document. Please try again.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,10 +48,3 @@ export const ZAdminDeleteUserMutationSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type TAdminDeleteUserMutationSchema = z.infer<typeof ZAdminDeleteUserMutationSchema>;
|
export type TAdminDeleteUserMutationSchema = z.infer<typeof ZAdminDeleteUserMutationSchema>;
|
||||||
|
|
||||||
export const ZAdminDeleteDocumentMutationSchema = z.object({
|
|
||||||
id: z.number().min(1),
|
|
||||||
reason: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TAdminDeleteDocomentMutationSchema = z.infer<typeof ZAdminDeleteDocumentMutationSchema>;
|
|
||||||
|
|||||||
@@ -153,10 +153,7 @@ export const authRouter = router({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
createPasskeySigninOptions: procedure.mutation(async ({ ctx }) => {
|
createPasskeySigninOptions: procedure.mutation(async ({ ctx }) => {
|
||||||
const cookies = parse(ctx.req.headers.cookie ?? '');
|
const sessionIdToken = parse(ctx.req.headers.cookie ?? '')['next-auth.csrf-token'];
|
||||||
|
|
||||||
const sessionIdToken =
|
|
||||||
cookies['__Host-next-auth.csrf-token'] || cookies['next-auth.csrf-token'];
|
|
||||||
|
|
||||||
if (!sessionIdToken) {
|
if (!sessionIdToken) {
|
||||||
throw new Error('Missing CSRF token');
|
throw new Error('Missing CSRF token');
|
||||||
|
|||||||
@@ -91,6 +91,11 @@
|
|||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
font-feature-settings: 'rlig' 1, 'calt' 1;
|
font-feature-settings: 'rlig' 1, 'calt' 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user