Compare commits

..

19 Commits

Author SHA1 Message Date
David Nguyen
3c0918963d fix: polish 2024-03-27 18:35:20 +08:00
David Nguyen
47916e3127 feat: add document auth 2FA 2024-03-27 16:26:59 +08:00
David Nguyen
62dd737cf0 feat: add document auth passkey 2024-03-27 16:13:22 +08:00
David Nguyen
0d510cf280 Merge branch 'main' into feat/document-auth 2024-03-27 15:19:24 +08:00
David Nguyen
f88faa43d9 chore: refactor env config 2024-03-27 15:17:10 +08:00
David Nguyen
074adccae2 chore: set global env for playwright 2024-03-26 21:39:45 +08:00
David Nguyen
844261c35c Merge branch 'main' into feat/document-auth 2024-03-26 21:36:58 +08:00
David Nguyen
c0fb5caf9c fix: update reauth constraints and tests 2024-03-26 18:33:20 +08:00
David Nguyen
b6c4cc9dc8 feat: restrict reauth to EE 2024-03-26 16:46:47 +08:00
David Nguyen
94da57704d Merge branch 'main' into feat/document-auth 2024-03-25 23:06:46 +08:00
David Nguyen
1375946bc5 chore: refactor 2024-03-21 22:02:09 +08:00
David Nguyen
dc6aba58e6 chore: typo 2024-03-17 14:47:06 +08:00
David Nguyen
364b9e03e1 fix: build 2024-03-17 14:11:49 +08:00
David Nguyen
5f3152b7db Merge branch 'main' into feat/document-auth 2024-03-17 13:58:37 +08:00
David Nguyen
dc8d8433dd chore: update documentation 2024-03-17 13:54:14 +08:00
David Nguyen
6781ff137e fix: test 2024-03-16 19:00:19 +08:00
David Nguyen
fa9099bc86 chore: add more tests 2024-03-16 18:41:25 +08:00
David Nguyen
228ac90036 chore: add initial tests 2024-03-16 15:54:20 +08:00
David Nguyen
8d1b0adbb2 feat: add document auth 2024-03-15 19:12:01 +08:00
25 changed files with 109 additions and 613 deletions

View File

@@ -14,7 +14,6 @@ import { LocaleDate } from '~/components/formatter/locale-date';
import { AdminActions } from './admin-actions';
import { RecipientItem } from './recipient-item';
import { SuperDeleteDocumentDialog } from './super-delete-document-dialog';
type AdminDocumentDetailsPageProps = {
params: {
@@ -82,10 +81,6 @@ export default async function AdminDocumentDetailsPage({ params }: AdminDocument
))}
</Accordion>
</div>
<hr className="my-4" />
{document && <SuperDeleteDocumentDialog document={document} />}
</div>
);
}

View File

@@ -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>
);
};

View File

@@ -58,7 +58,6 @@ export const UsersDataTable = ({
perPage,
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedSearchString]);
const onPaginationChange = (page: number, perPage: number) => {

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
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';

View File

@@ -1,4 +1,4 @@
import type { SVGAttributes } from 'react';
import { SVGAttributes } from 'react';
export type LogoProps = SVGAttributes<SVGSVGElement>;

View File

@@ -1,9 +1,9 @@
import type { HTMLAttributes } from 'react';
import { HTMLAttributes } from 'react';
import { Globe, Lock } from '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';
type TemplateTypeIcon = {

View File

@@ -47,9 +47,12 @@ export const ViewRecoveryCodesDialog = () => {
data: recoveryCodes,
mutate,
isLoading,
isError,
error,
} = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
// error?.data?.code
const viewRecoveryCodesForm = useForm<TViewRecoveryCodesForm>({
defaultValues: {
token: '',

View File

@@ -55,8 +55,11 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
});
const isSubmitting = form.formState.isSubmitting;
const hasTwoFactorAuthentication = user.twoFactorEnabled;
const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation();
const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
trpc.profile.deleteAccount.useMutation();
const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => {
try {

View File

@@ -1,4 +1,4 @@
import type { SVGAttributes } from 'react';
import { SVGAttributes } from 'react';
export type BackgroundProps = Omit<SVGAttributes<SVGElement>, 'viewBox'>;

View File

@@ -24,7 +24,7 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
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}
</div>

View File

@@ -3,7 +3,7 @@
import * as React from 'react';
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) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;

View File

@@ -1,25 +1,13 @@
import { expect, test } from '@playwright/test';
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 { prisma } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';
import { seedUser } from '@documenso/prisma/seed/users';
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 }) => {
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}`);
// Check if document has been viewed
const { status } = await getDocumentByToken(token);
const { status } = await getDocumentByToken({ token });
expect(status).toBe(DocumentStatus.PENDING);
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();
// Check if document has been signed
const { status: completedStatus } = await getDocumentByToken(token);
const { status: completedStatus } = await getDocumentByToken({ token });
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}`);
// Check if document has been viewed
const { status } = await getDocumentByToken(token);
const { status } = await getDocumentByToken({ token });
expect(status).toBe(DocumentStatus.PENDING);
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');
// Check if document has been signed
const { status: completedStatus } = await getDocumentByToken(token);
const { status: completedStatus } = await getDocumentByToken({ token });
expect(completedStatus).toBe(DocumentStatus.COMPLETED);
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -2,7 +2,7 @@
import { nanoid } from 'nanoid';
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 { 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 { getFile } from '../../universal/upload/get-file';
import { putFile } from '../../universal/upload/put-file';
import { flattenAnnotations } from '../pdf/flatten-annotations';
import { insertFieldInPDF } from '../pdf/insert-field-in-pdf';
import { normalizeSignatureAppearances } from '../pdf/normalize-signature-appearances';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { sendCompletedEmail } from './send-completed-email';
@@ -93,10 +91,31 @@ export const sealDocument = async ({
const doc = await PDFDocument.load(pdfData);
// Normalize and flatten layers that could cause issues with the signature
normalizeSignatureAppearances(doc);
doc.getForm().flatten();
flattenAnnotations(doc);
const form = doc.getForm();
// Remove old signatures
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) {
await insertFieldInPDF(doc, field);

View File

@@ -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 }),
});
};

View File

@@ -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 } });
});
};

View File

@@ -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);
});
}
};

View File

@@ -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);
}
});
}
}
};

View File

@@ -6,13 +6,7 @@
*/
-- AlterTable
ALTER TABLE "VerificationToken" ADD COLUMN "secondaryId" TEXT;
-- 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;
ALTER TABLE "VerificationToken" ADD COLUMN "secondaryId" TEXT NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_secondaryId_key" ON "VerificationToken"("secondaryId");

View File

@@ -1,6 +1,5 @@
import {
PDFArray,
PDFDict,
PDFDocument,
PDFHexString,
PDFName,
@@ -17,7 +16,7 @@ export type AddSigningPlaceholderOptions = {
export const addSigningPlaceholder = async ({ pdf }: AddSigningPlaceholderOptions) => {
const doc = await PDFDocument.load(pdf);
const [firstPage] = doc.getPages();
const pages = doc.getPages();
const byteRange = PDFArray.withContext(doc.context);
@@ -26,71 +25,64 @@ export const addSigningPlaceholder = async ({ pdf }: AddSigningPlaceholderOption
byteRange.push(PDFName.of(BYTE_RANGE_PLACEHOLDER));
byteRange.push(PDFName.of(BYTE_RANGE_PLACEHOLDER));
const signature = doc.context.register(
const signature = doc.context.obj({
Type: 'Sig',
Filter: 'Adobe.PPKLite',
SubFilter: 'adbe.pkcs7.detached',
ByteRange: byteRange,
Contents: PDFHexString.fromText(' '.repeat(8192)),
Reason: PDFString.of('Signed by Documenso'),
M: PDFString.fromDate(new Date()),
});
const signatureRef = doc.context.register(signature);
const widget = doc.context.obj({
Type: 'Annot',
Subtype: 'Widget',
FT: 'Sig',
Rect: [0, 0, 0, 0],
V: signatureRef,
T: PDFString.of('Signature1'),
F: 4,
P: pages[0].ref,
});
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({
Type: 'Sig',
Filter: 'Adobe.PPKLite',
SubFilter: 'adbe.pkcs7.detached',
ByteRange: byteRange,
Contents: PDFHexString.fromText(' '.repeat(8192)),
Reason: PDFString.of('Signed by Documenso'),
M: PDFString.fromDate(new Date()),
SigFlags: 3,
Fields: [widgetRef],
}),
);
const widget = doc.context.register(
doc.context.obj({
Type: 'Annot',
Subtype: 'Widget',
FT: 'Sig',
Rect: [0, 0, 0, 0],
V: signature,
T: PDFString.of('Signature1'),
F: 4,
P: firstPage.ref,
AP: doc.context.obj({
N: doc.context.register(doc.context.formXObject([rectangle(0, 0, 0, 0)])),
}),
}),
);
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 }));
};

View File

@@ -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 { updateUser } from '@documenso/lib/server-only/admin/update-user';
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 { deleteUser } from '@documenso/lib/server-only/user/delete-user';
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 {
ZAdminDeleteDocumentMutationSchema,
ZAdminDeleteUserMutationSchema,
ZAdminFindDocumentsQuerySchema,
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.',
});
}
}),
});

View File

@@ -48,10 +48,3 @@ export const ZAdminDeleteUserMutationSchema = z.object({
});
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>;

View File

@@ -153,10 +153,7 @@ export const authRouter = router({
}),
createPasskeySigninOptions: procedure.mutation(async ({ ctx }) => {
const cookies = parse(ctx.req.headers.cookie ?? '');
const sessionIdToken =
cookies['__Host-next-auth.csrf-token'] || cookies['next-auth.csrf-token'];
const sessionIdToken = parse(ctx.req.headers.cookie ?? '')['next-auth.csrf-token'];
if (!sessionIdToken) {
throw new Error('Missing CSRF token');

View File

@@ -91,6 +91,11 @@
@apply border-border;
}
html,
body {
scrollbar-gutter: stable;
}
body {
@apply bg-background text-foreground;
font-feature-settings: 'rlig' 1, 'calt' 1;
@@ -125,11 +130,11 @@
background: rgb(100 116 139 / 0.5);
}
/* Custom Swagger Dark Theme */
/* Custom Swagger Dark Theme */
.swagger-dark-theme .swagger-ui {
filter: invert(88%) hue-rotate(180deg);
}
.swagger-dark-theme .swagger-ui .microlight {
filter: invert(100%) hue-rotate(180deg);
}
}