mirror of
https://github.com/documenso/documenso.git
synced 2026-07-13 14:35:47 +10:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5efd34e95 | |||
| 21cff7a727 |
@@ -65,3 +65,4 @@ When you exceed a resource limit:
|
||||
- [Authentication](/docs/developers/getting-started/authentication) - API authentication guide
|
||||
- [API Versioning](/docs/developers/api/versioning) - API version management
|
||||
- [First API Call](/docs/developers/getting-started/first-api-call) - Getting started with the API
|
||||
- [Organisation Limits](/docs/self-hosting/configuration/organisation-limits) - Admins: set per-organisation resource quotas and rate limits (the HTTP rate limit above is separate and not admin-settable)
|
||||
|
||||
@@ -510,4 +510,5 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password"
|
||||
- [Email Configuration](/docs/self-hosting/configuration/email) - Configure email delivery
|
||||
- [Storage Configuration](/docs/self-hosting/configuration/storage) - Set up S3 storage
|
||||
- [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Configure document signing
|
||||
- [Organisation Limits](/docs/self-hosting/configuration/organisation-limits) - Set per-organisation document, email, and API limits from the admin panel
|
||||
- [Troubleshooting](/docs/self-hosting/maintenance/troubleshooting) - Common configuration issues
|
||||
|
||||
@@ -29,6 +29,11 @@ description: Configure your self-hosted Documenso instance with environment vari
|
||||
description="Digital signature certificate setup."
|
||||
href="/docs/self-hosting/configuration/signing-certificate"
|
||||
/>
|
||||
<Card
|
||||
title="Organisation Limits"
|
||||
description="Set per-organisation document, email, and API limits via the admin panel."
|
||||
href="/docs/self-hosting/configuration/organisation-limits"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
## Required Configuration
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"background-jobs",
|
||||
"signing-certificate",
|
||||
"telemetry",
|
||||
"organisation-limits",
|
||||
"advanced"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: Organisation Limits
|
||||
description: View and set per-organisation document, email, and API limits on a self-hosted Documenso instance using the admin panel's subscription claims.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
Per-organisation limits — document, email, and API usage, plus feature toggles and team/member caps — are controlled by **subscription claims**. You configure them in the admin panel, not through environment variables.
|
||||
|
||||
There are three distinct kinds of limit:
|
||||
|
||||
| Limit | Caps | Admin-settable |
|
||||
| ---------------------- | ------------------------------------------------- | ----------------------- |
|
||||
| Resource quota | Documents, emails, and API requests **per month** | Yes — per claim and org |
|
||||
| Resource rate limit | The same resources over a short window (e.g. `1h`) | Yes — per claim and org |
|
||||
| Global HTTP rate limit | API requests per IP (100/min, hardcoded) | No — see [Limitations](#limitations) |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running self-hosted Documenso instance.
|
||||
- An account with the **`ADMIN`** role — an account-level role, separate from organisation and team roles. New accounts are created with the `USER` role only. Grant the first admin by adding `ADMIN` to that user's `roles` directly in the database; after that, an existing admin can grant the role to others under **Admin Panel > Users > _(user)_ > Roles > Update user**.
|
||||
|
||||
Open the admin panel at `/admin`. The sidebar sections used below are **Claims**, **Organisations**, and **Organisation Stats**.
|
||||
|
||||
## Viewing usage
|
||||
|
||||
**One organisation:** open **Admin Panel > Organisations** and select it. The **Organisation usage** section shows the current period's document, email, and API usage against its quotas.
|
||||
|
||||
**All organisations:** open **Admin Panel > Organisation Stats** to sort and filter monthly usage. Filter by **claim** and by **period** (a UTC calendar month, shown as `YYYY-MM`), and switch between **Show usage**, **Show usage with quotas**, and **Show daily averages**.
|
||||
|
||||
<Callout type="warn">
|
||||
Usage counts **attempts**, not only successful actions. A request that exceeds a quota is still counted before it is rejected, so displayed usage can read higher than the number of actions that succeeded.
|
||||
</Callout>
|
||||
|
||||
## Subscription claims
|
||||
|
||||
A subscription claim is a named bundle of limits and feature flags (for example `Free`, `Individual`, `Teams`, `Platform`, or `Enterprise`). Claims are **templates**: when an organisation is created it receives a private copy of its claim and reads from that copy afterwards. Editing a claim template therefore affects organisations created later, not existing ones — to change an existing organisation, [edit it directly](#change-limits-for-one-organisation).
|
||||
|
||||
### Claim fields
|
||||
|
||||
Under **Admin Panel > Claims** (`/admin/claims`), each claim has:
|
||||
|
||||
| Field | Controls |
|
||||
| ----------------------- | --------------------------------------------------------------------------------- |
|
||||
| **Name** | The claim's display name. |
|
||||
| **Team Count** | Teams allowed. `0` = unlimited. |
|
||||
| **Member Count** | Members allowed. `0` = unlimited. |
|
||||
| **Envelope Item Count** | Uploaded files allowed per envelope. Minimum `1`. |
|
||||
| **Recipient Count** | Recipients allowed per document. `0` = unlimited. |
|
||||
| **Feature Flags** | Feature toggles (see [Feature flags](#feature-flags)). |
|
||||
| **Limits** | Monthly quota and rate-limit windows for Documents, Emails, and API. |
|
||||
| **Email transport** | Transport the claim uses. *Default (system mailer)* uses the instance default. |
|
||||
|
||||
### Quotas and rate limits
|
||||
|
||||
The **Limits** section has a column for **Documents**, **Emails**, and **API**, each with two controls:
|
||||
|
||||
- **Monthly quota** — how many of that resource are allowed per calendar month. An **empty** field is unlimited; **`0`** blocks the resource entirely.
|
||||
- **Rate limit windows** — optional short-window caps, each a duration and a maximum. A window is a number and a unit (`s`, `m`, `h`, `d`), such as `5m`, `1h`, or `24h`, and must be unique within the resource.
|
||||
|
||||
<Callout type="warn">
|
||||
Quotas and counts use opposite conventions for "unlimited": an **empty** quota is unlimited (and `0` blocks the resource), whereas `0` in the **Team**, **Member**, and **Recipient Count** fields means unlimited.
|
||||
</Callout>
|
||||
|
||||
### Feature flags
|
||||
|
||||
The **Feature Flags** section toggles capabilities such as Unlimited documents, Branding, Hide Documenso branding, Email domains, Embed authoring, Embed signing, White label for embed authoring/signing, 21 CFR, HIPAA, Authentication portal, Allow Legacy Envelopes, Signing reminders, QES signing, and Disable emails.
|
||||
|
||||
Some flags are Enterprise features. If your license does not include one, it is marked and cannot be enabled (you can still turn it off). See [Enterprise Edition](/docs/policies/enterprise-edition).
|
||||
|
||||
### Create or edit a claim template
|
||||
|
||||
1. Go to **Admin Panel > Claims**.
|
||||
2. Select **New claim**, or select an existing claim to edit it.
|
||||
3. Set the counts, feature flags, and the **Limits** section.
|
||||
4. Save. Changes apply to organisations created afterwards, not existing ones.
|
||||
|
||||
### Change limits for one organisation
|
||||
|
||||
To change limits for an existing organisation, edit it directly rather than its claim template.
|
||||
|
||||
1. Go to **Admin Panel > Organisations** and open the organisation.
|
||||
2. Adjust its quota, rate-limit, feature-flag, or email-transport fields.
|
||||
3. Save. Changes take effect immediately.
|
||||
|
||||
The organisation also shows the **Inherited subscription claim** it was created from.
|
||||
|
||||
## Usage reset
|
||||
|
||||
Monthly quota usage is keyed to the **UTC calendar month**. There is no scheduled reset job — when the month rolls over, the new period's counter starts at `0`.
|
||||
|
||||
## Limitations
|
||||
|
||||
The **global HTTP rate limit is not configurable.** Documenso enforces a hardcoded **100 requests per minute per IP address** on its API endpoint groups (`/api/v1`, `/api/v2`, and the tRPC API are limited separately), returning `429 Too Many Requests`. It is a per-IP safeguard applied at the HTTP layer — not per-organisation, not stored on any claim, and not adjustable from the admin panel. See [Rate Limits](/docs/developers/api/rate-limits).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause and fix |
|
||||
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| An organisation hit its limit unexpectedly | Usage counts rejected over-quota attempts. Compare usage against the quota under **Organisation Stats > Show usage with quotas**. |
|
||||
| A resource is blocked entirely, not just capped | The **Monthly quota** is `0`, which blocks the resource. Leave it empty for unlimited. |
|
||||
| Emails are not sending for an organisation | Check whether the **Disable emails** flag is enabled on the organisation's claim — it blocks all emails regardless of quota. |
|
||||
| A claim template edit had no effect | Template edits are not retroactive. Edit the organisation directly under **Admin Panel > Organisations**. |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Environment Variables](/docs/self-hosting/configuration/environment) - All configuration options
|
||||
- [Rate Limits](/docs/developers/api/rate-limits) - The global HTTP API rate limit (separate from claims)
|
||||
- [Enterprise Edition](/docs/policies/enterprise-edition) - Features unlocked by license flags
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { AlertTriangleIcon } from 'lucide-react';
|
||||
|
||||
export const DirectTemplateInvalidPageView = () => {
|
||||
return (
|
||||
<div className="mx-auto flex h-[70vh] w-full max-w-md flex-col items-center justify-center">
|
||||
<div>
|
||||
<AlertTriangleIcon className="h-10 w-10 text-destructive" />
|
||||
|
||||
<h1 className="mt-4 font-semibold text-3xl">
|
||||
<Trans>Invalid direct link template</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="mt-2 text-muted-foreground text-sm">
|
||||
<Trans>
|
||||
This direct link template cannot be used because one or more signers do not have a signature field assigned.
|
||||
Please contact the sender to update the template.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -54,7 +54,6 @@ import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop';
|
||||
import { EnvelopeEditorFieldsPageRenderer } from './envelope-editor-fields-page-renderer';
|
||||
import { EnvelopeEditorInvalidDirectTemplateAlert } from './envelope-editor-invalid-direct-template-alert';
|
||||
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
||||
import { EnvelopeRecipientSelector } from './envelope-recipient-selector';
|
||||
|
||||
@@ -239,8 +238,6 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<EnvelopeEditorInvalidDirectTemplateAlert />
|
||||
|
||||
{/* Document View */}
|
||||
<div className="mt-4 flex h-full flex-col items-center justify-center">
|
||||
{envelope.recipients.length === 0 && (
|
||||
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||
import { getRecipientsWithMissingFields } from '@documenso/lib/utils/recipients';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export type EnvelopeEditorInvalidDirectTemplateAlertProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Warns that a direct link template cannot be used because one or more signers
|
||||
* are missing a signature field.
|
||||
*/
|
||||
export const EnvelopeEditorInvalidDirectTemplateAlert = ({
|
||||
className,
|
||||
}: EnvelopeEditorInvalidDirectTemplateAlertProps) => {
|
||||
const { envelope, isTemplate } = useCurrentEnvelopeEditor();
|
||||
|
||||
const signersMissingSignatureFields = useMemo(() => {
|
||||
if (!isTemplate || !envelope.directLink?.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getRecipientsWithMissingFields(envelope.recipients, envelope.fields);
|
||||
}, [isTemplate, envelope.directLink, envelope.recipients, envelope.fields]);
|
||||
|
||||
if (signersMissingSignatureFields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
variant="destructive"
|
||||
className={cn('mx-auto w-full max-w-[800px] flex-row items-start gap-3 rounded-sm', className)}
|
||||
>
|
||||
<AlertTitle>
|
||||
<Trans>Invalid direct link template</Trans>
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
Recipients cannot use this direct link template because the following signers are missing a signature field
|
||||
</Trans>
|
||||
|
||||
<ul className="list-disc pl-5">
|
||||
{signersMissingSignatureFields.map((recipient, i) => (
|
||||
<li key={recipient.id}>{recipient.email || recipient.name || `Recipient ${i + 1}`}</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@@ -22,7 +22,6 @@ import { match } from 'ts-pattern';
|
||||
import { EnvelopeGenericPageRenderer } from '~/components/general/envelope-editor/envelope-generic-page-renderer';
|
||||
import { EnvelopePdfViewer } from '~/components/general/pdf-viewer/envelope-pdf-viewer';
|
||||
|
||||
import { EnvelopeEditorInvalidDirectTemplateAlert } from './envelope-editor-invalid-direct-template-alert';
|
||||
import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
||||
|
||||
export const EnvelopeEditorPreviewPage = () => {
|
||||
@@ -229,8 +228,6 @@ export const EnvelopeEditorPreviewPage = () => {
|
||||
{/* Horizontal envelope item selector */}
|
||||
<EnvelopeRendererFileSelector className="px-0" fields={editorFields.localFields} />
|
||||
|
||||
<EnvelopeEditorInvalidDirectTemplateAlert className="mb-4" />
|
||||
|
||||
<Alert variant="warning" className="mx-auto max-w-[800px]">
|
||||
<AlertTitle>
|
||||
<Trans>Preview Mode</Trans>
|
||||
|
||||
@@ -26,7 +26,6 @@ import { ErrorCode as DropzoneErrorCode, type FileRejection, useDropzone } from
|
||||
|
||||
import { EnvelopeItemDeleteDialog } from '~/components/dialogs/envelope-item-delete-dialog';
|
||||
|
||||
import { EnvelopeEditorInvalidDirectTemplateAlert } from './envelope-editor-invalid-direct-template-alert';
|
||||
import { EnvelopeEditorRecipientForm } from './envelope-editor-recipient-form';
|
||||
import { EnvelopeItemTitleInput } from './envelope-editor-title-input';
|
||||
|
||||
@@ -450,9 +449,6 @@ export const EnvelopeEditorUploadPage = () => {
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl space-y-6 p-8">
|
||||
<input {...getReplaceInputProps()} />
|
||||
|
||||
<EnvelopeEditorInvalidDirectTemplateAlert className="max-w-none" />
|
||||
|
||||
<Card backdropBlur={false} className="border">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { getEnvelopeForDirectTemplateSigning } from '@documenso/lib/server-only/
|
||||
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
|
||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { getRecipientsWithMissingFields } from '@documenso/lib/utils/recipients';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { Plural } from '@lingui/react/macro';
|
||||
import { UsersIcon } from 'lucide-react';
|
||||
@@ -15,7 +14,6 @@ import { redirect } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { Header as AuthenticatedHeader } from '~/components/general/app-header';
|
||||
import { DirectTemplateInvalidPageView } from '~/components/general/direct-template/direct-template-invalid-page';
|
||||
import { DirectTemplatePageView } from '~/components/general/direct-template/direct-template-page';
|
||||
import { DirectTemplateAuthPageView } from '~/components/general/direct-template/direct-template-signing-auth-page';
|
||||
import { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page';
|
||||
@@ -72,18 +70,8 @@ const handleV1Loader = async ({ params, request }: Route.LoaderArgs) => {
|
||||
};
|
||||
}
|
||||
|
||||
const recipientsWithMissingFields = getRecipientsWithMissingFields(template.recipients, template.fields);
|
||||
|
||||
if (recipientsWithMissingFields.length > 0) {
|
||||
return {
|
||||
isAccessAuthValid: true,
|
||||
isTemplateMissingSignatures: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
isAccessAuthValid: true,
|
||||
isTemplateMissingSignatures: false,
|
||||
template: {
|
||||
...template,
|
||||
folder: null,
|
||||
@@ -108,7 +96,6 @@ const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
|
||||
.then((envelopeForSigning) => {
|
||||
return {
|
||||
isDocumentAccessValid: true,
|
||||
isTemplateMissingSignatures: false,
|
||||
envelopeForSigning,
|
||||
} as const;
|
||||
})
|
||||
@@ -121,13 +108,6 @@ const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (error.code === AppErrorCode.MISSING_SIGNATURE_FIELD) {
|
||||
return {
|
||||
isDocumentAccessValid: true,
|
||||
isTemplateMissingSignatures: true,
|
||||
} as const;
|
||||
}
|
||||
|
||||
throw new Response('Not Found', { status: 404 });
|
||||
});
|
||||
};
|
||||
@@ -201,10 +181,6 @@ const DirectSigningPageV1 = ({ data }: { data: Awaited<ReturnType<typeof handleV
|
||||
return <DirectTemplateAuthPageView />;
|
||||
}
|
||||
|
||||
if (data.isTemplateMissingSignatures) {
|
||||
return <DirectTemplateInvalidPageView />;
|
||||
}
|
||||
|
||||
const { template, directTemplateRecipient } = data;
|
||||
|
||||
return (
|
||||
@@ -259,10 +235,6 @@ const DirectSigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV
|
||||
return <DocumentSigningAuthPageView email={''} emailHasAccount={true} />;
|
||||
}
|
||||
|
||||
if (data.isTemplateMissingSignatures) {
|
||||
return <DirectTemplateInvalidPageView />;
|
||||
}
|
||||
|
||||
const { envelope, recipient } = data.envelopeForSigning;
|
||||
|
||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||
|
||||
@@ -32,10 +32,6 @@ export const getDirectTemplateErrorMessage = (code: string): ToastMessageDescrip
|
||||
return match(code)
|
||||
.with('RECIPIENT_LIMIT_EXCEEDED', () => RECIPIENT_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||
.with(AppErrorCode.TOO_MANY_REQUESTS, () => FAIR_USE_LIMIT_EXCEEDED_ERROR_MESSAGE)
|
||||
.with(AppErrorCode.MISSING_SIGNATURE_FIELD, () => ({
|
||||
title: msg`Missing signature fields`,
|
||||
description: msg`This direct link template cannot be used because one or more signers do not have a signature field assigned.`,
|
||||
}))
|
||||
.otherwise(() => ({
|
||||
title: msg`Something went wrong`,
|
||||
description: msg`We were unable to submit this document at this time. Please try again later.`,
|
||||
@@ -81,10 +77,6 @@ export const getTemplateUseErrorMessage = (code: string): ToastMessageDescriptor
|
||||
title: msg`Error`,
|
||||
description: msg`The document was created but could not be sent to recipients.`,
|
||||
}))
|
||||
.with(AppErrorCode.MISSING_SIGNATURE_FIELD, () => ({
|
||||
title: msg`Missing signature fields`,
|
||||
description: msg`The document could not be sent because some signers do not have a signature field. Please edit the template and add a signature field for each signer.`,
|
||||
}))
|
||||
.with(AppErrorCode.INVALID_BODY, AppErrorCode.INVALID_REQUEST, () => ({
|
||||
title: msg`Error`,
|
||||
description: msg`The document could not be created because of missing or invalid information. Please review the template's recipients and fields.`,
|
||||
|
||||
@@ -106,5 +106,5 @@
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "2.14.0"
|
||||
"version": "2.15.0"
|
||||
}
|
||||
|
||||
Generated
+3
-123
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "2.14.0",
|
||||
"version": "2.15.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "2.14.0",
|
||||
"version": "2.15.0",
|
||||
"hasInstallScript": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
@@ -406,7 +406,7 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "2.14.0",
|
||||
"version": "2.15.0",
|
||||
"dependencies": {
|
||||
"@cantoo/pdf-lib": "^2.5.3",
|
||||
"@documenso/api": "*",
|
||||
@@ -31817,126 +31817,6 @@
|
||||
"react": "^18",
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz",
|
||||
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz",
|
||||
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz",
|
||||
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz",
|
||||
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz",
|
||||
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz",
|
||||
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz",
|
||||
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz",
|
||||
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"version": "2.14.0",
|
||||
"version": "2.15.0",
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"build": "turbo run build",
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { seedDirectTemplate } from '@documenso/prisma/seed/templates';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import { expect, type Page, test } from '@playwright/test';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import { clickEnvelopeEditorStep } from '../fixtures/envelope-editor';
|
||||
|
||||
const INVALID_DIRECT_TEMPLATE_ALERT_TITLE = 'Invalid direct link template';
|
||||
|
||||
/**
|
||||
* Place a field on the PDF canvas in the envelope editor.
|
||||
*/
|
||||
const placeFieldOnPdf = async (root: Page, fieldName: 'Signature' | 'Text', position: { x: number; y: number }) => {
|
||||
await root.getByRole('button', { name: fieldName, exact: true }).click();
|
||||
|
||||
const canvas = root.locator('.konva-container canvas').first();
|
||||
await expect(canvas).toBeVisible();
|
||||
await canvas.click({ position });
|
||||
};
|
||||
|
||||
/**
|
||||
* Seed a V2 direct template and open it in the native template editor.
|
||||
*
|
||||
* Only the native template editor is covered here: direct links only exist
|
||||
* for templates and are not part of the embedded editor surfaces.
|
||||
*/
|
||||
const openDirectTemplateEditor = async (page: Page, options: { createDirectRecipientSignatureField: boolean }) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const template = await seedDirectTemplate({
|
||||
title: `E2E Direct Template Validation ${Date.now()}`,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
internalVersion: 2,
|
||||
createDirectRecipientSignatureField: options.createDirectRecipientSignatureField,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
||||
});
|
||||
|
||||
return { user, team, template };
|
||||
};
|
||||
|
||||
test.describe('template editor', () => {
|
||||
test('shows invalid direct template warning when a signer has no signature field', async ({ page }) => {
|
||||
await openDirectTemplateEditor(page, { createDirectRecipientSignatureField: false });
|
||||
|
||||
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).toBeVisible();
|
||||
await expect(page.getByText('are missing a signature field')).toBeVisible();
|
||||
});
|
||||
|
||||
test('does not show the warning when all signers have signature fields', async ({ page }) => {
|
||||
await openDirectTemplateEditor(page, { createDirectRecipientSignatureField: true });
|
||||
|
||||
// Wait for the editor to render before asserting the banner is absent.
|
||||
await expect(page.getByTestId('envelope-editor-step-upload')).toBeVisible();
|
||||
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('warning disappears after placing a signature field', async ({ page }) => {
|
||||
await openDirectTemplateEditor(page, { createDirectRecipientSignatureField: false });
|
||||
|
||||
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).toBeVisible();
|
||||
|
||||
// Place a signature field for the direct recipient (auto-selected single recipient).
|
||||
await clickEnvelopeEditorStep(page, 'addFields');
|
||||
await expect(page.locator('.konva-container canvas').first()).toBeVisible();
|
||||
await placeFieldOnPdf(page, 'Signature', { x: 120, y: 140 });
|
||||
|
||||
// The banner clears once the field is autosaved and the envelope state updates.
|
||||
await expect(page.getByText(INVALID_DIRECT_TEMPLATE_ALERT_TITLE)).not.toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,6 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||
import { signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
@@ -74,19 +73,8 @@ test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
||||
await expect(page.locator('body')).toContainText('public-direct-template-title');
|
||||
await expect(page.locator('body')).toContainText('public-direct-template-description');
|
||||
|
||||
const directSignatureField = directTemplate.fields[0];
|
||||
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
}
|
||||
|
||||
await page.getByRole('link', { name: 'Sign' }).click();
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await signSignaturePad(page);
|
||||
await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
|
||||
await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
|
||||
|
||||
@@ -197,18 +197,7 @@ test('[DIRECT_TEMPLATES]: V1 direct template link auth access', async ({ page })
|
||||
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
|
||||
await expect(page.getByLabel('Email')).toBeDisabled();
|
||||
|
||||
const directSignatureField = directTemplateWithAuth.fields[0];
|
||||
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await signSignaturePad(page);
|
||||
await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
|
||||
await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
@@ -246,37 +235,6 @@ test('[DIRECT_TEMPLATES]: V2 direct template link auth access', async ({ page })
|
||||
await page.goto(directTemplatePath);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Personal direct template link' })).toBeVisible();
|
||||
|
||||
const directSignatureField = directTemplateWithAuth.fields[0];
|
||||
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
}
|
||||
|
||||
// Wait for the PDF and the Konva canvas overlay to be ready.
|
||||
await expect(page.locator('img[data-page-number]').first()).toBeVisible({ timeout: 30_000 });
|
||||
const canvas = page.locator('.konva-container canvas').first();
|
||||
await expect(canvas).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// Sign the direct template recipient's signature field via the canvas-based V2 UI.
|
||||
await signSignaturePad(page);
|
||||
|
||||
const canvasBox = await canvas.boundingBox();
|
||||
|
||||
if (!canvasBox) {
|
||||
throw new Error('Canvas bounding box not found');
|
||||
}
|
||||
|
||||
const x =
|
||||
(Number(directSignatureField.positionX) / 100) * canvasBox.width +
|
||||
((Number(directSignatureField.width) / 100) * canvasBox.width) / 2;
|
||||
const y =
|
||||
(Number(directSignatureField.positionY) / 100) * canvasBox.height +
|
||||
((Number(directSignatureField.height) / 100) * canvasBox.height) / 2;
|
||||
|
||||
await canvas.click({ position: { x, y } });
|
||||
await expect(page.getByText('0 Fields Remaining').first()).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await expect(page.getByLabel('Your Email')).not.toBeVisible();
|
||||
|
||||
@@ -308,16 +266,6 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
|
||||
|
||||
await expect(page.getByText('Next Recipient Name')).not.toBeVisible();
|
||||
|
||||
const directSignatureField = template.fields[0];
|
||||
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
}
|
||||
|
||||
await signSignaturePad(page);
|
||||
await page.locator(`#field-${directSignatureField.id}`).getByRole('button').click();
|
||||
await expect(page.locator(`#field-${directSignatureField.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(/\/sign/);
|
||||
@@ -351,13 +299,19 @@ test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with nex
|
||||
},
|
||||
});
|
||||
|
||||
// The seeded direct template already includes a signature field for the direct recipient.
|
||||
const directSignatureField = template.fields[0];
|
||||
const directTemplateRecipient = template.recipients[0];
|
||||
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
if (!directTemplateRecipient) {
|
||||
throw new Error('Expected direct template recipient to exist');
|
||||
}
|
||||
|
||||
// All SIGNER recipients need a signature field for sendDocument to dispatch emails.
|
||||
const directSignatureField = await seedSignatureFieldForRecipient({
|
||||
envelopeId: template.id,
|
||||
recipientId: directTemplateRecipient.id,
|
||||
positionY: 10,
|
||||
});
|
||||
|
||||
const originalName = 'Signer 2';
|
||||
const originalSecondSignerEmail = seedTestEmail();
|
||||
|
||||
@@ -459,13 +413,19 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex
|
||||
},
|
||||
});
|
||||
|
||||
// The seeded direct template already includes a signature field for the direct recipient.
|
||||
const directSignatureField = template.fields[0];
|
||||
const directTemplateRecipient = template.recipients[0];
|
||||
|
||||
if (!directSignatureField) {
|
||||
throw new Error('Expected seeded direct template signature field to exist');
|
||||
if (!directTemplateRecipient) {
|
||||
throw new Error('Expected direct template recipient to exist');
|
||||
}
|
||||
|
||||
// All SIGNER recipients need a signature field for sendDocument to dispatch emails.
|
||||
const directSignatureField = await seedSignatureFieldForRecipient({
|
||||
envelopeId: template.id,
|
||||
recipientId: directTemplateRecipient.id,
|
||||
positionY: 10,
|
||||
});
|
||||
|
||||
const originalName = 'Signer 2';
|
||||
const originalSecondSignerEmail = seedTestEmail();
|
||||
|
||||
@@ -561,48 +521,3 @@ test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with nex
|
||||
expect(updatedSecondRecipient.email).toBe(newSecondSignerEmail);
|
||||
await expectSigningRequestJobForRecipient(updatedSecondRecipient.id);
|
||||
});
|
||||
|
||||
test('[DIRECT_TEMPLATES]: V1 direct template without signature fields shows invalid template page', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const template = await seedDirectTemplate({
|
||||
title: 'V1 invalid direct template',
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
createDirectRecipientSignatureField: false,
|
||||
});
|
||||
|
||||
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Invalid direct link template' })).toBeVisible();
|
||||
await expect(page.getByText('This direct link template cannot be used because one or more signers')).toBeVisible();
|
||||
|
||||
// The signing flow must not render.
|
||||
await expect(page.getByRole('heading', { name: 'General' })).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Continue' })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('[DIRECT_TEMPLATES]: V2 direct template without signature fields shows invalid template page', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const template = await seedDirectTemplate({
|
||||
title: 'V2 invalid direct template',
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
internalVersion: 2,
|
||||
createDirectRecipientSignatureField: false,
|
||||
});
|
||||
|
||||
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Invalid direct link template' })).toBeVisible();
|
||||
await expect(page.getByText('This direct link template cannot be used because one or more signers')).toBeVisible();
|
||||
|
||||
// The signing flow (PDF canvas) must not render.
|
||||
await expect(page.locator('.konva-container canvas')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Complete' })).not.toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import { FIELD_SIGNATURE_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedTemplate } from '@documenso/prisma/seed/templates';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { DocumentStatus, FieldType } from '@prisma/client';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||
|
||||
const seedSignatureFieldForRecipient = async (options: { envelopeId: string; recipientId: number }) => {
|
||||
const envelopeItem = await prisma.envelopeItem.findFirstOrThrow({
|
||||
where: { envelopeId: options.envelopeId },
|
||||
});
|
||||
|
||||
return await prisma.field.create({
|
||||
data: {
|
||||
envelopeId: options.envelopeId,
|
||||
envelopeItemId: envelopeItem.id,
|
||||
recipientId: options.recipientId,
|
||||
type: FieldType.SIGNATURE,
|
||||
page: 1,
|
||||
positionX: 5,
|
||||
positionY: 10,
|
||||
width: 20,
|
||||
height: 5,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: FIELD_SIGNATURE_META_DEFAULT_VALUES,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
test('[TEMPLATE_USE]: shows missing signature fields error when sending a template without signature fields', async ({
|
||||
page,
|
||||
}) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
// seedTemplate creates one SIGNER recipient and no fields.
|
||||
await seedTemplate({
|
||||
title: 'Template missing signature fields',
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/templates`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Create document from template' })).toBeVisible();
|
||||
|
||||
// Enable distribution so the document is sent on creation.
|
||||
await page.locator('#distributeDocument').click();
|
||||
await page.getByRole('button', { name: 'Create and send' }).click();
|
||||
|
||||
await expectToastTextToBeVisible(page, 'Missing signature fields');
|
||||
await expectToastTextToBeVisible(
|
||||
page,
|
||||
'The document could not be sent because some signers do not have a signature field',
|
||||
);
|
||||
});
|
||||
|
||||
test('[TEMPLATE_USE]: creates and sends a document when signers have signature fields', async ({ page }) => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
const template = await seedTemplate({
|
||||
title: 'Template with signature fields',
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await seedSignatureFieldForRecipient({
|
||||
envelopeId: template.id,
|
||||
recipientId: template.recipients[0].id,
|
||||
});
|
||||
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/templates`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Create document from template' })).toBeVisible();
|
||||
|
||||
await page.locator('#distributeDocument').click();
|
||||
await page.getByRole('button', { name: 'Create and send' }).click();
|
||||
|
||||
await page.waitForURL(new RegExp(`/t/${team.url}/documents/envelope_.*`));
|
||||
|
||||
const envelopeId = page.url().split('/').pop()?.split('?')[0];
|
||||
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: { id: envelopeId },
|
||||
});
|
||||
|
||||
expect(envelope.status).toBe(DocumentStatus.PENDING);
|
||||
});
|
||||
@@ -36,13 +36,6 @@ export enum AppErrorCode {
|
||||
*/
|
||||
ENVELOPE_TSP_LOCKED = 'ENVELOPE_TSP_LOCKED',
|
||||
|
||||
/**
|
||||
* A signer recipient does not have a signature field assigned. Thrown when
|
||||
* distributing an envelope or using a direct template where at least one
|
||||
* signer has no signature field.
|
||||
*/
|
||||
MISSING_SIGNATURE_FIELD = 'MISSING_SIGNATURE_FIELD',
|
||||
|
||||
/**
|
||||
* CSC (Cloud Signature Consortium) error codes. See the CSC QES V1 spec
|
||||
* for the recovery taxonomy.
|
||||
@@ -91,7 +84,6 @@ export const genericErrorCodeToTrpcErrorCodeMap: Record<string, { code: string;
|
||||
[AppErrorCode.ENVELOPE_CANCELLED]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.ENVELOPE_LEGACY]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.ENVELOPE_TSP_LOCKED]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.MISSING_SIGNATURE_FIELD]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.CSC_INSTANCE_MODE_MISMATCH]: { code: 'BAD_REQUEST', status: 400 },
|
||||
[AppErrorCode.CSC_UNLICENSED]: { code: 'FORBIDDEN', status: 403 },
|
||||
[AppErrorCode.CSC_PROVIDER_INFO_FAILED]: { code: 'INTERNAL_SERVER_ERROR', status: 500 },
|
||||
@@ -299,7 +291,6 @@ export class AppError extends Error {
|
||||
AppErrorCode.ENVELOPE_CANCELLED,
|
||||
AppErrorCode.ENVELOPE_LEGACY,
|
||||
AppErrorCode.ENVELOPE_TSP_LOCKED,
|
||||
AppErrorCode.MISSING_SIGNATURE_FIELD,
|
||||
AppErrorCode.CSC_INSTANCE_MODE_MISMATCH,
|
||||
AppErrorCode.CSC_CREDENTIAL_LIST_EMPTY,
|
||||
AppErrorCode.CSC_CERT_INVALID,
|
||||
|
||||
@@ -194,7 +194,7 @@ export const sendDocument = async ({ id, userId, teamId, sendEmail, requestMetad
|
||||
.map((r) => (r.name ? `${r.name} (${r.email}, id: ${r.id})` : `${r.email} (id: ${r.id})`))
|
||||
.join(', ');
|
||||
|
||||
throw new AppError(AppErrorCode.MISSING_SIGNATURE_FIELD, {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `The following recipients are missing required fields: ${missingRecipientDescriptions}. Signers must have at least one signature field.`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { match } from 'ts-pattern';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { getRecipientsWithMissingFields } from '../../utils/recipients';
|
||||
import { extractFieldAutoInsertValues } from '../document/send-document';
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
|
||||
@@ -126,17 +125,6 @@ export const getEnvelopeForDirectTemplateSigning = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const recipientsWithMissingFields = getRecipientsWithMissingFields(
|
||||
envelope.recipients,
|
||||
envelope.recipients.flatMap((envelopeRecipient) => envelopeRecipient.fields),
|
||||
);
|
||||
|
||||
if (recipientsWithMissingFields.length > 0) {
|
||||
throw new AppError(AppErrorCode.MISSING_SIGNATURE_FIELD, {
|
||||
message: 'One or more signers on this direct template are missing a signature field',
|
||||
});
|
||||
}
|
||||
|
||||
const settings = await getTeamSettings({ teamId: envelope.teamId });
|
||||
|
||||
const sender = settings.includeSenderDetails
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
extractDocumentAuthMethods,
|
||||
} from '../../utils/document-auth';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { getRecipientsWithMissingFields } from '../../utils/recipients';
|
||||
import { sendDocument } from '../document/send-document';
|
||||
import { validateFieldAuth } from '../document/validate-field-auth';
|
||||
import { incrementDocumentId } from '../envelope/increment-id';
|
||||
@@ -173,17 +172,6 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const recipientsWithMissingFields = getRecipientsWithMissingFields(
|
||||
recipients,
|
||||
recipients.flatMap((recipient) => recipient.fields),
|
||||
);
|
||||
|
||||
if (recipientsWithMissingFields.length > 0) {
|
||||
throw new AppError(AppErrorCode.MISSING_SIGNATURE_FIELD, {
|
||||
message: 'One or more signers on this direct template are missing a signature field',
|
||||
});
|
||||
}
|
||||
|
||||
if (directTemplateEnvelope.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' });
|
||||
}
|
||||
|
||||
@@ -361,9 +361,9 @@ export const seedAlignmentTestDocument = async ({
|
||||
const { id, recipients, envelopeItems } = createdEnvelope;
|
||||
|
||||
if (isDirectTemplate) {
|
||||
const directTemplateRecipient = recipients.find((recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL);
|
||||
const directTemplateRecpient = recipients.find((recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL);
|
||||
|
||||
if (!directTemplateRecipient) {
|
||||
if (!directTemplateRecpient) {
|
||||
throw new Error('Need to create a direct template recipient');
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ export const seedAlignmentTestDocument = async ({
|
||||
envelopeId: id,
|
||||
enabled: true,
|
||||
token: directTemplateToken ?? Math.random().toString(),
|
||||
directTemplateRecipientId: directTemplateRecipient.id,
|
||||
directTemplateRecipientId: directTemplateRecpient.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
DIRECT_TEMPLATE_RECIPIENT_NAME,
|
||||
} from '@documenso/lib/constants/direct-templates';
|
||||
import { incrementTemplateId } from '@documenso/lib/server-only/envelope/increment-id';
|
||||
import { FIELD_SIGNATURE_META_DEFAULT_VALUES } from '@documenso/lib/types/field-meta';
|
||||
import { SignatureLevel } from '@documenso/lib/types/signature-level';
|
||||
import { prefixedId } from '@documenso/lib/universal/id';
|
||||
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
DocumentDataType,
|
||||
DocumentSource,
|
||||
EnvelopeType,
|
||||
FieldType,
|
||||
ReadStatus,
|
||||
RecipientRole,
|
||||
SendStatus,
|
||||
@@ -31,11 +29,6 @@ type SeedTemplateOptions = {
|
||||
teamId: number;
|
||||
internalVersion?: 1 | 2;
|
||||
createTemplateOptions?: Partial<Prisma.EnvelopeUncheckedCreateInput>;
|
||||
/**
|
||||
* Only used by seedDirectTemplate. Creates a signature field for the direct
|
||||
* recipient so the seeded direct template is valid. Defaults to true.
|
||||
*/
|
||||
createDirectRecipientSignatureField?: boolean;
|
||||
};
|
||||
|
||||
type CreateTemplateOptions = {
|
||||
@@ -205,11 +198,11 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => {
|
||||
},
|
||||
});
|
||||
|
||||
const directTemplateRecipient = template.recipients.find(
|
||||
const directTemplateRecpient = template.recipients.find(
|
||||
(recipient) => recipient.email === DIRECT_TEMPLATE_RECIPIENT_EMAIL,
|
||||
);
|
||||
|
||||
if (!directTemplateRecipient) {
|
||||
if (!directTemplateRecpient) {
|
||||
throw new Error('Need to create a direct template recipient');
|
||||
}
|
||||
|
||||
@@ -218,35 +211,10 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => {
|
||||
envelopeId: template.id,
|
||||
enabled: true,
|
||||
token: Math.random().toString(),
|
||||
directTemplateRecipientId: directTemplateRecipient.id,
|
||||
directTemplateRecipientId: directTemplateRecpient.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { createDirectRecipientSignatureField = true } = options;
|
||||
|
||||
if (createDirectRecipientSignatureField) {
|
||||
const envelopeItem = await prisma.envelopeItem.findFirstOrThrow({
|
||||
where: { envelopeId: template.id },
|
||||
});
|
||||
|
||||
await prisma.field.create({
|
||||
data: {
|
||||
envelopeId: template.id,
|
||||
envelopeItemId: envelopeItem.id,
|
||||
recipientId: directTemplateRecipient.id,
|
||||
type: FieldType.SIGNATURE,
|
||||
page: 1,
|
||||
positionX: 5,
|
||||
positionY: 10,
|
||||
width: 20,
|
||||
height: 5,
|
||||
customText: '',
|
||||
inserted: false,
|
||||
fieldMeta: FIELD_SIGNATURE_META_DEFAULT_VALUES,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
id: template.id,
|
||||
|
||||
Reference in New Issue
Block a user