Compare commits

...

15 Commits

Author SHA1 Message Date
Catalin Pit
dc035ed08c feat: update marketing banner 2024-04-10 15:52:00 +03:00
Catalin Pit
fb2fd17ad8 feat: add myself as signer 2024-04-10 15:38:31 +03:00
Timur Ercan
6285ef2cc0 feat: building documenso part 2 (#1083)
- blog article "building documenso part 2"
2024-04-09 16:13:47 +02:00
Timur Ercan
e2987b3ef1 chore: phrasing, typos 2024-04-09 16:08:13 +02:00
Timur Ercan
d97ab04d57 Merge branch 'main' into feat/building-documenso-part-2 2024-04-09 15:56:39 +02:00
Timur Ercan
665c943d8f chore: grammarly 2024-04-09 15:54:57 +02:00
David Nguyen
8fe6533ef5 fix: document audit log field security migration (#1081)
## Description

When document audit logs were first introduced, we by default set the
`fieldSecurity` to `NONE`

Now that document auth has been added, this is causing issues since we
do not use `NONE` to define field that has no migrations required, but
rather have the `fieldSecurity` field itself be undefined.

To keeps things consistent, this migration replaces `NONE` with
undefined.

There are a few ways to approach this:
- Run a prisma migration on the JSON
- Modify the data before we pass the data to the schema in
`parseDocumentAuditLogData`
- Use `NONE` instead of undefined

If anyone thinks there's a better way to do this, please drop a comment
🙇
2024-04-09 18:48:15 +07:00
Adithya Krishna
fd170f095b chore: add @documenso/pdf-sign to the tech stack (#1084)
**Description:**

This PR adds the package to our tech stack

---------

Signed-off-by: Adithya Krishna <aadithya794@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-04-09 18:47:14 +07:00
David Nguyen
1400c335a5 fix: improve document loading ui consistency (#1082)
## Description

General UI updates

## Changes Made

- Add consistent spacing between document edit/view/log pages
- Add document status to document audit log page
- Update document loading page to reserve space for the document status
below the title
- Update the document audit log page to show full dates in the correct
locale
2024-04-09 11:31:53 +07:00
Lucas Smith
03bf16522d feat: add prefilling pdf form fields via api (#1086)
## Description

Adds the ability to prefill native PDF form fields via the API using
either normal documents or templates.

Since we won't always know when a document is uploaded and has forms we
opt to do this on creation for templates and on sending the document to
recipients in all cases. This means that for a created document it can
look a little funky since the form fields are missing the data until the
document is sent.

This should be improved in a later change but since we've scoped this to
an API only workflow for now we are less concerned with the visual
issues.

## Related Issue

N/A

## Changes Made

- Added the `formValues` field the document model
- Added a new method for finding and filling form fields based on a `key
| value` pair
- Updated the API input shapes to take the new field.

## Testing Performed

- Have created and tested a document using the API both for creation and
usage with a template.
- Have verified that the fields display as expected either during
creation or sending depending on the document type.
2024-04-08 20:55:54 +07:00
Catalin Pit
627265f016 fix: return updated doc (#1089)
## Description

Fetch the updated version of the document after sealing it and return
it. Previously, the `document.documentData.data` wasn't up to date. Now
it is.

## Related Issue

Fixes #1088.

## Testing Performed

* Added console.logs in the code to make sure it returns the proper data
* Set up a webhook and tested that the webhook receives the updated data

## Checklist

<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->

- [x] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these
changes.
- [ ] I have updated the documentation to reflect these changes, if
applicable.
- [x] I have followed the project's coding style guidelines.
- [ ] I have addressed the code review feedback from the previous
submission, if applicable.
2024-04-08 19:28:50 +07:00
Mythie
08b693ff95 feat: add prefilling pdf form fields via api 2024-04-08 17:01:11 +07:00
Timur Ercan
97ce3530e0 Merge branch 'main' into feat/building-documenso-part-2 2024-04-06 10:45:34 +02:00
Timur Ercan
33f3565715 feat: blog article building documenso part 2 2024-04-05 17:21:49 +02:00
Timur Ercan
950a697115 fix: description part 1 2024-04-05 17:21:29 +02:00
20 changed files with 385 additions and 34 deletions

View File

@@ -82,7 +82,7 @@ Contact us if you are interested in our Enterprise plan for large organizations
- [NextAuth.js](https://next-auth.js.org/) - Authentication
- [react-email](https://react.email/) - Email Templates
- [tRPC](https://trpc.io/) - API
- [Node SignPDF](https://github.com/vbuch/node-signpdf) - Digital Signature
- [@documenso/pdf-sign](https://www.npmjs.com/package/@documenso/pdf-sign) - PDF Signatures
- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs
- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation
- [Stripe](https://stripe.com/) - Payments

View File

@@ -1,6 +1,6 @@
---
title: 'Building Documenso — Part 1: Certificates'
description: In today's fast-paced world, productivity and efficiency are crucial for success, both in personal and professional endeavors. We all strive to make the most of our time and energy to achieve our goals effectively. However, it's not always easy to stay on track and maintain peak performance. In this blog post, we'll explore 10 valuable tips to help you boost productivity and efficiency in your daily life.
description: Let's take a look why you need a signing certificate and how Documenso does it.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'

View File

@@ -0,0 +1,117 @@
---
title: 'Building Documenso — Part 2: Signature Validity'
description: Is a signature valid? And what does that mean? It's a surprisingly complex question; let's take a look.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2024-04-05
tags:
- Document Signature
- Certificates
- Signing
---
<figure>
<MdxNextImage
src="/blog/eu-validate-1.png"
width= "650"
height= "650"
alt= "A report card for signature validity."
/>
<figcaption className="text-center">
If a tree does not comply with the EU trust list, does it make a sound when validating?r
</figcaption>
</figure>
> TLDR; Signatures can be valid and compliant for different signature levels, even if some validators show higher-level errors. Not all helpful security measures are mandated by law.
# A valid question
A few days ago, an early adopter brought up this question in our [Discord](https://documen.so/discord):
<figure>
<MdxNextImage
src="/blog/eu-validate-2.png"
width= "650"
height= "650"
alt= "A report card for signature validity."
/>
<figcaption className="text-center">
You can check out the validator here: [https://documen.so/eu-validator](https://documen.so/eu-validator)
</figcaption>
</figure>
For those unfamiliar with the tool, he used the validator tool of the EU's Digital Signature Service (DSS) Framework to check the signature of a document signed with Documenso. The EU provides this tool to help users and providers check the validity level of their signatures.
A short refresher from [Building Documenso — Part 1: Certificates](https://documen.so/certs):
> Documenso inserts all visual signatures into the document and then seals it using the "Documenso Inc." corporate certificate. This makes the resulting PDF document tamper-proof and guarantees it hasn't changed since signing.
Before we answer if the document was signed correctly, we need to understand what the goal was.
There are three signature levels in the European eIDAS regulation:
1. **Simple Electronic Signatures (Level 1/ SES):** This is just a visual signature or even a checkbox on a document.
2. **Advanded Electronic Signatures (Level 2/ AES)**: An actual crypographic signature (not just a seal on the whole document, but a specific signature), using a certificate linked to the identification data of the signer.
3. **Qualified Electronic Signatures (Level 3/ QES):** Same as 2. but done by a government-certified entity on certified hardware and after identifying the signer with an official ID document (e.g., passport)
> 💡 Side Note: Number 2 (AES) is how most people imagine digital signatures. But most of the market uses 1. plus a seal on the whole document under the name of the signing provider (e.g., Documenso). The signer's data is only inserted visually, not in the actual signature. Why? One of the reasons is that it's much easier, and without a readily available open source framework to draw from, it is quite tricky to build. This is something we aim to build (which many have done) and open source (which no one has done).
From the perspective of eIDAS, Documenso offers Level 1/ SES signatures since it does not adhere to all of the requirements of Level 2/ AES. This means that, technically, there is no legal need to seal the document to achieve this level of validity (at least within eIDAS). We do it anyway since it improves the level of confidence users can have in the signed document. Sealing the document, even though not legally required, is a great example of Documenso's approach to signatures. First, we aim to provide all legal requirements for a given use case. Then, we add any protection that can be added without unwarranted friction to the creation of the signature.
## Not if valid, but how valid
**Q: So, is the signature in the image valid?**
A: Yes, as an eidas Level 1 SES.
**Q: Then why does it say "Unable to build a certificate chain up to a trusted list"**
A: The certificate we use to seal the document after inserting the signatures is not on the EU Trust list.
**Q: Does that mean it is less secure?**
A: No, it means the provider (Wisekey) is not on a list maintained by the EU. The cryptographic signature is just as strong as any other
For someone who does not deal with this stuff daily, this can be hard to comprehend. Whether you use a certificate you generated yourself, one generated by a certificate authority (CA) like Wisekey, or one by another on the EU trust list (e.g., Bundesdruckerei), the cryptographic security guaranteeing that the document has not been tampered with is always the same. Many providers like Documenso, DocuSign, PandaDoc, and Digisigner all use this method for their regular plans. That means if you were to run a document signed by them through the validator above, the result would be the same[1]. The interesting question is why? Why do it like this?
## Certificate Infrastructure is broken
While there are some actual expenses involved in providing AES and QES, the blunt reality is that it's just good business to charge for them per signature, making it unsuitable for the "standard offerings"; almost no one has the resources to set this up themselves. While this initial process of becoming a QES-certified entity is really expensive, selling the certificates afterward is very lucrative. This leads to less innovation in the space and only big players providing these high-compliance services. Even certificates only used to seal documents without being QES certified are sold for a large range of prices, and they cost almost nothing to produce.
## Why Though?
**Q: Why do people buy a certificate for money and not just generate one themselves? Isn't the cryptographic security the same?**
A: Self-generated certificates are not recognized for higher-level compliance signatures like QES
**Q: So if you don't need higher-level signatures, you could just generate one yourself?**
A: Yes, you could. Since eIDAS Level 1 does not require a cert, you could use your own.
**Q: Why don't more people?**
A: One reason is that apart from the EU trust list, there are others, like the Adobe trust list. While not legally required, being on that one (like Wisekey) gives you a green checkmark in Adobe PDF, which is how most people check signature validity.
**Q: Not a question, but all of this sounds weird**
A: It is. This is one of the reasons why Documenso exists. We plan to make this easier.
**Q: How?**
A: By explaining and providing easy-to-use tools and eventually free, highly compliant signature certificates for everyone.
Eventually, we plan to start a free certificate authority called Let's Sign, named after another instituion that broke the paid certificate paradigm to the benefit of the internet: [Let's Encrypt](https://letsencrypt.org/).
As always, feel free to connect on [Twitter / X](https://twitter.com/eltimuro) (DM open) or [Discord](https://documen.so/discord) if you have any questions or comments.
Best from Hamburg\
Timur
\
\
\
[1] The signature format (e.g. PKCS7-B) will vary. It's the format what the signature inserted into the document looks like. eIDAS itself does not specifically require any given format, but the PAdES defined by the EU is mostly used by european providers.

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

View File

@@ -2,10 +2,8 @@
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import launchWeekTwoImage from '@documenso/assets/images/background-lw-2.png';
import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils';
@@ -48,16 +46,8 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
})}
>
{showProfilesAnnouncementBar && (
<div className="relative inline-flex w-full items-center justify-center overflow-hidden px-4 py-2.5">
<div className="absolute inset-0 -z-[1]">
<Image
src={launchWeekTwoImage}
className="h-full w-full object-cover"
alt="Launch Week 2"
/>
</div>
<div className="text-background text-center text-sm text-white">
<div className="relative inline-flex w-full items-center justify-center overflow-hidden bg-[#e7f3df] px-4 py-2.5">
<div className="text-foreground text-center text-sm font-medium">
Claim your documenso public profile username now!{' '}
<span className="hidden font-semibold md:inline">documenso.com/u/yourname</span>
<div className="mt-1.5 block md:ml-4 md:mt-0 md:inline-block">

View File

@@ -100,7 +100,7 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
</div>
<EditDocumentForm
className="mt-8"
className="mt-6"
initialDocument={document}
documentRootPath={documentRootPath}
isDocumentEnterprise={isDocumentEnterprise}

View File

@@ -2,6 +2,8 @@ import Link from 'next/link';
import { ChevronLeft, Loader } from 'lucide-react';
import { Skeleton } from '@documenso/ui/primitives/skeleton';
export default function Loading() {
return (
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
@@ -13,7 +15,12 @@ export default function Loading() {
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
Loading Document...
</h1>
<div className="mt-8 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
<div className="flex h-10 items-center">
<Skeleton className="my-6 h-4 w-24 rounded-2xl" />
</div>
<div className="mt-4 grid h-[80vh] max-h-[60rem] w-full grid-cols-12 gap-x-8">
<div className="dark:bg-background border-border col-span-12 rounded-xl border-2 bg-white/50 p-2 before:rounded-xl lg:col-span-6 xl:col-span-7">
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center">
<Loader className="text-documenso h-12 w-12 animate-spin" />

View File

@@ -2,16 +2,21 @@ import Link from 'next/link';
import { redirect } from 'next/navigation';
import { ChevronLeft, DownloadIcon } from 'lucide-react';
import { DateTime } from 'luxon';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import type { Recipient, Team } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { Card } from '@documenso/ui/primitives/card';
import { FRIENDLY_STATUS_MAP } from '~/components/formatter/document-status';
import {
DocumentStatus as DocumentStatusComponent,
FRIENDLY_STATUS_MAP,
} from '~/components/formatter/document-status';
import { DocumentLogsDataTable } from './document-logs-data-table';
@@ -23,6 +28,8 @@ export type DocumentLogsPageViewProps = {
};
export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageViewProps) => {
const locale = getLocale();
const { id } = params;
const documentId = Number(id);
@@ -67,15 +74,21 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
},
{
description: 'Created by',
value: document.User.name ?? document.User.email,
value: document.User.name
? `${document.User.name} (${document.User.email})`
: document.User.email,
},
{
description: 'Date created',
value: document.createdAt.toISOString(),
value: DateTime.fromJSDate(document.createdAt)
.setLocale(locale)
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
},
{
description: 'Last updated',
value: document.updatedAt.toISOString(),
value: DateTime.fromJSDate(document.updatedAt)
.setLocale(locale)
.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS),
},
{
description: 'Time zone',
@@ -90,7 +103,7 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
text = `${recipient.name} (${recipient.email})`;
}
return `${text} - ${recipient.role}`;
return `[${recipient.role}] ${text}`;
};
return (
@@ -104,9 +117,19 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
</Link>
<div className="flex flex-col justify-between sm:flex-row">
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
{document.title}
</h1>
<div>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
{document.title}
</h1>
<div className="mt-2.5 flex items-center gap-x-6">
<DocumentStatusComponent
inheritColor
status={document.status}
className="text-muted-foreground"
/>
</div>
</div>
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
<Button variant="outline" className="mr-2 w-full sm:w-auto">

View File

@@ -13,6 +13,7 @@ import { createField } from '@documenso/lib/server-only/field/create-field';
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { updateField } from '@documenso/lib/server-only/field/update-field';
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
@@ -20,6 +21,8 @@ import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/s
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { putFile } from '@documenso/lib/universal/upload/put-file';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
@@ -156,6 +159,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
title: body.title,
userId: user.id,
teamId: team?.id,
formValues: body.formValues,
documentDataId: documentData.id,
requestMetadata: extractNextApiRequestMetadata(args.req),
});
@@ -217,12 +221,37 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
recipients: body.recipients,
});
let documentDataId = document.documentDataId;
if (body.formValues) {
const pdf = await getFile(document.documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(pdf),
formValues: body.formValues,
});
const newDocumentData = await putFile({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
documentDataId = newDocumentData.id;
}
await updateDocument({
documentId: document.id,
userId: user.id,
teamId: team?.id,
data: {
title: fileName,
formValues: body.formValues,
documentData: {
connect: {
id: documentDataId,
},
},
},
});

View File

@@ -73,6 +73,7 @@ export const ZCreateDocumentMutationSchema = z.object({
redirectUrl: z.string(),
})
.partial(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
});
export type TCreateDocumentMutationSchema = z.infer<typeof ZCreateDocumentMutationSchema>;
@@ -112,6 +113,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
})
.partial()
.optional(),
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
});
export type TCreateDocumentFromTemplateMutationSchema = z.infer<

View File

@@ -14,6 +14,7 @@ export type CreateDocumentOptions = {
userId: number;
teamId?: number;
documentDataId: string;
formValues?: Record<string, string | number | boolean>;
requestMetadata?: RequestMetadata;
};
@@ -22,6 +23,7 @@ export const createDocument = async ({
title,
documentDataId,
teamId,
formValues,
requestMetadata,
}: CreateDocumentOptions) => {
const user = await prisma.user.findFirstOrThrow({
@@ -51,6 +53,7 @@ export const createDocument = async ({
documentDataId,
userId,
teamId,
formValues,
},
});

View File

@@ -153,9 +153,19 @@ export const sealDocument = async ({
await sendCompletedEmail({ documentId, requestMetadata });
}
const updatedDocument = await prisma.document.findFirstOrThrow({
where: {
id: document.id,
},
include: {
documentData: true,
Recipient: true,
},
});
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_COMPLETED,
data: document,
data: updatedDocument,
userId: document.userId,
teamId: document.teamId ?? undefined,
});

View File

@@ -17,6 +17,9 @@ import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
} from '../../constants/recipient-roles';
import { getFile } from '../../universal/upload/get-file';
import { putFile } from '../../universal/upload/put-file';
import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
export type SendDocumentOptions = {
@@ -65,6 +68,7 @@ export const sendDocument = async ({
include: {
Recipient: true,
documentMeta: true,
documentData: true,
},
});
@@ -82,6 +86,38 @@ export const sendDocument = async ({
throw new Error('Can not send completed document');
}
const { documentData } = document;
if (!documentData.data) {
throw new Error('Document data not found');
}
if (document.formValues) {
const file = await getFile(documentData);
const prefilled = await insertFormValuesInPdf({
pdf: Buffer.from(file),
formValues: document.formValues as Record<string, string | number | boolean>,
});
const newDocumentData = await putFile({
name: document.title,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(prefilled),
});
const result = await prisma.document.update({
where: {
id: document.id,
},
data: {
documentDataId: newDocumentData.id,
},
});
Object.assign(document, result);
}
await Promise.all(
document.Recipient.map(async (recipient) => {
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {

View File

@@ -0,0 +1,54 @@
import { PDFCheckBox, PDFDocument, PDFDropdown, PDFRadioGroup, PDFTextField } from 'pdf-lib';
export type InsertFormValuesInPdfOptions = {
pdf: Buffer;
formValues: Record<string, string | boolean | number>;
};
export const insertFormValuesInPdf = async ({ pdf, formValues }: InsertFormValuesInPdfOptions) => {
const doc = await PDFDocument.load(pdf);
const form = doc.getForm();
if (!form) {
return pdf;
}
for (const [key, value] of Object.entries(formValues)) {
try {
const field = form.getField(key);
if (!field) {
continue;
}
if (typeof value === 'boolean' && field instanceof PDFCheckBox) {
if (value) {
field.check();
} else {
field.uncheck();
}
}
if (field instanceof PDFTextField) {
field.setText(value.toString());
}
if (field instanceof PDFDropdown) {
field.select(value.toString());
}
if (field instanceof PDFRadioGroup) {
field.select(value.toString());
}
} catch (err) {
if (err instanceof Error) {
console.error(`Error setting value for field ${key}: ${err.message}`);
} else {
console.error(`Error setting value for field ${key}`);
}
}
}
return await doc.save().then((buf) => Buffer.from(buf));
};

View File

@@ -79,6 +79,7 @@ export const createDocumentFromTemplate = async ({
id: 'asc',
},
},
documentData: true,
},
});

View File

@@ -236,11 +236,29 @@ export const ZDocumentAuditLogEventDocumentFieldInsertedSchema = z.object({
data: z.string(),
}),
]),
fieldSecurity: z
.object({
type: ZRecipientActionAuthTypesSchema,
})
.optional(),
fieldSecurity: z.preprocess(
(input) => {
const legacyNoneSecurityType = JSON.stringify({
type: 'NONE',
});
// Replace legacy 'NONE' field security type with undefined.
if (
typeof input === 'object' &&
input !== null &&
JSON.stringify(input) === legacyNoneSecurityType
) {
return undefined;
}
return input;
},
z
.object({
type: ZRecipientActionAuthTypesSchema,
})
.optional(),
),
}),
});

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Document" ADD COLUMN "formValues" JSONB;

View File

@@ -257,6 +257,7 @@ model Document {
userId Int
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
authOptions Json?
formValues Json?
title String
status DocumentStatus @default(DRAFT)
Recipient Recipient[]

View File

@@ -5,6 +5,7 @@ import React, { useId, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { motion } from 'framer-motion';
import { InfoIcon, Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
@@ -60,6 +61,9 @@ export const AddSignersFormPartial = ({
}: AddSignersFormProps) => {
const { toast } = useToast();
const { remaining } = useLimits();
const { data: session } = useSession();
const user = session?.user;
const [selfSignerFormId, setSelfSignerFormId] = useState<string | undefined>(undefined);
const initialId = useId();
@@ -135,6 +139,20 @@ export const AddSignersFormPartial = ({
);
};
const onAddSelfSigner = () => {
const newSelfSignerId = nanoid(12);
appendSigner({
formId: newSelfSignerId,
name: user?.name ?? '',
email: user?.email ?? '',
role: RecipientRole.SIGNER,
actionAuth: undefined,
});
setSelfSignerFormId(newSelfSignerId);
};
const onAddSigner = () => {
appendSigner({
formId: nanoid(12),
@@ -158,6 +176,10 @@ export const AddSignersFormPartial = ({
return;
}
if (signer.formId === selfSignerFormId) {
setSelfSignerFormId(undefined);
}
removeSigner(index);
};
@@ -209,8 +231,13 @@ export const AddSignersFormPartial = ({
<Input
type="email"
placeholder="Email"
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
{...field}
disabled={
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
signer.formId === selfSignerFormId ||
signers[index].email === user?.email
}
onKeyDown={onKeyDown}
/>
</FormControl>
@@ -237,8 +264,13 @@ export const AddSignersFormPartial = ({
<FormControl>
<Input
placeholder="Name"
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
{...field}
disabled={
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
signer.formId === selfSignerFormId ||
signers[index].email === user?.email
}
onKeyDown={onKeyDown}
/>
</FormControl>
@@ -258,7 +290,12 @@ export const AddSignersFormPartial = ({
<Select
{...field}
onValueChange={field.onChange}
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
disabled={
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
signer.formId === selfSignerFormId ||
signers[index].email === user?.email
}
>
<SelectTrigger className="bg-background text-muted-foreground">
<SelectValue placeholder="Inherit authentication method" />
@@ -330,7 +367,12 @@ export const AddSignersFormPartial = ({
<Select
{...field}
onValueChange={field.onChange}
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
disabled={
isSubmitting ||
hasBeenSentToRecipientId(signer.nativeId) ||
signer.formId === selfSignerFormId ||
signers[index].email === user?.email
}
>
<SelectTrigger className="bg-background w-[60px]">
{/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */}
@@ -403,12 +445,28 @@ export const AddSignersFormPartial = ({
>
<Button
type="button"
className="flex-1"
disabled={isSubmitting || signers.length >= remaining.recipients}
onClick={() => onAddSigner()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add Signer
</Button>
<Button
type="button"
variant="secondary"
className="dark:bg-muted dark:hover:bg-muted/80 bg-black/5 hover:bg-black/10"
disabled={
isSubmitting ||
signers.length >= remaining.recipients ||
!!selfSignerFormId ||
signers.some((signer) => signer.email === user?.email)
}
onClick={() => onAddSelfSigner()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add myself
</Button>
{!alwaysShowAdvancedSettings && isDocumentEnterprise && (
<div className="flex flex-row items-center">