mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 00:43:40 +10:00
Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c02dfaba1a | |||
| 54befb5962 | |||
| 26f0c4c5b7 | |||
| 7f85388eb7 | |||
| 3cf2963cd0 | |||
| cc5ef3df16 | |||
| 4b72e7d546 | |||
| ba0dead96f | |||
| 40472bc26c | |||
| 3ff7f70a7d | |||
| 5c41740859 | |||
| d6268b1d7d | |||
| 12223c79cb | |||
| b16f979eb3 | |||
| db031e2865 | |||
| 4e0038f2e8 | |||
| c5efd34e95 | |||
| 21cff7a727 | |||
| 400b6a24f1 | |||
| 1b1e3d197b | |||
| a276e18e1f | |||
| 50f272be87 | |||
| a55e6d9484 | |||
| d35d13db23 | |||
| 337f85f021 | |||
| 2332b0316b | |||
| 393b51d484 | |||
| 5a8335e0eb | |||
| 562d78e2d7 | |||
| 3b110cf70d | |||
| 7062fadf0b | |||
| 9cdd2e7ff9 | |||
| a70b0702c3 | |||
| 1f170ef5e5 | |||
| 8f68393241 | |||
| 381293af0c | |||
| 97835b8dbb | |||
| 977d07330b | |||
| 037170f625 | |||
| c219305eb1 | |||
| 96ab78c33f | |||
| 241929bb97 | |||
| 94adea149d | |||
| 9c5eb43a26 | |||
| e0ef11e8c3 | |||
| 187b612568 | |||
| b37529a1cf | |||
| 04f6e76178 | |||
| f2525ae95b | |||
| 2f24a8eab2 | |||
| d9b7722325 | |||
| 783123f72b | |||
| e8ed1c3d99 | |||
| c23d739f76 | |||
| 0bf58ca66e | |||
| dee3259088 | |||
| 6ad1a2dfaf | |||
| 306e7fe5ed | |||
| 219db32fdf | |||
| 948d1bbf12 | |||
| 40d20ad068 | |||
| a99bdf5e20 | |||
| 4f346d3c2d | |||
| d5ce222482 | |||
| 9b59f1a273 | |||
| 15549a6758 | |||
| eb45d1e5a9 | |||
| 0aa84cecc8 | |||
| 702e747375 | |||
| 3887aa67c8 | |||
| b84b87cea6 | |||
| ac0a0086d6 | |||
| 8c11266747 | |||
| 3c0345f755 | |||
| 3e47f1913c | |||
| 9ccf50ed95 | |||
| ecc98fbd41 | |||
| 58f0f5da43 | |||
| d5c6cf4ad5 | |||
| 90462bf414 | |||
| 791a54bb8b | |||
| 5f4e0ccf6b | |||
| 583e35c768 | |||
| 1e129580b8 | |||
| 184cbd6770 | |||
| b53295a9d5 | |||
| 8448e333cf | |||
| 03b5fe6117 | |||
| f60698a353 | |||
| 7c48ae6ff4 | |||
| 4ee789ea37 | |||
| ebf5b75a19 | |||
| 0ecde7ac1e | |||
| c41e387220 | |||
| 7f796ed74e | |||
| 0a21598fec | |||
| 240bef1a66 | |||
| 9583e79056 | |||
| 993a494784 | |||
| 743d31651f | |||
| ce96238464 | |||
| 8b8e7e9f2e | |||
| 50006ca053 | |||
| c3135a3ce7 | |||
| d2f60b13fd | |||
| c50a01d004 | |||
| 4bda501d51 | |||
| a7713f7228 | |||
| 536142be03 | |||
| 44c4826e92 | |||
| 61138cdd81 | |||
| 22ceff43e3 | |||
| a84da2f2c7 | |||
| 7e8da85bd8 | |||
| d304d8720c | |||
| 9da2db2e67 | |||
| 993df7dc21 | |||
| 807d094cf2 | |||
| 3cef238f46 | |||
| 886c40a46b | |||
| b1b82b775c | |||
| 0fe697c26c | |||
| 7c0031679a | |||
| 6bb0496224 | |||
| eedf483957 | |||
| 5421b0d1cc | |||
| fa2c53bd72 | |||
| 6ac67e646c | |||
| 6a20fefd7b | |||
| 43fe558459 | |||
| 0a6b0452dc | |||
| fec5d55250 | |||
| f1b235819e | |||
| d0f9f68689 | |||
| f93a98e9a5 |
@@ -0,0 +1,122 @@
|
|||||||
|
---
|
||||||
|
date: 2026-05-28
|
||||||
|
title: Custom Brand Logo Url
|
||||||
|
---
|
||||||
|
|
||||||
|
# Problem
|
||||||
|
|
||||||
|
`brandingUrl` (the configured "Brand Website") is persisted and editable in branding
|
||||||
|
settings, but historically it was never consumed anywhere. It flowed into the database,
|
||||||
|
the settings form, and the admin read-only view, but never affected any rendered output.
|
||||||
|
|
||||||
|
We want `brandingUrl` to actually do something, with deliberately different behavior per
|
||||||
|
surface.
|
||||||
|
|
||||||
|
# Relationship we're going for
|
||||||
|
|
||||||
|
`brandingUrl` is an **email-only** linking concept. It is intentionally **not** used on
|
||||||
|
in-app signing surfaces.
|
||||||
|
|
||||||
|
| Surface | Custom branding logo configured | `brandingUrl` behavior |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Transactional emails (logo) | Logo shown | Logo links to `brandingUrl` when it is a safe http(s) URL; otherwise plain image |
|
||||||
|
| Transactional emails (footer) | n/a | `brandingUrl` rendered as a link in the footer when it is a safe http(s) URL |
|
||||||
|
| Signing pages (V1 + V2, normal + direct-template) | Logo shown | Ignored — logo is a plain image with no link |
|
||||||
|
| Signing pages (no custom logo) | Documenso fallback shown | Fallback keeps its internal `/` link |
|
||||||
|
| Embedded signing | Logo shown | Ignored (logo not linked) |
|
||||||
|
| Embedded authoring/editor | Logo shown | Ignored |
|
||||||
|
| Settings / admin branding previews | n/a | Unchanged (display only) |
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
|
||||||
|
- On signing pages the recipient is mid-task; sending them off to an external marketing
|
||||||
|
site via the logo is undesirable, so the custom logo is a plain image there.
|
||||||
|
- In emails the logo and a footer link to the brand's own site are a normal, expected
|
||||||
|
pattern and reinforce that the email is legitimately from that brand.
|
||||||
|
|
||||||
|
# Decisions
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Use `brandingUrl` only in transactional email rendering:
|
||||||
|
- The shared email logo component links the custom branding logo to `brandingUrl`.
|
||||||
|
- The shared email footer renders `brandingUrl` as a link.
|
||||||
|
- On signing surfaces, render a configured custom branding logo as a plain image with no
|
||||||
|
link wrapper. Leave the Documenso fallback logo's internal `/` link untouched.
|
||||||
|
- Do not change embedded signing, embedded authoring/editor, or settings/admin previews.
|
||||||
|
- No Prisma schema or database migration. `brandingUrl` already exists and is editable.
|
||||||
|
|
||||||
|
## URL safety
|
||||||
|
|
||||||
|
Rendering must be defensive because old/imported data can bypass the branding form's URL
|
||||||
|
validation. Only treat the stored value as a usable Brand Website when it parses as an
|
||||||
|
absolute `http:` or `https:` URL.
|
||||||
|
|
||||||
|
- Empty, missing, invalid, relative, or non-http(s) values are treated as "no Brand
|
||||||
|
Website" and produce a plain logo / no footer link.
|
||||||
|
- Do not mutate stored settings or run a cleanup migration.
|
||||||
|
- Factored into a single shared helper so both email logo and footer apply identical rules:
|
||||||
|
- `packages/email/utils/branding-url.ts` -> `getSafeBrandingUrl(value): string | null`.
|
||||||
|
|
||||||
|
## Email rendering
|
||||||
|
|
||||||
|
- New shared component `packages/email/template-components/template-branding-logo.tsx`
|
||||||
|
(`TemplateBrandingLogo`) renders either:
|
||||||
|
- the custom branding logo, wrapped in a `Link` to the safe `brandingUrl` with
|
||||||
|
`target="_blank"` when one exists, or a plain `Img` when not; or
|
||||||
|
- the Documenso fallback logo (`/static/logo.png`) when custom branding is disabled or
|
||||||
|
no logo is set.
|
||||||
|
- This component replaced the duplicated `brandingEnabled && brandingLogo ? <Img/> : <fallback/>`
|
||||||
|
ternary that was copy-pasted across all transactional email templates.
|
||||||
|
- `packages/email/template-components/template-footer.tsx` renders `brandingUrl` as a
|
||||||
|
footer link (via `getSafeBrandingUrl`) when branding is enabled and the URL is safe.
|
||||||
|
|
||||||
|
The branding context already exposes `brandingUrl` (`packages/email/providers/branding.tsx`),
|
||||||
|
populated by `teamGlobalSettingsToBranding` / `organisationGlobalSettingsToBranding`
|
||||||
|
(which spread `...settings`), so no additional plumbing into the email branding context was
|
||||||
|
required.
|
||||||
|
|
||||||
|
## Signing rendering
|
||||||
|
|
||||||
|
- `apps/remix/app/components/general/document-signing/document-signing-page-view-v1.tsx`:
|
||||||
|
custom logo renders as a bare `<img>`. `brandingUrl` is not read; the local branding type
|
||||||
|
and loader payload no longer carry it.
|
||||||
|
- `apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx` (V2,
|
||||||
|
shared by normal and direct-template signing): custom logo renders as a bare `<img>`; the
|
||||||
|
Documenso fallback keeps its `<Link to="/">`.
|
||||||
|
- `apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx`: V1 loader branding payload no
|
||||||
|
longer includes `brandingUrl`.
|
||||||
|
- `packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts` and
|
||||||
|
`get-envelope-for-direct-template-signing.ts`: `brandingUrl` removed from the V2
|
||||||
|
`EnvelopeForSigningResponse.settings` schema/payload since it is not consumed there.
|
||||||
|
|
||||||
|
# History
|
||||||
|
|
||||||
|
An earlier iteration of this plan wired `brandingUrl` into the in-app signing pages so a
|
||||||
|
custom logo linked to the Brand Website (external `<a target="_blank">`, internal `/`
|
||||||
|
fallback otherwise) and added `brandingUrl` to the V1/V2 signing payloads. That direction
|
||||||
|
was reversed: signing-page logos are now plain images and `brandingUrl` is email-only. The
|
||||||
|
signing payload additions were removed.
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
|
||||||
|
`packages/app-tests/e2e/signing-branding.spec.ts`:
|
||||||
|
|
||||||
|
- V1 normal `/sign/:token`: custom logo is a plain image, not inside a link, and no
|
||||||
|
`brandingUrl` link is present.
|
||||||
|
- V2 normal `/sign/:token` and V2 direct-template: same plain-image assertions.
|
||||||
|
- V2 with no custom logo: Documenso fallback still links to `/`.
|
||||||
|
- Embedded signing: no custom-logo Brand Website link is rendered.
|
||||||
|
|
||||||
|
# Acceptance criteria
|
||||||
|
|
||||||
|
- A custom branding logo on any signing surface (V1, V2 normal, V2 direct-template, embedded)
|
||||||
|
renders as a plain image with no link, and `brandingUrl` is never rendered as a link there.
|
||||||
|
- Documenso fallback logos continue linking to `/`.
|
||||||
|
- In transactional emails, when a custom logo and a safe `brandingUrl` are configured, the
|
||||||
|
email logo links to `brandingUrl` (new tab) and the footer shows the Brand Website link.
|
||||||
|
- In transactional emails, when `brandingUrl` is empty/invalid/relative/non-http(s), the logo
|
||||||
|
is a plain image and no footer Brand Website link is shown.
|
||||||
|
- URL safety is enforced through the single shared `getSafeBrandingUrl` helper.
|
||||||
|
- Settings/admin branding previews are unchanged.
|
||||||
|
- No schema or migration changes.
|
||||||
+24
-2
@@ -48,7 +48,7 @@ NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documen
|
|||||||
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
|
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
|
||||||
|
|
||||||
# [[SIGNING]]
|
# [[SIGNING]]
|
||||||
# The transport to use for document signing. Available options: local (default) | gcloud-hsm
|
# The transport to use for document signing. Available options: local (default) | gcloud-hsm | csc
|
||||||
NEXT_PRIVATE_SIGNING_TRANSPORT="local"
|
NEXT_PRIVATE_SIGNING_TRANSPORT="local"
|
||||||
# OPTIONAL: The passphrase to use for the local file-based signing transport.
|
# OPTIONAL: The passphrase to use for the local file-based signing transport.
|
||||||
NEXT_PRIVATE_SIGNING_PASSPHRASE=
|
NEXT_PRIVATE_SIGNING_PASSPHRASE=
|
||||||
@@ -70,6 +70,14 @@ NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_FILE_PATH=
|
|||||||
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS=
|
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS=
|
||||||
# OPTIONAL: The Google Secret Manager path to retrieve the certificate for the gcloud-hsm signing transport.
|
# OPTIONAL: The Google Secret Manager path to retrieve the certificate for the gcloud-hsm signing transport.
|
||||||
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH=
|
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH=
|
||||||
|
# OPTIONAL: The base URL of the Cloud Signature Consortium (CSC) provider for the csc signing transport.
|
||||||
|
NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL=
|
||||||
|
# OPTIONAL: The OAuth client ID registered with the CSC provider for the csc signing transport.
|
||||||
|
NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID=
|
||||||
|
# OPTIONAL: The OAuth client secret registered with the CSC provider for the csc signing transport.
|
||||||
|
NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET=
|
||||||
|
# OPTIONAL: Default signature level for envelopes created on a CSC instance when the caller doesn't specify one. Available options: AES (default) | QES. Explicit AES/QES requests always pass through unchanged.
|
||||||
|
NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL=
|
||||||
# OPTIONAL: Comma-separated list of timestamp authority URLs for PDF signing (enables LTV and archival timestamps).
|
# OPTIONAL: Comma-separated list of timestamp authority URLs for PDF signing (enables LTV and archival timestamps).
|
||||||
NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=
|
NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=
|
||||||
# OPTIONAL: Contact info to embed in PDF signatures. Defaults to the webapp URL.
|
# OPTIONAL: Contact info to embed in PDF signatures. Defaults to the webapp URL.
|
||||||
@@ -95,7 +103,7 @@ NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID="documenso"
|
|||||||
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY="password"
|
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY="password"
|
||||||
|
|
||||||
# [[SMTP]]
|
# [[SMTP]]
|
||||||
# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | mailchannels
|
# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | resend | mailchannels
|
||||||
NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth"
|
NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth"
|
||||||
# OPTIONAL: Defines the host to use for sending emails.
|
# OPTIONAL: Defines the host to use for sending emails.
|
||||||
NEXT_PRIVATE_SMTP_HOST="127.0.0.1"
|
NEXT_PRIVATE_SMTP_HOST="127.0.0.1"
|
||||||
@@ -172,6 +180,20 @@ NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=
|
|||||||
NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=
|
NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=
|
||||||
# OPTIONAL: Comma-separated list of email domains allowed to sign up (e.g., example.com,acme.org).
|
# OPTIONAL: Comma-separated list of email domains allowed to sign up (e.g., example.com,acme.org).
|
||||||
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=
|
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=
|
||||||
|
# OPTIONAL: Set to "true" to disable all signin methods (email, Google, Microsoft, OIDC).
|
||||||
|
NEXT_PUBLIC_DISABLE_SIGNIN=
|
||||||
|
# OPTIONAL: Set to "true" to disable email/password signin only. Also closes /forgot-password and /reset-password.
|
||||||
|
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN=
|
||||||
|
# OPTIONAL: Set to "true" to hide the Google signin button.
|
||||||
|
NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN=
|
||||||
|
# OPTIONAL: Set to "true" to hide the Microsoft signin button.
|
||||||
|
NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN=
|
||||||
|
# OPTIONAL: Set to "true" to hide the OIDC signin button.
|
||||||
|
NEXT_PUBLIC_DISABLE_OIDC_SIGNIN=
|
||||||
|
# OPTIONAL: When OIDC is the only enabled signin transport, /signin auto-redirects
|
||||||
|
# to the OIDC provider (rendering only a spinner). Set to "true" to disable this
|
||||||
|
# and keep showing the signin page.
|
||||||
|
NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT=
|
||||||
# OPTIONAL: Set to true to use internal webapp url in browserless requests.
|
# OPTIONAL: Set to true to use internal webapp url in browserless requests.
|
||||||
NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false
|
NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ body:
|
|||||||
label: Browser [e.g., Chrome, Firefox]
|
label: Browser [e.g., Chrome, Firefox]
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Version [e.g., 2.0.1]
|
label: Version [e.g., 2.13.0]
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Please check the boxes that apply to this issue report.
|
label: Please check the boxes that apply to this issue report.
|
||||||
@@ -44,4 +44,3 @@ body:
|
|||||||
- label: I have included relevant environment information.
|
- label: I have included relevant environment information.
|
||||||
- label: I have included any relevant screenshots.
|
- label: I have included any relevant screenshots.
|
||||||
- label: I understand that this is a voluntary contribution and that there is no guarantee of resolution.
|
- label: I understand that this is a voluntary contribution and that there is no guarantee of resolution.
|
||||||
- label: I want to work on creating a PR for this issue if approved
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Security vulnerability
|
||||||
|
url: https://github.com/documenso/documenso/security/advisories/new
|
||||||
|
about: Please report security vulnerabilities privately via GitHub Security Advisories. Do not open a public issue.
|
||||||
|
- name: Questions & Discussions
|
||||||
|
url: https://github.com/documenso/documenso/discussions
|
||||||
|
about: Ask questions, share ideas, and discuss Documenso with the community.
|
||||||
|
- name: Discord
|
||||||
|
url: https://documen.so/discord
|
||||||
|
about: Chat with the community and the team.
|
||||||
@@ -33,4 +33,3 @@ body:
|
|||||||
- label: I have explained the use case or scenario for this feature.
|
- label: I have explained the use case or scenario for this feature.
|
||||||
- label: I have included any relevant technical details or design suggestions.
|
- label: I have included any relevant technical details or design suggestions.
|
||||||
- label: I understand that this is a suggestion and that there is no guarantee of implementation.
|
- label: I understand that this is a suggestion and that there is no guarantee of implementation.
|
||||||
- label: I want to work on creating a PR for this issue if approved
|
|
||||||
|
|||||||
@@ -15,17 +15,6 @@ body:
|
|||||||
description: 'Are there any additional context or information that might be relevant to the improvement suggestion.'
|
description: 'Are there any additional context or information that might be relevant to the improvement suggestion.'
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: dropdown
|
|
||||||
id: assignee
|
|
||||||
attributes:
|
|
||||||
label: 'Do you want to work on this improvement?'
|
|
||||||
multiple: false
|
|
||||||
options:
|
|
||||||
- 'No'
|
|
||||||
- 'Yes'
|
|
||||||
default: 0
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: 'Please check the boxes that apply to this improvement suggestion.'
|
label: 'Please check the boxes that apply to this improvement suggestion.'
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
<!--
|
||||||
|
We are no longer accepting external pull requests.
|
||||||
|
|
||||||
|
Aside from a small group of trusted contributors we reach out to directly,
|
||||||
|
new external PRs will usually be closed with a request to open an issue instead.
|
||||||
|
This is a security decision. See https://documenso.com/blog/why-we-re-pausing-external-pull-requests
|
||||||
|
|
||||||
|
If you're a trusted contributor or maintainer, continue below.
|
||||||
|
Otherwise, please open a detailed issue: https://github.com/documenso/documenso/issues/new/choose
|
||||||
|
-->
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
<!--- Describe the changes introduced by this pull request. -->
|
<!--- Describe the changes introduced by this pull request. -->
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
name: Test Addition
|
|
||||||
about: Submit a new test, either unit or end-to-end (E2E), for review and inclusion
|
|
||||||
---
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!--- Provide a clear and concise description of the new test you are adding. -->
|
|
||||||
<!--- Explain the purpose of the test and what it aims to validate. -->
|
|
||||||
|
|
||||||
## Related Issue
|
|
||||||
|
|
||||||
<!--- If this test addition is related to a specific issue, reference it here using #issue_number. -->
|
|
||||||
<!--- For example, "Fixes #123" or "Addresses #456". -->
|
|
||||||
|
|
||||||
## Test Details
|
|
||||||
|
|
||||||
<!--- Describe the details of the test you're adding. -->
|
|
||||||
<!--- Include information about inputs, expected outputs, and any specific scenarios. -->
|
|
||||||
|
|
||||||
- Test Name: Name of the test
|
|
||||||
- Type: [Unit / E2E]
|
|
||||||
- Description: Brief description of what the test checks
|
|
||||||
- Inputs: What inputs the test uses (if applicable)
|
|
||||||
- Expected Output: What output or behavior the test expects
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
<!--- Please check the boxes that apply to this pull request. -->
|
|
||||||
<!--- You can add or remove items as needed. -->
|
|
||||||
|
|
||||||
- [ ] I have written the new test and ensured it works as intended.
|
|
||||||
- [ ] I have added necessary documentation to explain the purpose of the test.
|
|
||||||
- [ ] I have followed the project's testing guidelines and coding style.
|
|
||||||
- [ ] I have addressed any review feedback from previous submissions, if applicable.
|
|
||||||
|
|
||||||
## Additional Notes
|
|
||||||
|
|
||||||
<!--- Provide any additional context or notes for the reviewers. -->
|
|
||||||
<!--- This might include explanations about the testing approach or any potential concerns. -->
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
name: 'Welcome New Contributors'
|
name: 'Welcome New Contributors'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
types: ['opened']
|
|
||||||
issues:
|
issues:
|
||||||
types: ['opened']
|
types: ['opened']
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
issues: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -20,10 +17,7 @@ jobs:
|
|||||||
- uses: actions/first-interaction@v1
|
- uses: actions/first-interaction@v1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
pr-message: |
|
|
||||||
Thank you for creating your first Pull Request and for being a part of the open signing revolution! 💚🚀
|
|
||||||
<br /> Feel free to hop into our community in [Discord](https://documen.so/discord)
|
|
||||||
issue-message: |
|
issue-message: |
|
||||||
Thank you for opening your first issue and for being a part of the open signing revolution!
|
Thank you for opening your first issue and for being a part of the open signing revolution!
|
||||||
<br /> One of our team members will review it and get back to you as soon as it possible 💚
|
<br /> One of our team members will review it and get back to you as soon as possible 💚
|
||||||
<br /> Meanwhile, please feel free to hop into our community in [Discord](https://documen.so/discord)
|
<br /> Meanwhile, please feel free to hop into our community in [Discord](https://documen.so/discord)
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
name: 'Issue Assignee Check'
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: ['assigned']
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
countIssues:
|
|
||||||
if: ${{ github.event.issue.assignee }} && github.event.action == 'assigned' && github.event.sender.type == 'User'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
|
|
||||||
- name: Install Octokit
|
|
||||||
run: npm install @octokit/rest@18
|
|
||||||
|
|
||||||
- name: Check Assigned User's Issue Count
|
|
||||||
id: parse-comment
|
|
||||||
uses: actions/github-script@v6
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const { Octokit } = require("@octokit/rest");
|
|
||||||
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
|
||||||
|
|
||||||
const username = context.payload.issue.assignee.login;
|
|
||||||
console.log(`Username Extracted: ${username}`);
|
|
||||||
|
|
||||||
const { data: issues } = await octokit.issues.listForRepo({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
assignee: username,
|
|
||||||
state: 'open'
|
|
||||||
});
|
|
||||||
|
|
||||||
const issueCount = issues.length;
|
|
||||||
console.log(`Issue Count For ${username}: ${issueCount}`);
|
|
||||||
|
|
||||||
if (issueCount > 3) {
|
|
||||||
let issueCountMessage = `### 🚨 Documenso Police 🚨`;
|
|
||||||
issueCountMessage += `\n@${username} has ${issueCount} open issues assigned already. Consider whether this issue should be assigned to them or left open for another contributor.`;
|
|
||||||
|
|
||||||
await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: context.issue.number,
|
|
||||||
body: issueCountMessage,
|
|
||||||
headers: {
|
|
||||||
'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
name: 'PR Review Reminder'
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: ['opened', 'ready_for_review']
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
checkPRs:
|
|
||||||
if: ${{ github.event.pull_request.user.login }} && github.event.action == ('opened' || 'ready_for_review')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
|
|
||||||
- name: Install Octokit
|
|
||||||
run: npm install @octokit/rest@18
|
|
||||||
|
|
||||||
- name: Check user's PRs awaiting review
|
|
||||||
id: parse-prs
|
|
||||||
uses: actions/github-script@v5
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
script: |
|
|
||||||
const { Octokit } = require("@octokit/rest");
|
|
||||||
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
|
||||||
|
|
||||||
const username = context.payload.pull_request.user.login;
|
|
||||||
console.log(`Username Extracted: ${username}`);
|
|
||||||
|
|
||||||
const { data: pullRequests } = await octokit.pulls.list({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
state: 'open',
|
|
||||||
sort: 'created',
|
|
||||||
direction: 'asc',
|
|
||||||
});
|
|
||||||
|
|
||||||
const userPullRequests = pullRequests.filter(pr => pr.user.login === username && (pr.state === 'open' || pr.state === 'ready_for_review'));
|
|
||||||
const prCount = userPullRequests.length;
|
|
||||||
console.log(`PR Count for ${username}: ${prCount}`);
|
|
||||||
|
|
||||||
if (prCount > 3) {
|
|
||||||
let prReminderMessage = `🚨 @${username} has ${prCount} pull requests awaiting review. Please consider reviewing them when possible. 🚨`;
|
|
||||||
|
|
||||||
await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: context.payload.pull_request.number,
|
|
||||||
body: prReminderMessage,
|
|
||||||
headers: {
|
|
||||||
'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -16,24 +16,6 @@ jobs:
|
|||||||
name: Validate PR title
|
name: Validate PR title
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check PR creator's previous activity
|
|
||||||
id: check_activity
|
|
||||||
run: |
|
|
||||||
CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login')
|
|
||||||
ACTIVITY=$(curl -s "https://api.github.com/search/commits?q=author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count')
|
|
||||||
if [ "$ACTIVITY" -eq 0 ]; then
|
|
||||||
echo "::set-output name=is_new::true"
|
|
||||||
else
|
|
||||||
echo "::set-output name=is_new::false"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Count PRs created by user
|
|
||||||
id: count_prs
|
|
||||||
run: |
|
|
||||||
CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login')
|
|
||||||
PR_COUNT=$(curl -s "https://api.github.com/search/issues?q=type:pr+is:open+author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count')
|
|
||||||
echo "::set-output name=pr_count::$PR_COUNT"
|
|
||||||
|
|
||||||
- uses: amannn/action-semantic-pull-request@v5
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
id: lint_pr_title
|
id: lint_pr_title
|
||||||
env:
|
env:
|
||||||
@@ -44,8 +26,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
header: pr-title-lint-error
|
header: pr-title-lint-error
|
||||||
message: |
|
message: |
|
||||||
Hey There! and thank you for opening this pull request! 📝👋🏼
|
|
||||||
|
|
||||||
We require pull request titles to follow the [Conventional Commits Spec](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
We require pull request titles to follow the [Conventional Commits Spec](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||||
|
|
||||||
Details:
|
Details:
|
||||||
@@ -53,10 +33,3 @@ jobs:
|
|||||||
```
|
```
|
||||||
${{ steps.lint_pr_title.outputs.error_message }}
|
${{ steps.lint_pr_title.outputs.error_message }}
|
||||||
```
|
```
|
||||||
|
|
||||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null && steps.check_activity.outputs.is_new == 'false' && steps.count_prs.outputs.pr_count < 2}}
|
|
||||||
uses: marocchino/sticky-pull-request-comment@v2
|
|
||||||
with:
|
|
||||||
header: pr-title-lint-error
|
|
||||||
message: |
|
|
||||||
Thank you for following the naming conventions for pull request titles! 💚🚀
|
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ jobs:
|
|||||||
stale-pr-message: 'This PR has not seen activitiy for a while. It will be closed in 30 days unless further activity is detected.'
|
stale-pr-message: 'This PR has not seen activitiy for a while. It will be closed in 30 days unless further activity is detected.'
|
||||||
close-pr-message: 'This PR has been closed because of inactivity.'
|
close-pr-message: 'This PR has been closed because of inactivity.'
|
||||||
exempt-pr-labels: 'WIP,on-hold,needs review'
|
exempt-pr-labels: 'WIP,on-hold,needs review'
|
||||||
exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,assigned,needs triage'
|
exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,status: assigned,status: triage'
|
||||||
|
|||||||
+1
-2
@@ -31,8 +31,7 @@ vscode:
|
|||||||
extensions:
|
extensions:
|
||||||
- aaron-bond.better-comments
|
- aaron-bond.better-comments
|
||||||
- bradlc.vscode-tailwindcss
|
- bradlc.vscode-tailwindcss
|
||||||
- dbaeumer.vscode-eslint
|
- biomejs.biome
|
||||||
- esbenp.prettier-vscode
|
|
||||||
- mikestead.dotenv
|
- mikestead.dotenv
|
||||||
- unifiedjs.vscode-mdx
|
- unifiedjs.vscode-mdx
|
||||||
- GitHub.vscode-pull-request-github
|
- GitHub.vscode-pull-request-github
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
legacy-peer-deps = true
|
legacy-peer-deps = true
|
||||||
prefer-dedupe = true
|
prefer-dedupe = true
|
||||||
min-release-age = 7
|
# min-release-age = 7
|
||||||
|
|||||||
Vendored
+1
-1
@@ -29,6 +29,6 @@
|
|||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
# General Issues
|
# Report security vulnerabilities privately via GitHub Security Advisories (preferred).
|
||||||
|
Contact: https://github.com/documenso/documenso/security/advisories/new
|
||||||
|
|
||||||
|
# Alternatively, report critical issues privately by email.
|
||||||
|
Contact: mailto:security@documenso.com
|
||||||
|
|
||||||
|
# Security policy
|
||||||
|
Policy: https://github.com/documenso/documenso/security/policy
|
||||||
|
|
||||||
|
# General (non-security) issues
|
||||||
Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
|
Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
|
||||||
|
|
||||||
# Report critical issues privately to let us take appropriate action before publishing.
|
|
||||||
Contact: mailto:security@documenso.com
|
|
||||||
Preferred-Languages: en
|
Preferred-Languages: en
|
||||||
Canonical: https://documenso.com/.well-known/security.txt
|
Canonical: https://documenso.com/.well-known/security.txt
|
||||||
|
|||||||
+1
-1
@@ -42,8 +42,8 @@ Documenso is an open-source document signing platform built as a **monorepo** us
|
|||||||
| Package | Description | Port |
|
| Package | Description | Port |
|
||||||
| -------------------------- | -------------------------------------------------------- | ---- |
|
| -------------------------- | -------------------------------------------------------- | ---- |
|
||||||
| `@documenso/remix` | Main application - React Router (Remix) with Hono server | 3000 |
|
| `@documenso/remix` | Main application - React Router (Remix) with Hono server | 3000 |
|
||||||
| `@documenso/documentation` | Documentation site (Next.js + Nextra) | 3002 |
|
|
||||||
| `@documenso/openpage-api` | Public analytics API | 3003 |
|
| `@documenso/openpage-api` | Public analytics API | 3003 |
|
||||||
|
| `@documenso/docs` | Documentation site | 3004 |
|
||||||
|
|
||||||
### Core Packages (`packages/`)
|
### Core Packages (`packages/`)
|
||||||
|
|
||||||
|
|||||||
+20
-6
@@ -1,13 +1,27 @@
|
|||||||
# Contributing to Documenso
|
# Contributing to Documenso
|
||||||
|
|
||||||
If you plan to contribute to Documenso, please take a moment to feel awesome ✨ People like you are what open source is about ♥. Any contributions, no matter how big or small, are highly appreciated.
|
> **We are no longer accepting external pull requests.**
|
||||||
|
>
|
||||||
|
> Aside from a small group of trusted contributors we reach out to directly, we no longer merge external PRs. New pull requests will usually be closed with a request to open an issue instead. This is a security decision, not a judgement on your work. Read [Why We're Pausing External Pull Requests](https://documenso.com/blog/why-we-re-pausing-external-pull-requests) for the full reasoning.
|
||||||
|
>
|
||||||
|
> Documenso stays open source. You can still read, audit, run, and fork the code. The best way to contribute is through detailed issues.
|
||||||
|
|
||||||
## Before getting started
|
## How to contribute now
|
||||||
|
|
||||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission.
|
The most useful contribution is a detailed issue. Treat it like a spec. The more detail, the better:
|
||||||
- Select an issue from [here](https://github.com/documenso/documenso/issues) or create a new one
|
|
||||||
- Consider the results from the discussion on the issue
|
- The problem you're trying to solve, and who it affects
|
||||||
- Accept the [Contributor License Agreement](https://documen.so/cla) to ensure we can accept your contributions.
|
- How you expect the feature or change to behave
|
||||||
|
- Edge cases, constraints, and anything you've already considered
|
||||||
|
- Examples, mockups, or references where they help
|
||||||
|
|
||||||
|
Before opening an issue, search [existing issues](https://github.com/documenso/documenso/issues) and [discussions](https://github.com/documenso/documenso/discussions) for related items. If a proposal is detailed and fits where Documenso is heading, we'll pick it up and build against it.
|
||||||
|
|
||||||
|
For security vulnerabilities, do not open a public issue. Follow our [Security Policy](./SECURITY.md) instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The sections below are for trusted contributors working with us directly, and for anyone running Documenso locally or maintaining a fork.
|
||||||
|
|
||||||
## English only PRs and Issues
|
## English only PRs and Issues
|
||||||
|
|
||||||
|
|||||||
@@ -51,16 +51,18 @@ Join us in creating the next generation of open trust infrastructure.
|
|||||||
|
|
||||||
## Community and Next Steps 🎯
|
## Community and Next Steps 🎯
|
||||||
|
|
||||||
- Check out the first source code release in this repository and test it.
|
- Try Documenso by self-hosting it or signing up at [documenso.com](https://documenso.com).
|
||||||
- Tell us what you think in the [Discussions](https://github.com/documenso/documenso/discussions).
|
- Tell us what you think in the [Discussions](https://github.com/documenso/documenso/discussions).
|
||||||
- Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members.
|
- Join the [Discord server](https://documen.so/discord) for any questions and getting to know other community members.
|
||||||
- ⭐ the repository to help us raise awareness.
|
- ⭐ the repository to help us raise awareness.
|
||||||
- Spread the word on Twitter that Documenso is working towards a more open signing tool.
|
- Open detailed [issues](https://github.com/documenso/documenso/issues) to report bugs or propose features.
|
||||||
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
- To contribute, please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
|
> **Note**: We no longer accept external pull requests, aside from a small group of trusted contributors we reach out to directly. The best way to contribute is through detailed issues. Read [Why We're Pausing External Pull Requests](https://documenso.com/blog/why-we-re-pausing-external-pull-requests) for the reasoning.
|
||||||
|
|
||||||
|
- Documenso stays open source. You can read, audit, run, and fork the code.
|
||||||
|
- To report issues or propose changes, see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
|
||||||
|
|
||||||
## Contact us
|
## Contact us
|
||||||
|
|
||||||
@@ -81,17 +83,21 @@ Contact us if you are interested in our Enterprise plan for large organizations
|
|||||||
<a href=""><img src="" alt=""></a>
|
<a href=""><img src="" alt=""></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
- [Typescript](https://www.typescriptlang.org/) - Language
|
- [TypeScript](https://www.typescriptlang.org/) - Language
|
||||||
- [ReactRouter](https://reactrouter.com/) - Framework
|
- [React Router v7](https://reactrouter.com/) - Framework
|
||||||
|
- [Hono](https://hono.dev/) - Server
|
||||||
- [Prisma](https://www.prisma.io/) - ORM
|
- [Prisma](https://www.prisma.io/) - ORM
|
||||||
- [Tailwind](https://tailwindcss.com/) - CSS
|
- [Tailwind CSS](https://tailwindcss.com/) - CSS
|
||||||
- [shadcn/ui](https://ui.shadcn.com/) - Component Library
|
- [shadcn/ui](https://ui.shadcn.com/) + [Radix UI](https://www.radix-ui.com/) - Component Library
|
||||||
- [react-email](https://react.email/) - Email Templates
|
- [react-email](https://react.email/) - Email Templates
|
||||||
|
- [Lingui](https://lingui.dev/) - Internationalization
|
||||||
- [tRPC](https://trpc.io/) - API
|
- [tRPC](https://trpc.io/) - API
|
||||||
- [@documenso/pdf-sign](https://www.npmjs.com/package/@documenso/pdf-sign) - PDF Signatures (launching soon)
|
- [@libpdf/core](https://www.npmjs.com/package/@libpdf/core) - PDF Signatures
|
||||||
- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs
|
- [pdf.js](https://mozilla.github.io/pdf.js/) - Viewing PDFs
|
||||||
- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation
|
- [@cantoo/pdf-lib](https://github.com/cantoo-scribe/pdf-lib) - PDF manipulation
|
||||||
- [Stripe](https://stripe.com/) - Payments
|
- [Stripe](https://stripe.com/) - Payments
|
||||||
|
- [Biome](https://biomejs.dev/) - Linting & Formatting
|
||||||
|
- [Playwright](https://playwright.dev/) - E2E Testing
|
||||||
|
|
||||||
<!-- - Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned. -->
|
<!-- - Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned. -->
|
||||||
|
|
||||||
@@ -182,7 +188,7 @@ For full instructions, requirements, and configuration details, see the [Self Ho
|
|||||||
|
|
||||||
#### Railway
|
#### Railway
|
||||||
|
|
||||||
[](https://railway.app/template/bG6D4p)
|
[](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic)
|
||||||
|
|
||||||
#### Render
|
#### Render
|
||||||
|
|
||||||
@@ -196,6 +202,10 @@ For full instructions, requirements, and configuration details, see the [Self Ho
|
|||||||
|
|
||||||
[](https://elest.io/open-source/documenso)
|
[](https://elest.io/open-source/documenso)
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
If you believe you have found a security vulnerability in Documenso, please report it through our [Security Policy](https://github.com/documenso/documenso/security/policy). We prioritize private reports via [GitHub Security Advisories](https://github.com/documenso/documenso/security/advisories/new). See [SECURITY.md](./SECURITY.md) for scope and details.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
For troubleshooting self-hosted deployments, see the [Troubleshooting guide](https://docs.documenso.com/docs/self-hosting/maintenance/troubleshooting) and [Tips & Common Pitfalls](https://docs.documenso.com/docs/self-hosting/getting-started/tips).
|
For troubleshooting self-hosted deployments, see the [Troubleshooting guide](https://docs.documenso.com/docs/self-hosting/maintenance/troubleshooting) and [Tips & Common Pitfalls](https://docs.documenso.com/docs/self-hosting/getting-started/tips).
|
||||||
|
|||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
We take the security of Documenso seriously. As a platform trusted with legally binding documents, the safety of the project and the people who rely on it is a priority for us. We're grateful to the security researchers who help keep it that way. If you've found an issue, we'd genuinely like to hear about it.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Report security vulnerabilities privately. Do not open a public issue, discussion, or pull request for security reports.
|
||||||
|
|
||||||
|
We accept reports through two channels, in order of preference:
|
||||||
|
|
||||||
|
1. **GitHub Security Advisories (preferred)**. Use the [private vulnerability reporting form](https://github.com/documenso/documenso/security/advisories/new). This is our primary channel and lets us triage and work with you on a fix.
|
||||||
|
2. **Email**. If you cannot use GitHub Security Advisories, email [security@documenso.com](mailto:security@documenso.com).
|
||||||
|
|
||||||
|
Include the affected version, a clear description, steps to reproduce, and the potential impact.
|
||||||
|
|
||||||
|
## Triage and Response
|
||||||
|
|
||||||
|
We triage reports as we have availability. We read every report we receive, and we appreciate the time and effort it takes to put one together.
|
||||||
|
|
||||||
|
We also run [Codex](https://openai.com/codex/) security analysis across the codebase. If Codex has already reported the issue you're sending us, we may close your report as a duplicate. Please don't take this as a reflection on your work; it just means our automated tooling happened to surface the same thing first.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This policy covers vulnerabilities in the Documenso application code in this repository.
|
||||||
|
|
||||||
|
The items below are out of scope and will not be accepted. They are deployment, infrastructure, and configuration concerns that belong with the operator's firewall, network, and environment setup, not the application:
|
||||||
|
|
||||||
|
- Server-Side Request Forgery (SSRF) and related network-egress concerns
|
||||||
|
- DNS rebinding and other DNS-level issues
|
||||||
|
- Rate limiting, denial of service, and volumetric attacks
|
||||||
|
- TLS and certificate configuration, HTTP security headers, and other reverse-proxy or web-server configuration
|
||||||
|
- Findings that depend on insecure self-hosted infrastructure or misconfiguration
|
||||||
|
|
||||||
|
If you're unsure whether something is in scope, report it privately anyway and we'll happily take a look.
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Security fixes are applied to the latest release. Run the most recent version of Documenso.
|
||||||
+6
-64
@@ -1,67 +1,9 @@
|
|||||||
# Creating your own signing certificate
|
# Signing Certificate
|
||||||
|
|
||||||
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
|
Documenso needs a signing certificate to digitally sign documents. For full, up-to-date instructions on generating, converting, and configuring a certificate, see the official documentation:
|
||||||
|
|
||||||
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:
|
- [Signing Certificate](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate): Overview and all certificate options
|
||||||
|
- [Local Certificate](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate/local): Generate a self-signed `.p12` certificate with OpenSSL
|
||||||
|
- [Google Cloud HSM](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm): Sign using Google Cloud KMS
|
||||||
|
|
||||||
`openssl genrsa -out private.key 2048`
|
For deploying Documenso with Docker, see the [Docker Deployment](https://docs.documenso.com/docs/self-hosting/deployment/docker) and [Docker Compose](https://docs.documenso.com/docs/self-hosting/deployment/docker-compose) guides.
|
||||||
|
|
||||||
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
|
|
||||||
|
|
||||||
`openssl req -new -x509 -key private.key -out certificate.crt -days 365`
|
|
||||||
|
|
||||||
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The `-days` parameter sets the number of days for which the certificate is valid.
|
|
||||||
|
|
||||||
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following commands to do this:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Set certificate password securely (won't appear in command history)
|
|
||||||
read -s -p "Enter certificate password: " CERT_PASS
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Create the p12 certificate using the environment variable
|
|
||||||
openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt \
|
|
||||||
-password env:CERT_PASS \
|
|
||||||
-keypbe PBE-SHA1-3DES \
|
|
||||||
-certpbe PBE-SHA1-3DES \
|
|
||||||
-macalg sha1
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **IMPORTANT**: A certificate password is required to prevent signing failures. Make sure to use a strong password (minimum 4 characters) when prompted. Certificates without passwords will cause "Failed to get private key bags" errors during document signing.
|
|
||||||
|
|
||||||
5. Place the certificate `/apps/remix/resources/certificate.p12` (If the path does not exist, it needs to be created)
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
|
|
||||||
|
|
||||||
Want to create a production ready docker image? Follow these steps:
|
|
||||||
|
|
||||||
- cd into `docker` directory
|
|
||||||
- Make `build.sh` executable by running `chmod +x build.sh`
|
|
||||||
- Run `./build.sh` to start building the docker image.
|
|
||||||
- Publish the image to your docker registry of choice (or) If you prefer running the image from local, run the below command
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Command Breakdown:
|
|
||||||
|
|
||||||
- `-d` - Let's you run the container in background
|
|
||||||
- `-p` - Passes down which ports to use. First half is the host port, Second half is the app port. You can change the first half anything you want and reverse proxy to that port.
|
|
||||||
- `-v` - Volume let's you persist the data
|
|
||||||
- `--name` - Name of the container
|
|
||||||
- `documenso:latest` - Image you have built
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
|
||||||
|
|
||||||
## Railway
|
|
||||||
|
|
||||||
[](https://railway.app/template/DjrRRX)
|
|
||||||
|
|
||||||
## Render
|
|
||||||
|
|
||||||
[](https://render.com/deploy?repo=https://github.com/documenso/documenso)
|
|
||||||
|
|||||||
+9
-38
@@ -1,45 +1,16 @@
|
|||||||
# docs
|
# @documenso/docs
|
||||||
|
|
||||||
This is a Next.js application generated with
|
The Documenso documentation site, built with [Next.js](https://nextjs.org/) and [Fumadocs](https://fumadocs.dev/). Published at [docs.documenso.com](https://docs.documenso.com).
|
||||||
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
|
|
||||||
|
|
||||||
Run development server:
|
Content lives under `content/docs/` as MDX. See [WRITING_STYLE.md](../../WRITING_STYLE.md) for the documentation writing conventions.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
# From the monorepo root
|
||||||
# or
|
npm run dev --filter=@documenso/docs
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Open http://localhost:3000 with your browser to see the result.
|
## Structure
|
||||||
|
|
||||||
## Explore
|
- `content/docs/`: Documentation pages (MDX).
|
||||||
|
- `lib/source.ts`: Content source adapter.
|
||||||
In the project, you can see:
|
- `lib/layout.shared.tsx`: Shared layout options.
|
||||||
|
|
||||||
- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
|
|
||||||
- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.
|
|
||||||
|
|
||||||
| Route | Description |
|
|
||||||
| ------------------------- | ------------------------------------------------------ |
|
|
||||||
| `app/(home)` | The route group for your landing page and other pages. |
|
|
||||||
| `app/docs` | The documentation layout and pages. |
|
|
||||||
| `app/api/search/route.ts` | The Route Handler for search. |
|
|
||||||
|
|
||||||
### Fumadocs MDX
|
|
||||||
|
|
||||||
A `source.config.ts` config file has been included, you can customise different options like frontmatter schema.
|
|
||||||
|
|
||||||
Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about Next.js and Fumadocs, take a look at the following
|
|
||||||
resources:
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
|
|
||||||
features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { Callout } from 'fumadocs-ui/components/callout';
|
|||||||
| 21 CFR Part 11 | Compliant (Enterprise) |
|
| 21 CFR Part 11 | Compliant (Enterprise) |
|
||||||
| SOC 2 | Compliant |
|
| SOC 2 | Compliant |
|
||||||
| ISO 27001 | Planned |
|
| ISO 27001 | Planned |
|
||||||
| HIPAA | Planned |
|
| HIPAA | Compliant (Enterprise) |
|
||||||
|
|
||||||
## 21 CFR Part 11
|
## 21 CFR Part 11
|
||||||
|
|
||||||
|
|||||||
@@ -97,12 +97,12 @@ Documenso implements digital signatures with the following characteristics:
|
|||||||
- **Timestamps**: RFC 3161 timestamps can be applied to signatures
|
- **Timestamps**: RFC 3161 timestamps can be applied to signatures
|
||||||
- **Signature visualization**: Signed documents include visual signature representations
|
- **Signature visualization**: Signed documents include visual signature representations
|
||||||
|
|
||||||
For specific implementation details and configuration options, refer to the [signing certificates](/signing-certificates/overview) documentation.
|
For specific implementation details and configuration options, refer to the [signing certificates](/docs/concepts/signing-certificates) documentation.
|
||||||
|
|
||||||
Self-hosted deployments can configure their own signing certificates and timestamp authorities to meet specific compliance requirements.
|
Self-hosted deployments can configure their own signing certificates and timestamp authorities to meet specific compliance requirements.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Legal Validity](/compliance/legal-validity) - Legal frameworks for electronic signatures
|
- [E-Sign Compliance](/docs/compliance/esign) - Legal frameworks for electronic signatures
|
||||||
- [Signing Certificates Overview](/signing-certificates/overview) - Certificate configuration
|
- [Signing Certificates](/docs/concepts/signing-certificates) - Certificate configuration
|
||||||
- [Audit Log](/features/audit-log) - Document activity tracking
|
- [Signing Workflow](/docs/concepts/signing-workflow) - Document activity and audit trail
|
||||||
|
|||||||
@@ -167,5 +167,5 @@ To enable sequential signing:
|
|||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Add Recipients](/users/documents/add-recipients) - How to add recipients to a document
|
- [Add Recipients](/docs/users/documents/add-recipients) - How to add recipients to a document
|
||||||
- [Field Types](/concepts/field-types) - Learn about the different field types you can assign to recipients
|
- [Field Types](/docs/concepts/field-types) - Learn about the different field types you can assign to recipients
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: Common Errors
|
||||||
|
description: A comprehensive troubleshooting matrix for Documenso API and Webhook integration errors.
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide provides a comprehensive troubleshooting matrix for the standard error codes returned by the Documenso API. Use this reference to diagnose and resolve integration issues related to envelopes, recipients, and webhooks.
|
||||||
|
|
||||||
|
## Application Error Codes
|
||||||
|
|
||||||
|
| Error Code | Description | Recommended Action |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `ALREADY_EXISTS` | The resource you are attempting to create already exists. | Verify if the entity (e.g., user, envelope, webhook) has already been instantiated. Use a `PUT` or `PATCH` request to update the existing resource instead of `POST`. |
|
||||||
|
| `EXPIRED_CODE` | The provided access code or token has expired. | Generate a new access code or request a new invitation link before retrying the request. |
|
||||||
|
| `INVALID_BODY` | The request payload is malformed. | Inspect your JSON payload structure. Ensure it strictly adheres to the expected schema and that no required fields are missing. |
|
||||||
|
| `INVALID_REQUEST` | The overall request is malformed or invalid. | Review your API call parameters, including the URL, query parameters, and headers. Correct the request syntax. |
|
||||||
|
| `RECIPIENT_EXPIRED` | The signing link or recipient access has expired. | Generate and resend a new invitation to the affected recipient. |
|
||||||
|
| `LIMIT_EXCEEDED` | Your account usage quota has been exceeded. | Check your current plan limits. Upgrade your subscription or wait until your billing cycle renews. |
|
||||||
|
| `NOT_FOUND` | The requested resource could not be found (404). | Verify the resource ID (envelope, document, webhook) passed in the URL. Ensure the resource has not been deleted. |
|
||||||
|
| `NOT_IMPLEMENTED` | The requested feature is not currently supported by the server. | Consult the API documentation to verify available methods. Do not use this endpoint at this time. |
|
||||||
|
| `NOT_SETUP` | The required configuration for this action is incomplete. | Access your account or integration settings and complete the necessary configuration before retrying. |
|
||||||
|
| `INVALID_CAPTCHA` | Security token (Captcha) validation failed. | Ensure the Captcha token is correctly generated on the client side and transmitted without alteration in your request. |
|
||||||
|
| `UNAUTHORIZED` | Missing or invalid authentication (401). | Verify that your API key is correct, active, and properly formatted in the `Authorization` header (e.g., `Bearer <YOUR_API_KEY>`). |
|
||||||
|
| `FORBIDDEN` | Access to the resource is denied (403). | Ensure your API key or user account has the necessary permissions and roles to execute this specific action. |
|
||||||
|
| `UNKNOWN_ERROR` | An unexpected internal server error occurred (500). | Retry the request later. If the issue persists, contact technical support with your request payload and the timestamp of the incident. |
|
||||||
|
| `RETRY_EXCEPTION` | The operation failed temporarily but can be retried. | Implement an automatic retry logic in your integration, ideally using an exponential backoff strategy. |
|
||||||
|
| `SCHEMA_FAILED` | Strict data schema validation failed. | Verify that the data types sent (string, number, boolean) exactly match the OpenAPI specification. |
|
||||||
|
| `TOO_MANY_REQUESTS` | Rate limit exceeded (429). | Reduce the frequency of your API calls. Implement rate-limiting handling based on the response headers. |
|
||||||
|
| `TWO_FACTOR_AUTH_FAILED` | Two-factor authentication (2FA) failed. | Verify the provided 2FA code. Ensure it was entered correctly and has not expired. |
|
||||||
|
| `WEBHOOK_INVALID_REQUEST` | The webhook-related request is invalid. | Check your receiving endpoint configuration. Ensure the URL is correct and that your server accepts `POST` requests from Documenso. |
|
||||||
|
|
||||||
|
## Envelope State Errors
|
||||||
|
|
||||||
|
The following errors occur when attempting to perform actions on an envelope that are incompatible with its current state.
|
||||||
|
|
||||||
|
| Error Code | Description | Recommended Action |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `ENVELOPE_DRAFT` | The action cannot be performed because the envelope is still in a draft state. | Finalize the envelope configuration and transition it to the `PENDING` (sent) state before attempting this operation. |
|
||||||
|
| `ENVELOPE_COMPLETED` | The action cannot be performed because the envelope is already completed. | No further modifications (e.g., adding signers, modifying documents) can be made to an envelope once the signing process is finished. |
|
||||||
|
| `ENVELOPE_REJECTED` | The action cannot be performed because the envelope was rejected by a recipient. | The signing flow is permanently halted. Create a new envelope if you wish to resubmit the document. |
|
||||||
|
| `ENVELOPE_LEGACY` | The action cannot be performed because the envelope uses an obsolete format. | This envelope was created with a legacy version of the system. Recreate the envelope using the current API version to interact with it. |
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Documents API](/docs/developers/api/documents)
|
||||||
|
- [Webhooks](/docs/developers/webhooks)
|
||||||
@@ -6,6 +6,8 @@ description: Create, manage, and send documents for signing via the API.
|
|||||||
import { Callout } from 'fumadocs-ui/components/callout';
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||||
|
|
||||||
|
<EnvelopeWarning />
|
||||||
|
|
||||||
<Callout type="warn">
|
<Callout type="warn">
|
||||||
This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference,
|
This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference,
|
||||||
see the [OpenAPI Reference](https://openapi.documenso.com).
|
see the [OpenAPI Reference](https://openapi.documenso.com).
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ description: Complete reference for the Documenso REST API.
|
|||||||
|
|
||||||
import { Callout } from 'fumadocs-ui/components/callout';
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
|
|
||||||
|
<EnvelopeWarning />
|
||||||
|
|
||||||
<Callout type="warn">
|
<Callout type="warn">
|
||||||
The guides below cover common API patterns but may not reflect the latest endpoints or parameters.
|
The guides below cover common API patterns but may not reflect the latest endpoints or parameters.
|
||||||
For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com).
|
For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com).
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
{
|
{
|
||||||
"title": "API Reference",
|
"title": "API Reference",
|
||||||
"pages": ["documents", "recipients", "fields", "templates", "teams", "rate-limits", "versioning", "developer-mode"]
|
"pages": [
|
||||||
|
"documents",
|
||||||
|
"recipients",
|
||||||
|
"fields",
|
||||||
|
"templates",
|
||||||
|
"teams",
|
||||||
|
"rate-limits",
|
||||||
|
"versioning",
|
||||||
|
"migrate-to-envelopes",
|
||||||
|
"developer-mode",
|
||||||
|
"common-errors"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
---
|
||||||
|
title: Migrating to Envelopes
|
||||||
|
description: Why Documenso unified documents and templates into envelopes, and how to migrate from the deprecated document and template create endpoints.
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||||
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
|
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||||
|
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The following items have been deprecated and will be removed on the <strong>1st of March 2027</strong>:
|
||||||
|
|
||||||
|
- <strong>API V1</strong>
|
||||||
|
- <strong>A subset of SDK/API V2 endpoints</strong>
|
||||||
|
- <strong>Legacy documents and templates</strong>
|
||||||
|
- <strong>EmbedCreateDocumentV1</strong>
|
||||||
|
- <strong>EmbedCreateTemplateV1</strong>
|
||||||
|
- <strong>EmbedUpdateDocumentV1</strong>
|
||||||
|
- <strong>EmbedUpdateTemplateV1</strong>
|
||||||
|
|
||||||
|
The beta endpoint `/api/v2-beta` will also be removed. Use `/api/v2` instead, which is a drop-in replacement.
|
||||||
|
|
||||||
|
Nothing breaks before 1st of March 2027, so you can migrate at your own pace.
|
||||||
|
|
||||||
|
## What are legacy documents and templates
|
||||||
|
|
||||||
|
These are documents and templates created by the following endpoints:
|
||||||
|
|
||||||
|
- `POST /api/v2/document/create`
|
||||||
|
- `POST /api/v2/document/create/beta`
|
||||||
|
- `POST /api/v2/template/create`
|
||||||
|
- `POST /api/v2/template/create/beta`
|
||||||
|
- `POST /api/v1/documents`
|
||||||
|
- `POST /api/v1/templates`
|
||||||
|
- `POST /api/v1/templates/create-document`
|
||||||
|
- `POST /api/v1/templates/generate-document`
|
||||||
|
|
||||||
|
## What replaces legacy documents and templates
|
||||||
|
|
||||||
|
At the end of 2025 we introduced a unified system for documents and templates, called <strong>envelopes</strong>.
|
||||||
|
|
||||||
|
We still reference documents and templates throughout the documentation and application to distinguish them, but internally they are envelopes.
|
||||||
|
|
||||||
|
Moving to the envelope system gives you:
|
||||||
|
|
||||||
|
- **Multiple PDFs in one envelope.** Send several documents to sign in a single request.
|
||||||
|
- **One API for documents and templates.** Learn one set of endpoints instead of two misaligned ones.
|
||||||
|
- **A better editor and signing experience** for you and your recipients.
|
||||||
|
|
||||||
|
## How to migrate
|
||||||
|
|
||||||
|
{/* prettier-ignore */}
|
||||||
|
<Steps>
|
||||||
|
<Step>
|
||||||
|
### Switch to the envelope endpoints
|
||||||
|
|
||||||
|
Replace each deprecated endpoint with its `/api/v2/envelope/*` equivalent from the [mapping tables](#endpoint-mapping-reference) below.
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
### Set the envelope `type` on create
|
||||||
|
|
||||||
|
A single endpoint, `POST /api/v2/envelope/create`, can create both documents and templates. Set `type` to `DOCUMENT` or `TEMPLATE`. You can now upload more than one PDF using the `files` field.
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
### Update how you store IDs
|
||||||
|
|
||||||
|
Envelope IDs are **strings** (for example `envelope_abc123`), not numbers. Update any code that stores, parses, or compares IDs.
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
### Test, then remove the old calls
|
||||||
|
|
||||||
|
Verify the new flow against your account, then delete the deprecated calls.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
The main data differences are as follows:
|
||||||
|
- ID format changed from number to string (e.g. `42` to `envelope_abc123`)
|
||||||
|
- pageNumber becomes page
|
||||||
|
- pageX becomes positionX
|
||||||
|
- pageY becomes positionY
|
||||||
|
|
||||||
|
See the [Documents API](/docs/developers/api/documents) and [Templates API](/docs/developers/api/templates) for the full envelope reference.
|
||||||
|
|
||||||
|
### Deprecated V1 API Endpoints
|
||||||
|
|
||||||
|
Full reference in the [V1 OpenAPI reference](https://openapi-v1.documenso.com).
|
||||||
|
|
||||||
|
| Deprecated endpoint | Replacement |
|
||||||
|
| -------------------------------------------------------- | ----------------------------------------------------- |
|
||||||
|
| `GET /api/v1/documents` | `GET /api/v2/envelope` |
|
||||||
|
| `GET /api/v1/documents/{id}` | `GET /api/v2/envelope/{envelopeId}` |
|
||||||
|
| `POST /api/v1/documents` | `POST /api/v2/envelope/create` |
|
||||||
|
| `POST /api/v1/documents/{id}/send` | `POST /api/v2/envelope/distribute` |
|
||||||
|
| `POST /api/v1/documents/{id}/resend` | `POST /api/v2/envelope/redistribute` |
|
||||||
|
| `DELETE /api/v1/documents/{id}` | `POST /api/v2/envelope/delete` |
|
||||||
|
| `GET /api/v1/documents/{id}/download` | `GET /api/v2/envelope/item/{envelopeItemId}/download` |
|
||||||
|
| `POST /api/v1/documents/{id}/recipients` | `POST /api/v2/envelope/recipient/create-many` |
|
||||||
|
| `PATCH /api/v1/documents/{id}/recipients/{recipientId}` | `POST /api/v2/envelope/recipient/update-many` |
|
||||||
|
| `DELETE /api/v1/documents/{id}/recipients/{recipientId}` | `POST /api/v2/envelope/recipient/delete` |
|
||||||
|
| `POST /api/v1/documents/{id}/fields` | `POST /api/v2/envelope/field/create-many` |
|
||||||
|
| `PATCH /api/v1/documents/{id}/fields/{fieldId}` | `POST /api/v2/envelope/field/update-many` |
|
||||||
|
| `DELETE /api/v1/documents/{id}/fields/{fieldId}` | `POST /api/v2/envelope/field/delete` |
|
||||||
|
| `GET /api/v1/templates` | `GET /api/v2/envelope` (with `type=TEMPLATE`) |
|
||||||
|
| `GET /api/v1/templates/{id}` | `GET /api/v2/envelope/{envelopeId}` |
|
||||||
|
| `POST /api/v1/templates` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) |
|
||||||
|
| `DELETE /api/v1/templates/{id}` | `POST /api/v2/envelope/delete` |
|
||||||
|
| `POST /api/v1/templates/{templateId}/create-document` | `POST /api/v2/envelope/use` |
|
||||||
|
| `POST /api/v1/templates/{templateId}/generate-document` | `POST /api/v2/envelope/use` |
|
||||||
|
|
||||||
|
### Deprecated V2 API Endpoints
|
||||||
|
|
||||||
|
Full reference in the [V2 OpenAPI reference](https://openapi.documenso.com).
|
||||||
|
|
||||||
|
#### Documents
|
||||||
|
|
||||||
|
| Deprecated endpoint | Replacement |
|
||||||
|
| ------------------------------------------------- | ----------------------------------------------------- |
|
||||||
|
| `GET /api/v2/document` | `GET /api/v2/envelope` |
|
||||||
|
| `GET /api/v2/document/{documentId}` | `GET /api/v2/envelope/{envelopeId}` |
|
||||||
|
| `POST /api/v2/document/get-many` | `POST /api/v2/envelope/get-many` |
|
||||||
|
| `POST /api/v2/document/create` | `POST /api/v2/envelope/create` |
|
||||||
|
| `POST /api/v2/document/create/beta` | `POST /api/v2/envelope/create` |
|
||||||
|
| `POST /api/v2/document/update` | `POST /api/v2/envelope/update` |
|
||||||
|
| `POST /api/v2/document/delete` | `POST /api/v2/envelope/delete` |
|
||||||
|
| `POST /api/v2/document/duplicate` | `POST /api/v2/envelope/duplicate` |
|
||||||
|
| `POST /api/v2/document/distribute` | `POST /api/v2/envelope/distribute` |
|
||||||
|
| `POST /api/v2/document/redistribute` | `POST /api/v2/envelope/redistribute` |
|
||||||
|
| `GET /api/v2/document/attachment` | `GET /api/v2/envelope/attachment` |
|
||||||
|
| `POST /api/v2/document/attachment/create` | `POST /api/v2/envelope/attachment/create` |
|
||||||
|
| `POST /api/v2/document/attachment/update` | `POST /api/v2/envelope/attachment/update` |
|
||||||
|
| `POST /api/v2/document/attachment/delete` | `POST /api/v2/envelope/attachment/delete` |
|
||||||
|
| `GET /api/v2/document/{documentId}/download` | `GET /api/v2/envelope/item/{envelopeItemId}/download` |
|
||||||
|
| `GET /api/v2/document/{documentId}/download-beta` | `GET /api/v2/envelope/item/{envelopeItemId}/download` |
|
||||||
|
|
||||||
|
#### Templates
|
||||||
|
|
||||||
|
| Deprecated endpoint | Replacement |
|
||||||
|
| ------------------------------------- | ------------------------------------------------ |
|
||||||
|
| `GET /api/v2/template` | `GET /api/v2/envelope` (with `type=TEMPLATE`) |
|
||||||
|
| `GET /api/v2/template/{templateId}` | `GET /api/v2/envelope/{envelopeId}` |
|
||||||
|
| `POST /api/v2/template/get-many` | `POST /api/v2/envelope/get-many` |
|
||||||
|
| `POST /api/v2/template/create` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) |
|
||||||
|
| `POST /api/v2/template/create/beta` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) |
|
||||||
|
| `POST /api/v2/template/update` | `POST /api/v2/envelope/update` |
|
||||||
|
| `POST /api/v2/template/duplicate` | `POST /api/v2/envelope/duplicate` |
|
||||||
|
| `POST /api/v2/template/delete` | `POST /api/v2/envelope/delete` |
|
||||||
|
| `POST /api/v2/template/use` | `POST /api/v2/envelope/use` |
|
||||||
|
| `POST /api/v2/template/direct/create` | **Pending replacement** |
|
||||||
|
| `POST /api/v2/template/direct/delete` | **Pending replacement** |
|
||||||
|
| `POST /api/v2/template/direct/toggle` | **Pending replacement** |
|
||||||
|
|
||||||
|
#### Document fields
|
||||||
|
|
||||||
|
| Deprecated endpoint | Replacement |
|
||||||
|
| ----------------------------------------- | ----------------------------------------- |
|
||||||
|
| `GET /api/v2/document/field/{fieldId}` | `GET /api/v2/envelope/field/{fieldId}` |
|
||||||
|
| `POST /api/v2/document/field/create` | `POST /api/v2/envelope/field/create-many` |
|
||||||
|
| `POST /api/v2/document/field/create-many` | `POST /api/v2/envelope/field/create-many` |
|
||||||
|
| `POST /api/v2/document/field/update` | `POST /api/v2/envelope/field/update-many` |
|
||||||
|
| `POST /api/v2/document/field/update-many` | `POST /api/v2/envelope/field/update-many` |
|
||||||
|
| `POST /api/v2/document/field/delete` | `POST /api/v2/envelope/field/delete` |
|
||||||
|
|
||||||
|
#### Template fields
|
||||||
|
|
||||||
|
| Deprecated endpoint | Replacement |
|
||||||
|
| ----------------------------------------- | ----------------------------------------- |
|
||||||
|
| `GET /api/v2/template/field/{fieldId}` | `GET /api/v2/envelope/field/{fieldId}` |
|
||||||
|
| `POST /api/v2/template/field/create` | `POST /api/v2/envelope/field/create-many` |
|
||||||
|
| `POST /api/v2/template/field/create-many` | `POST /api/v2/envelope/field/create-many` |
|
||||||
|
| `POST /api/v2/template/field/update` | `POST /api/v2/envelope/field/update-many` |
|
||||||
|
| `POST /api/v2/template/field/update-many` | `POST /api/v2/envelope/field/update-many` |
|
||||||
|
| `POST /api/v2/template/field/delete` | `POST /api/v2/envelope/field/delete` |
|
||||||
|
|
||||||
|
#### Document recipients
|
||||||
|
|
||||||
|
| Deprecated endpoint | Replacement |
|
||||||
|
| ---------------------------------------------- | ---------------------------------------------- |
|
||||||
|
| `GET /api/v2/document/recipient/{recipientId}` | `GET /api/v2/envelope/recipient/{recipientId}` |
|
||||||
|
| `POST /api/v2/document/recipient/create` | `POST /api/v2/envelope/recipient/create-many` |
|
||||||
|
| `POST /api/v2/document/recipient/create-many` | `POST /api/v2/envelope/recipient/create-many` |
|
||||||
|
| `POST /api/v2/document/recipient/update` | `POST /api/v2/envelope/recipient/update-many` |
|
||||||
|
| `POST /api/v2/document/recipient/update-many` | `POST /api/v2/envelope/recipient/update-many` |
|
||||||
|
| `POST /api/v2/document/recipient/delete` | `POST /api/v2/envelope/recipient/delete` |
|
||||||
|
|
||||||
|
#### Template recipients
|
||||||
|
|
||||||
|
| Deprecated endpoint | Replacement |
|
||||||
|
| ---------------------------------------------- | ---------------------------------------------- |
|
||||||
|
| `GET /api/v2/template/recipient/{recipientId}` | `GET /api/v2/envelope/recipient/{recipientId}` |
|
||||||
|
| `POST /api/v2/template/recipient/create` | `POST /api/v2/envelope/recipient/create-many` |
|
||||||
|
| `POST /api/v2/template/recipient/create-many` | `POST /api/v2/envelope/recipient/create-many` |
|
||||||
|
| `POST /api/v2/template/recipient/update` | `POST /api/v2/envelope/recipient/update-many` |
|
||||||
|
| `POST /api/v2/template/recipient/update-many` | `POST /api/v2/envelope/recipient/update-many` |
|
||||||
|
| `POST /api/v2/template/recipient/delete` | `POST /api/v2/envelope/recipient/delete` |
|
||||||
|
|
||||||
|
### Embedding components
|
||||||
|
|
||||||
|
| Deprecated component | Replacement |
|
||||||
|
| ----------------------- | --------------------- |
|
||||||
|
| `EmbedCreateDocumentV1` | `EmbedCreateEnvelope` |
|
||||||
|
| `EmbedCreateTemplateV1` | `EmbedCreateEnvelope` |
|
||||||
|
| `EmbedUpdateDocumentV1` | `EmbedUpdateEnvelope` |
|
||||||
|
| `EmbedUpdateTemplateV1` | `EmbedUpdateEnvelope` |
|
||||||
|
|
||||||
|
See the [embedding guide](/docs/developers/embedding) for the envelope components.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<Accordions>
|
||||||
|
<Accordion title="What happens on 1 March 2027?">
|
||||||
|
The deprecated V1 API, the V2 endpoints listed above, and the V1 embedding components are removed.
|
||||||
|
Requests to them will fail, so migrate to the envelope API before that date.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Will my existing documents and templates keep working?">
|
||||||
|
Yes. Documents and templates you already created remain in your account and continue to work. They will automatically be converted to envelopes. Only
|
||||||
|
the deprecated endpoints you call are going away. Your data is not deleted.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Do I need a new API token?">
|
||||||
|
No. Authentication is unchanged. The same API token works for the envelope endpoints under
|
||||||
|
`https://app.documenso.com/api/v2`.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="What is the difference between a document and a template now?">
|
||||||
|
Both are envelopes, distinguished by a `type` field of `DOCUMENT` or `TEMPLATE`. They share the same
|
||||||
|
endpoints, recipients, fields, and attachments.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="I use an official SDK, what should I do?">
|
||||||
|
The function calls to the legacy endpoints will break on the 1st of March 2027. Update to the latest SDK version and switch to its envelope methods.
|
||||||
|
The deprecated document and template methods map to the envelope endpoints in the tables above.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="I need more time or help migrating">
|
||||||
|
Reach out to [support@documenso.com](mailto:support@documenso.com) with your use case and we will
|
||||||
|
help you plan the migration.
|
||||||
|
</Accordion>
|
||||||
|
</Accordions>
|
||||||
|
|
||||||
|
## Getting help
|
||||||
|
|
||||||
|
- [V2 OpenAPI reference](https://openapi.documenso.com): the up-to-date envelope API.
|
||||||
|
- [V1 OpenAPI reference](https://openapi-v1.documenso.com): the deprecated V1 API.
|
||||||
|
- [support@documenso.com](mailto:support@documenso.com): migration questions and extensions.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [Documents API](/docs/developers/api/documents): create and manage envelopes
|
||||||
|
- [Templates API](/docs/developers/api/templates): work with templates and direct links
|
||||||
|
- [Fields API](/docs/developers/api/fields) and [Recipients API](/docs/developers/api/recipients)
|
||||||
|
- [API Versioning](/docs/developers/api/versioning): how Documenso versions the public API
|
||||||
@@ -11,9 +11,14 @@ Documenso enforces rate limits on all API endpoints to ensure service stability.
|
|||||||
|
|
||||||
## HTTP Rate Limits
|
## HTTP Rate Limits
|
||||||
|
|
||||||
**Limit:** 100 requests per minute per IP address
|
**Limit:** 1000 requests per minute per IP address
|
||||||
**Response:** 429 Too Many Requests
|
**Response:** 429 Too Many Requests
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
This is the global per-IP ceiling. Your organisation may have its own rate limits configured below
|
||||||
|
this value, in which case you can be rate-limited before reaching the global limit.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
### Rate Limit Response
|
### Rate Limit Response
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -65,3 +70,4 @@ When you exceed a resource limit:
|
|||||||
- [Authentication](/docs/developers/getting-started/authentication) - API authentication guide
|
- [Authentication](/docs/developers/getting-started/authentication) - API authentication guide
|
||||||
- [API Versioning](/docs/developers/api/versioning) - API version management
|
- [API Versioning](/docs/developers/api/versioning) - API version management
|
||||||
- [First API Call](/docs/developers/getting-started/first-api-call) - Getting started with the API
|
- [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)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ description: Create documents from reusable templates via API.
|
|||||||
import { Callout } from 'fumadocs-ui/components/callout';
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||||
|
|
||||||
|
<EnvelopeWarning />
|
||||||
|
|
||||||
<Callout type="warn">
|
<Callout type="warn">
|
||||||
This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference,
|
This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference,
|
||||||
see the [OpenAPI Reference](https://openapi.documenso.com).
|
see the [OpenAPI Reference](https://openapi.documenso.com).
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ description: Versioning information for the Documenso public API.
|
|||||||
|
|
||||||
import { Callout } from 'fumadocs-ui/components/callout';
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
|
|
||||||
|
<EnvelopeWarning />
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Documenso uses API versioning to manage changes to the public API. This allows us to introduce new features, fix bugs, and make other changes without breaking existing integrations.
|
Documenso uses API versioning to manage changes to the public API. This allows us to introduce new features, fix bugs, and make other changes without breaking existing integrations.
|
||||||
@@ -19,7 +21,16 @@ Also, we may deprecate certain features or endpoints in the API. When we depreca
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Documents, Templates, and Envelopes
|
||||||
|
|
||||||
|
Documenso has unified documents and templates into a single resource called an **envelope**. New integrations should create documents and templates through the `/envelope/*` endpoints. The `POST /document/create` and `POST /template/create` endpoints (including their `/beta` variants) are deprecated in favor of `POST /envelope/create`.
|
||||||
|
|
||||||
|
See [Migrating to the Envelope API](/docs/developers/api/migrate-to-envelopes) for the rationale and step-by-step migration examples.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
|
- [Migrating to the Envelope API](/docs/developers/api/migrate-to-envelopes) - Move from the document and template create endpoints
|
||||||
- [Authentication](/docs/developers/getting-started/authentication) - API authentication guide
|
- [Authentication](/docs/developers/getting-started/authentication) - API authentication guide
|
||||||
- [Rate Limits](/docs/developers/api/rate-limits) - API rate limit details
|
- [Rate Limits](/docs/developers/api/rate-limits) - API rate limit details
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
title: iframe
|
||||||
|
description: Embed the signing experience directly in your application using an iframe.
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
|
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||||
|
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||||
|
|
||||||
|
<Callout type="warn" title="iframes are not recommended">
|
||||||
|
Embedding via iframe is not recommended. We strongly recommend using the [official SDKs](/docs/developers/embedding/sdks) instead.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
### Basic iframe Embedding
|
||||||
|
|
||||||
|
```html
|
||||||
|
<iframe
|
||||||
|
src="https://app.documenso.com/embed/sign/abc123xyz"
|
||||||
|
width="100%"
|
||||||
|
height="800"
|
||||||
|
frameborder="0"
|
||||||
|
allow="clipboard-write"
|
||||||
|
></iframe>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout title="Use the correct embed URL">
|
||||||
|
The URL you embed depends on the embed mode you’re using (for example direct links vs sign-token embeds). Use the
|
||||||
|
embed URL provided by Documenso for your flow.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
### iframe Customization
|
||||||
|
|
||||||
|
You can customize the embedded signing experience by passing **encoded options in the iframe URL fragment** (everything
|
||||||
|
after `#`).
|
||||||
|
|
||||||
|
Documenso expects the fragment to be **base64** of:
|
||||||
|
|
||||||
|
- `encodeURIComponent(JSON.stringify(options))`
|
||||||
|
|
||||||
|
#### Supported options
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
| ------ | ---- | ----------- |
|
||||||
|
| `name` | `string` | Prefill signer name. |
|
||||||
|
| `email` | `string` | Prefill signer email. |
|
||||||
|
| `lockName` | `boolean` | Lock the name field (prevents editing). |
|
||||||
|
| `lockEmail` | `boolean` | Lock the email field (prevents editing). |
|
||||||
|
| `language` | `string` | Force the embed language (e.g. `en`). |
|
||||||
|
| `darkModeDisabled` | `boolean` | Disable dark mode behavior. |
|
||||||
|
| `allowDocumentRejection` | `boolean` | Allow or disallow document rejection. |
|
||||||
|
| `css` | `string` | Inject custom CSS into the embed. |
|
||||||
|
| `cssVars` | `object` | Override embed CSS variables (see the CSS Variables page). |
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const buildEmbedSrc = (host: string, token: string) => {
|
||||||
|
const options = {
|
||||||
|
name: 'Ada Lovelace',
|
||||||
|
email: 'ada@example.com',
|
||||||
|
lockName: true,
|
||||||
|
lockEmail: true,
|
||||||
|
language: 'en',
|
||||||
|
darkModeDisabled: false,
|
||||||
|
allowDocumentRejection: true,
|
||||||
|
css: ':root { --radius: 12px; }',
|
||||||
|
cssVars: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodedOptions = btoa(encodeURIComponent(JSON.stringify(options)));
|
||||||
|
|
||||||
|
return `${new URL(`/embed/sign/${token}`, host).toString()}#${encodedOptions}`;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
A complete example can be found in the [Embeds repository](https://github.com/documenso/embeds/blob/main/packages/mitosis/src/sign-document.lite.tsx).
|
||||||
|
|
||||||
|
<Callout type="info" title="Why use the URL fragment?">
|
||||||
|
The fragment is **not sent to the server** as part of the HTTP request, but it is available to the embedded app in
|
||||||
|
the browser. This makes it a convenient way to pass client-side configuration without changing the base embed URL.
|
||||||
|
</Callout>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"title": "Embedding",
|
"title": "Embedding",
|
||||||
"pages": ["sdks", "direct-links", "css-variables", "editor"]
|
"pages": ["sdks", "direct-links", "css-variables", "editor", "iframe"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { Callout } from 'fumadocs-ui/components/callout';
|
|||||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||||
|
|
||||||
|
<EnvelopeWarning />
|
||||||
|
|
||||||
## Workflow 1: Send a Document for Signature
|
## Workflow 1: Send a Document for Signature
|
||||||
|
|
||||||
The most common workflow: upload a PDF, add recipients with signature fields, and send for signing.
|
The most common workflow: upload a PDF, add recipients with signature fields, and send for signing.
|
||||||
@@ -472,7 +474,7 @@ Send the same document to multiple recipients in parallel. Useful for policy ack
|
|||||||
<code>distributeDocument: true</code>
|
<code>distributeDocument: true</code>
|
||||||
</Step>
|
</Step>
|
||||||
<Step>
|
<Step>
|
||||||
Process in batches with a short delay to respect rate limits (e.g. 100 requests/minute)
|
Process in batches with a short delay to respect rate limits (e.g. 1000 requests/minute)
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
@@ -638,8 +640,8 @@ done
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
The API allows 100 requests per minute. For large batches, implement rate limiting with delays
|
The API allows 1000 requests per minute (your organisation may have its own lower limit). For large
|
||||||
between requests to avoid hitting limits.
|
batches, implement rate limiting with delays between requests to avoid hitting limits.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ title: Examples
|
|||||||
description: Common integration patterns and end-to-end workflows.
|
description: Common integration patterns and end-to-end workflows.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<EnvelopeWarning />
|
||||||
|
|
||||||
<Cards>
|
<Cards>
|
||||||
<Card
|
<Card
|
||||||
title="Common Workflows"
|
title="Common Workflows"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
|||||||
import { Callout } from 'fumadocs-ui/components/callout';
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||||
|
|
||||||
|
<EnvelopeWarning />
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- A Documenso account (cloud or self-hosted)
|
- A Documenso account (cloud or self-hosted)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { Callout } from 'fumadocs-ui/components/callout';
|
|||||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||||
|
|
||||||
|
<EnvelopeWarning />
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Before starting, you need:
|
Before starting, you need:
|
||||||
@@ -483,7 +485,7 @@ The API returns standard HTTP status codes and JSON error responses:
|
|||||||
|
|
||||||
### Handling Rate Limits
|
### Handling Rate Limits
|
||||||
|
|
||||||
The API allows 100 requests per minute per IP address. When rate limited, wait at least 60 seconds before retrying:
|
The API allows 1000 requests per minute per IP address. Your organisation may have its own lower rate limits. When rate limited, wait at least 60 seconds before retrying:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
async function fetchWithRetry(url, options, maxRetries = 3) {
|
async function fetchWithRetry(url, options, maxRetries = 3) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ title: Getting Started
|
|||||||
description: Get your API key and make your first API call.
|
description: Get your API key and make your first API call.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<EnvelopeWarning />
|
||||||
|
|
||||||
<Cards>
|
<Cards>
|
||||||
<Card
|
<Card
|
||||||
title="Authentication"
|
title="Authentication"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ title: Developer Guide
|
|||||||
description: Integrate Documenso into your applications using the REST API, webhooks, and embedding options.
|
description: Integrate Documenso into your applications using the REST API, webhooks, and embedding options.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<EnvelopeWarning />
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
<Cards>
|
<Cards>
|
||||||
|
|||||||
@@ -15,16 +15,17 @@ Pick the one that fits your needs the best.
|
|||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- [Typescript](https://www.typescriptlang.org/) - Language
|
- [TypeScript](https://www.typescriptlang.org/) - Language
|
||||||
- [React Router](https://reactrouter.com/) - Framework
|
- [React Router v7](https://reactrouter.com/) - Framework
|
||||||
|
- [Hono](https://hono.dev/) - Server
|
||||||
- [Prisma](https://www.prisma.io/) - ORM
|
- [Prisma](https://www.prisma.io/) - ORM
|
||||||
- [Tailwind](https://tailwindcss.com/) - CSS
|
- [Tailwind CSS](https://tailwindcss.com/) - CSS
|
||||||
- [shadcn/ui](https://ui.shadcn.com/) - Component Library
|
- [shadcn/ui](https://ui.shadcn.com/) + [Radix UI](https://www.radix-ui.com/) - Component Library
|
||||||
- [react-email](https://react.email/) - Email Templates
|
- [react-email](https://react.email/) - Email Templates
|
||||||
|
- [Lingui](https://lingui.dev/) - Internationalization
|
||||||
- [tRPC](https://trpc.io/) - API
|
- [tRPC](https://trpc.io/) - API
|
||||||
- [@documenso/pdf-sign](https://www.npmjs.com/package/@documenso/pdf-sign) - PDF Signatures
|
- [@libpdf/core](https://www.npmjs.com/package/@libpdf/core) - PDF Signing and Manipulation
|
||||||
- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs
|
- [pdf.js](https://mozilla.github.io/pdf.js/) - Viewing PDFs
|
||||||
- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation
|
|
||||||
- [Stripe](https://stripe.com/) - Payments
|
- [Stripe](https://stripe.com/) - Payments
|
||||||
|
|
||||||
<div className="mt-16 flex items-center justify-center gap-4">
|
<div className="mt-16 flex items-center justify-center gap-4">
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ The Enterprise Edition is required when you:
|
|||||||
4. Restart your Documenso instance
|
4. Restart your Documenso instance
|
||||||
5. Verify the license is active in the **Admin Panel** under the **Stats** section
|
5. Verify the license is active in the **Admin Panel** under the **Stats** section
|
||||||
|
|
||||||
|
See [Apply Your License Key](/docs/self-hosting/configuration/license) for the full walkthrough, including how to enable individual features once licensed.
|
||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</Accordions>
|
</Accordions>
|
||||||
|
|
||||||
@@ -197,7 +199,7 @@ See [Support](/docs/policies/support) for complete support options.
|
|||||||
1. Sign the Enterprise license agreement
|
1. Sign the Enterprise license agreement
|
||||||
2. Receive license key and access credentials
|
2. Receive license key and access credentials
|
||||||
3. Deploy using [self-hosting guides](/docs/self-hosting) or access Documenso Cloud
|
3. Deploy using [self-hosting guides](/docs/self-hosting) or access Documenso Cloud
|
||||||
4. Configure Enterprise features with support assistance
|
4. Apply the key — see [Apply Your License Key](/docs/self-hosting/configuration/license) — and configure Enterprise features with support assistance
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step>
|
<Step>
|
||||||
@@ -238,6 +240,7 @@ See [Support](/docs/policies/support) for complete support options.
|
|||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
- [Apply Your License Key](/docs/self-hosting/configuration/license) - Step-by-step license activation
|
||||||
- [Community Edition](/docs/policies/community-edition) - AGPL-3.0 open-source license
|
- [Community Edition](/docs/policies/community-edition) - AGPL-3.0 open-source license
|
||||||
- [Licenses](/docs/policies/licenses) - Complete licensing overview and FAQ
|
- [Licenses](/docs/policies/licenses) - Complete licensing overview and FAQ
|
||||||
- [Support](/docs/policies/support) - Support channels and response times
|
- [Support](/docs/policies/support) - Support channels and response times
|
||||||
|
|||||||
@@ -41,12 +41,17 @@ When a limit is reached, requests return a `429 Too Many Requests` response with
|
|||||||
|
|
||||||
| Action | Limit | Window |
|
| Action | Limit | Window |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| API requests (v1 and v2) | 100 requests | 1 minute |
|
| API requests (v1 and v2) | 1000 requests | 1 minute |
|
||||||
| File uploads | 20 requests | 1 minute |
|
| File uploads | 20 requests | 1 minute |
|
||||||
| AI features | 3 requests | 1 minute |
|
| AI features | 3 requests | 1 minute |
|
||||||
|
|
||||||
Authentication endpoints (login, signup, password reset, etc.) are also rate-limited to protect against abuse.
|
Authentication endpoints (login, signup, password reset, etc.) are also rate-limited to protect against abuse.
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
The API request limit above is the global per-IP ceiling. Individual organisations also have their
|
||||||
|
own rate limits, which may be configured below this value.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
Rate limits may vary by plan. Enterprise plans can include higher or custom limits. Contact
|
Rate limits may vary by plan. Enterprise plans can include higher or custom limits. Contact
|
||||||
[sales](https://documen.so/sales) for details.
|
[sales](https://documen.so/sales) for details.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"privacy",
|
"privacy",
|
||||||
"terms",
|
"terms",
|
||||||
"security",
|
"security",
|
||||||
|
"verify-email",
|
||||||
"support"
|
"support"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
title: Verifying Emails from Documenso
|
||||||
|
description: How to confirm that an email is genuinely from Documenso, and what to do if you receive a suspicious message.
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
|
|
||||||
|
## Check the Sender Domain
|
||||||
|
|
||||||
|
All email sent by Documenso originates from one of the following domains. If you receive an email claiming to be from Documenso and the sender address does not end in one of these domains, treat it as suspicious.
|
||||||
|
|
||||||
|
| Domain | Used for |
|
||||||
|
| ------------------------ | -------------------------------------------------------------- |
|
||||||
|
| `app.documenso.com` | Transactional email |
|
||||||
|
| `documensomail.com` | Transactional email |
|
||||||
|
| `documensoemail.com` | Transactional email |
|
||||||
|
| Custom domain | [Enterprise organisations](/docs/users/organisations/email-domains) using a custom email domain |
|
||||||
|
|
||||||
|
Typical sender addresses include:
|
||||||
|
|
||||||
|
- `noreply@app.documenso.com`
|
||||||
|
- `noreply@free.documensomail.com`
|
||||||
|
- `noreply@send.documensoemail.com`
|
||||||
|
|
||||||
|
<Callout type="warn">
|
||||||
|
A misspelling such as `documenso-email.com`, `documensoemaiI.com` (capital i instead of l), or any other variation is not a Documenso domain.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Types of Email Documenso Sends
|
||||||
|
|
||||||
|
Documenso sends email only for the following purposes:
|
||||||
|
|
||||||
|
- **Account verification** — confirming your email address when you sign up or change it
|
||||||
|
- **Password reset** — a link to reset your password that you requested
|
||||||
|
- **Document invitations** — notifying you that a document has been shared with you to sign, approve, or view
|
||||||
|
- **Signing reminders** — follow-up reminders for pending document actions
|
||||||
|
- **Completed document notifications** — confirmation that all parties have signed a document
|
||||||
|
- **Team invitations** — inviting you to join an organisation or team
|
||||||
|
|
||||||
|
## What Documenso Will Never Do
|
||||||
|
|
||||||
|
- Ask for your password via email
|
||||||
|
- Send you an attachment and ask you to open it to verify your identity
|
||||||
|
- Ask you to confirm payment details or billing information over email
|
||||||
|
- Send unsolicited marketing emails if you have not opted in
|
||||||
|
|
||||||
|
## How to Tell If an Email Is Legitimate
|
||||||
|
|
||||||
|
1. **Check the sender address** — the domain must be `documenso.com` or `documensomail.com`
|
||||||
|
2. **Look at the link destination** — hover over any link before clicking; it should point to `app.documenso.com`
|
||||||
|
3. **Watch for urgency or threats** — legitimate Documenso emails do not threaten account suspension to pressure you into clicking a link immediately
|
||||||
|
4. **Verify the action yourself** — if in doubt, log in to [app.documenso.com](https://app.documenso.com) directly (not via the email link) and check whether the document or notification exists there
|
||||||
|
|
||||||
|
## Report a Suspicious Email
|
||||||
|
|
||||||
|
If you receive an email that appears to impersonate Documenso:
|
||||||
|
|
||||||
|
1. Do not click any links or download any attachments
|
||||||
|
2. Forward the email as an attachment to **support@documenso.com**
|
||||||
|
3. Delete the email from your inbox
|
||||||
|
|
||||||
|
You can also report phishing emails directly to your email provider using their built-in reporting tools.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Security Policy](/docs/policies/security) — Documenso's security practices and vulnerability disclosure process
|
||||||
|
- [Create an Account](/docs/users/getting-started/create-account) — What to expect during sign-up
|
||||||
|
- [Security Settings](/docs/users/settings/security) — Enable two-factor authentication and manage sessions
|
||||||
@@ -278,7 +278,9 @@ Test your email configuration by creating an account or resetting a password. Th
|
|||||||
|
|
||||||
### Using a Test SMTP Server
|
### Using a Test SMTP Server
|
||||||
|
|
||||||
For development or testing, use a local SMTP server like [Mailhog](https://github.com/mailhog/MailHog) or [Mailpit](https://github.com/axllent/mailpit):
|
For development or testing, use a local SMTP server like [Inbucket](https://www.inbucket.org/), [Mailpit](https://github.com/axllent/mailpit), or [Mailhog](https://github.com/mailhog/MailHog). The default development setup (`docker/development/compose.yml`) already runs Inbucket, with its web UI on port 9000 and SMTP on port 2500.
|
||||||
|
|
||||||
|
To run one standalone instead:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Using Docker
|
# Using Docker
|
||||||
|
|||||||
@@ -86,6 +86,21 @@ Callback URL: `https://<your-domain>/api/auth/callback/microsoft`
|
|||||||
| `NEXT_PRIVATE_OIDC_SKIP_VERIFY` | `false` | Skip email verification for OIDC accounts |
|
| `NEXT_PRIVATE_OIDC_SKIP_VERIFY` | `false` | Skip email verification for OIDC accounts |
|
||||||
| `NEXT_PRIVATE_OIDC_PROMPT` | `login` | OIDC prompt parameter. Set to empty string to omit |
|
| `NEXT_PRIVATE_OIDC_PROMPT` | `login` | OIDC prompt parameter. Set to empty string to omit |
|
||||||
|
|
||||||
|
### Webhooks
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| --------------------------------------- | ------- | ------------------------------------------------------------------------ |
|
||||||
|
| `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` | - | Comma-separated hostnames or IPs allowed to resolve to private addresses |
|
||||||
|
|
||||||
|
Before delivering a webhook, Documenso checks whether the target resolves to a
|
||||||
|
private or loopback address and blocks it if so. This check is best-effort and
|
||||||
|
fails open. Use `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` to allow specific
|
||||||
|
internal hosts, for example when delivering to a service on your own network:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS="hooks.internal.example,10.0.0.5"
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Email Configuration
|
## Email Configuration
|
||||||
@@ -186,9 +201,9 @@ Documenso requires a certificate to digitally sign documents.
|
|||||||
|
|
||||||
### Transport Selection
|
### Transport Selection
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
| -------------------------------- | ---------------------------------------- | ------- |
|
| -------------------------------- | ------------------------------------------------- | ------- |
|
||||||
| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Signing backend: `local` or `gcloud-hsm` | `local` |
|
| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Signing backend: `local`, `gcloud-hsm`, or `csc` | `local` |
|
||||||
|
|
||||||
### Local Signing
|
### Local Signing
|
||||||
|
|
||||||
@@ -210,11 +225,36 @@ Documenso requires a certificate to digitally sign documents.
|
|||||||
| `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS` | Base64-encoded certificate chain |
|
| `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS` | Base64-encoded certificate chain |
|
||||||
| `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH` | Google Secret Manager path for certificate retrieval |
|
| `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH` | Google Secret Manager path for certificate retrieval |
|
||||||
|
|
||||||
|
### Cloud Signature Consortium (CSC)
|
||||||
|
|
||||||
|
Routes signing through a third-party Trust Service Provider for Advanced and Qualified Electronic Signatures (AES/QES). Instance-wide; set `NEXT_PRIVATE_SIGNING_TRANSPORT=csc` to enable. See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup walkthrough.
|
||||||
|
|
||||||
|
CSC mode requires an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Without a valid license, the instance will refuse to start in `csc` mode.
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||||
|
| `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | Base URL of the CSC provider's API | |
|
||||||
|
| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID` | OAuth client ID registered with the CSC provider | |
|
||||||
|
| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET` | OAuth client secret registered with the CSC provider | |
|
||||||
|
| `NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` | Default legal tier for new envelopes when the caller doesn't specify one. `AES` or `QES`. Explicit requests pass through. | `AES` |
|
||||||
|
|
||||||
|
The OAuth callback URL registered with the CSC provider is fixed at `${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback` — register this exact URL with the TSP.
|
||||||
|
|
||||||
|
#### Derived Public Variables
|
||||||
|
|
||||||
|
The following client-visible variable is **derived automatically** from the private transport at server startup. Do not set it manually — any value set in the environment is overwritten on boot.
|
||||||
|
|
||||||
|
| Variable | Derived from | Value |
|
||||||
|
| ------------------------------------- | -------------------------------------------------- | ------------------------------------------------- |
|
||||||
|
| `NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC` | `NEXT_PRIVATE_SIGNING_TRANSPORT === 'csc'` | `'true'` when CSC mode is active, else `'false'` |
|
||||||
|
|
||||||
|
The authoring UI uses this flag to gate features that AES/QES envelopes cannot support (parallel signing, assistant role, dictate next signer). Deriving it from the private transport prevents the client-side flag from drifting from the real server-side configuration.
|
||||||
|
|
||||||
### Signature Options
|
### Signature Options
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
| ------------------------------------------- | ----------------------------------------------------------- | ---------- |
|
| ------------------------------------------- | ----------------------------------------------------------- | ---------- |
|
||||||
| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | Comma-separated timestamp authority URLs for LTV signatures | |
|
| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | Comma-separated timestamp authority URLs for LTV signatures. Optional for `local` / `gcloud-hsm` (signatures omit the timestamp when unset). **Required** when `NEXT_PRIVATE_SIGNING_TRANSPORT=csc` — the instance refuses to start without it. See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes#timestamp-authority-resolution). | |
|
||||||
| `NEXT_PUBLIC_SIGNING_CONTACT_INFO` | Contact info embedded in PDF signatures | Webapp URL |
|
| `NEXT_PUBLIC_SIGNING_CONTACT_INFO` | Contact info embedded in PDF signatures | Webapp URL |
|
||||||
| `NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER` | Use `adbe.pkcs7.detached` instead of `ETSI.CAdES.detached` | `false` |
|
| `NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER` | Use `adbe.pkcs7.detached` instead of `ETSI.CAdES.detached` | `false` |
|
||||||
|
|
||||||
@@ -232,6 +272,12 @@ For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/con
|
|||||||
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft. Existing linked users can still sign in | `false` |
|
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft. Existing linked users can still sign in | `false` |
|
||||||
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC, including the organisation portal | `false` |
|
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC, including the organisation portal | `false` |
|
||||||
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | |
|
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch. Disable all signin methods application-wide | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin. Also closes `/forgot-password` and `/reset-password` | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN` | Hide the Microsoft signin button | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable the automatic `/signin` redirect when OIDC is the only enabled transport | `false` |
|
||||||
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | |
|
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | |
|
||||||
| `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` |
|
| `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` |
|
||||||
|
|
||||||
@@ -263,6 +309,44 @@ NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true"
|
|||||||
NEXT_PUBLIC_DISABLE_SIGNUP="true"
|
NEXT_PUBLIC_DISABLE_SIGNUP="true"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sign-in Restrictions
|
||||||
|
|
||||||
|
You can control which methods are available for users to sign in with the following environment variables:
|
||||||
|
|
||||||
|
- **`NEXT_PUBLIC_DISABLE_SIGNIN`** (master switch): Set to `true` to block all signin methods (email/password, Google, Microsoft, OIDC). Hides every signin entry point on `/signin` and rejects email/password signin server-side with a `SIGNIN_DISABLED` error.
|
||||||
|
- **`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN`**: Set to `true` to disable email/password signin only. The email/password form is hidden, the `/forgot-password` and `/reset-password` pages redirect to `/signin`, and the corresponding server endpoints reject requests. SSO signin is unaffected.
|
||||||
|
- **`NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN`**, **`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN`**, **`NEXT_PUBLIC_DISABLE_OIDC_SIGNIN`**: Set to `true` to hide the matching SSO button on the signin page. Useful when an SSO provider is kept configured for account linking but not advertised as a signin entry point.
|
||||||
|
|
||||||
|
These flags are opt-in: when none are set, signin behaviour is unchanged from a stock Documenso instance.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Allow only OIDC signin (e.g. enterprise SSO-only)
|
||||||
|
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true"
|
||||||
|
NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true"
|
||||||
|
NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true"
|
||||||
|
|
||||||
|
# Or disable signin entirely
|
||||||
|
NEXT_PUBLIC_DISABLE_SIGNIN="true"
|
||||||
|
```
|
||||||
|
|
||||||
|
### OIDC Auto-redirect
|
||||||
|
|
||||||
|
When OIDC is the only enabled signin transport on your instance, `/signin` automatically redirects users straight to the OIDC provider instead of showing the signin form. The page renders a spinner while the redirect happens. No extra configuration is required — disabling every other signin method is enough to trigger it.
|
||||||
|
|
||||||
|
- **`NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT`**: Set to `true` to opt out of the automatic redirect and keep rendering the signin page even when OIDC is the only enabled transport.
|
||||||
|
|
||||||
|
The redirect only triggers when OIDC is configured and email/password, Google, and Microsoft signin are all disabled. If any other transport remains enabled, the signin form is shown as normal.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# OIDC-only signin: disabling all other methods auto-redirects to the provider
|
||||||
|
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true"
|
||||||
|
NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true"
|
||||||
|
NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true"
|
||||||
|
|
||||||
|
# Opt out of the auto-redirect while still OIDC-only
|
||||||
|
# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT="true"
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## AI Features
|
## AI Features
|
||||||
@@ -359,11 +443,11 @@ Telemetry collects only: app version, installation ID, and node ID. No personal
|
|||||||
|
|
||||||
## Enterprise Features
|
## Enterprise Features
|
||||||
|
|
||||||
These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance.
|
These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance. See [Apply Your License Key](/docs/self-hosting/configuration/license) for step-by-step setup.
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
| ------------------------------------ | ------------------------------------------------ |
|
| ------------------------------------ | ------------------------------------------------ |
|
||||||
| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features |
|
| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features — see [Apply Your License Key](/docs/self-hosting/configuration/license) for how to apply it |
|
||||||
| `NEXT_PRIVATE_STRIPE_API_KEY` | Stripe API key for billing |
|
| `NEXT_PRIVATE_STRIPE_API_KEY` | Stripe API key for billing |
|
||||||
| `NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET` | Stripe webhook secret |
|
| `NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET` | Stripe webhook secret |
|
||||||
| `NEXT_PRIVATE_SES_ACCESS_KEY_ID` | AWS SES access key for email domain verification |
|
| `NEXT_PRIVATE_SES_ACCESS_KEY_ID` | AWS SES access key for email domain verification |
|
||||||
@@ -406,6 +490,16 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password"
|
|||||||
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true"
|
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true"
|
||||||
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP="true"
|
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP="true"
|
||||||
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
|
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
|
||||||
|
|
||||||
|
# Sign-in restrictions (optional)
|
||||||
|
# NEXT_PUBLIC_DISABLE_SIGNIN="true"
|
||||||
|
# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true"
|
||||||
|
# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true"
|
||||||
|
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true"
|
||||||
|
# NEXT_PUBLIC_DISABLE_OIDC_SIGNIN="true"
|
||||||
|
|
||||||
|
# Opt out of the automatic OIDC redirect when OIDC is the only enabled transport (optional)
|
||||||
|
# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT="true"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -416,4 +510,5 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password"
|
|||||||
- [Email Configuration](/docs/self-hosting/configuration/email) - Configure email delivery
|
- [Email Configuration](/docs/self-hosting/configuration/email) - Configure email delivery
|
||||||
- [Storage Configuration](/docs/self-hosting/configuration/storage) - Set up S3 storage
|
- [Storage Configuration](/docs/self-hosting/configuration/storage) - Set up S3 storage
|
||||||
- [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Configure document signing
|
- [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
|
- [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."
|
description="Digital signature certificate setup."
|
||||||
href="/docs/self-hosting/configuration/signing-certificate"
|
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>
|
</Cards>
|
||||||
|
|
||||||
## Required Configuration
|
## Required Configuration
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
title: Apply Your License Key
|
||||||
|
description: Activate your Enterprise license key to unlock enterprise features on your self-hosted instance.
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||||
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
|
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||||
|
|
||||||
|
A license key activates the Enterprise features available to your self-hosted instance, such as CSC signing, SSO, embed white-labelling, and 21 CFR Part 11 compliance.
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
The license key applies to your **whole instance**, not an individual user account. There's one
|
||||||
|
key per deployment.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- An active Enterprise license key — contact [sales](https://documen.so/enterprise) to set up an
|
||||||
|
Enterprise subscription, then copy your key from [license.documenso.com](https://license.documenso.com).
|
||||||
|
See [Enterprise Edition](/docs/policies/enterprise-edition) for details.
|
||||||
|
- A running self-hosted Documenso instance that you're able to restart
|
||||||
|
|
||||||
|
## Step 1: Set the environment variable
|
||||||
|
|
||||||
|
Set `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` to your license key.
|
||||||
|
|
||||||
|
<Tabs items={['Docker Compose', 'docker run', '.env']}>
|
||||||
|
<Tab value="Docker Compose">
|
||||||
|
|
||||||
|
Add the variable to your `.env` file (or directly under `environment:` in `compose.yml`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
Then apply it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab value="docker run">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name documenso \
|
||||||
|
-e NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here \
|
||||||
|
documenso/documenso:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab value=".env">
|
||||||
|
|
||||||
|
If you're running Documenso directly (not in a container), add the variable to your `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Step 2: Restart the instance
|
||||||
|
|
||||||
|
The license key is only read once, at process startup. Setting the variable in a running container or shell has no effect until the process restarts.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker Compose
|
||||||
|
docker compose restart documenso
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker restart documenso
|
||||||
|
```
|
||||||
|
|
||||||
|
On startup, Documenso validates the key against the Documenso license server and caches the result locally for future startups, so a brief license-server outage won't lock you out.
|
||||||
|
|
||||||
|
## What the license enables
|
||||||
|
|
||||||
|
A valid license doesn't turn every enterprise feature on everywhere — activation depends on the feature:
|
||||||
|
|
||||||
|
- **CSC signing** activates instance-wide automatically once the license is active and CSC transport is configured. See [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup.
|
||||||
|
- **SSO, embed white-labelling, 21 CFR Part 11, and similar** are provisioned per organisation. Follow each feature's own guide to configure it once the license is active.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
<Accordions type="multiple">
|
||||||
|
<Accordion title="Enterprise features are still unavailable after applying the key">
|
||||||
|
- Confirm the key is present in the environment the running process actually reads — `docker
|
||||||
|
exec` into the container and check `env | grep LICENSE` if unsure.
|
||||||
|
- Confirm the instance was fully restarted after the variable was set, not just reloaded.
|
||||||
|
- Re-copy the key to rule out truncation or accidental whitespace.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="A specific feature still isn't working">
|
||||||
|
Instance-wide features (like CSC signing) also need their own configuration — an active license
|
||||||
|
alone isn't enough. Check that feature's guide to confirm the required settings are in place.
|
||||||
|
Per-organisation features additionally need to be provisioned for the organisation that's using
|
||||||
|
them.
|
||||||
|
</Accordion>
|
||||||
|
</Accordions>
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference
|
||||||
|
- [Enterprise Edition](/docs/policies/enterprise-edition) - What's included and how to purchase a license
|
||||||
|
- [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) - Enable CSC-based signing
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
"title": "Configuration",
|
"title": "Configuration",
|
||||||
"pages": [
|
"pages": [
|
||||||
"environment",
|
"environment",
|
||||||
|
"license",
|
||||||
"database",
|
"database",
|
||||||
"email",
|
"email",
|
||||||
"storage",
|
"storage",
|
||||||
"background-jobs",
|
"background-jobs",
|
||||||
"signing-certificate",
|
"signing-certificate",
|
||||||
"telemetry",
|
"telemetry",
|
||||||
|
"organisation-limits",
|
||||||
"advanced"
|
"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 (1000/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 **1000 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
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
---
|
||||||
|
title: CSC (AES / QES)
|
||||||
|
description: Configure Cloud Signature Consortium signing for Advanced and Qualified Electronic Signatures via a third-party Trust Service Provider.
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
|
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||||
|
|
||||||
|
The `csc` signing transport routes signatures through a third-party Trust Service Provider (TSP) using the [Cloud Signature Consortium API v1.0.4.0](https://cloudsignatureconsortium.org/). Each recipient authenticates directly with the TSP (Strong Customer Authentication) and the TSP returns a per-recipient signature bound to the document hash. Documenso assembles the resulting PAdES signature inside the PDF.
|
||||||
|
|
||||||
|
This transport enables **Advanced Electronic Signatures (AES)** and **Qualified Electronic Signatures (QES)** under eIDAS. See [Signature Levels](/docs/compliance/signature-levels) for the legal framework.
|
||||||
|
|
||||||
|
<Callout type="warn">
|
||||||
|
CSC mode is **instance-wide**: one CSC provider per Documenso install. All envelopes created
|
||||||
|
while the instance runs in `csc` mode use AES or QES. Switching `NEXT_PRIVATE_SIGNING_TRANSPORT`
|
||||||
|
is a one-way operational migration — see [Switching Transports](#switching-transports).
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<Callout type="warn">
|
||||||
|
CSC mode requires an active [Enterprise Edition](/docs/policies/enterprise-edition) license. The
|
||||||
|
instance refuses to start in `csc` mode without it.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
{/* prettier-ignore */}
|
||||||
|
<Steps>
|
||||||
|
<Step>
|
||||||
|
|
||||||
|
### A TSP account
|
||||||
|
|
||||||
|
Establish a relationship with a CSC-compatible Trust Service Provider. The TSP issues qualified or advanced certificates to your signers, holds the private keys in its HSM, and exposes a CSC v1.0.4.0-compliant API.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
|
||||||
|
### OAuth client credentials
|
||||||
|
|
||||||
|
Register Documenso as an OAuth client with the TSP. You will receive a client ID and client secret, and must supply Documenso's callback URL when registering:
|
||||||
|
|
||||||
|
```
|
||||||
|
${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
The callback URL is fixed — Documenso derives it from `NEXT_PUBLIC_WEBAPP_URL` and the route mount path. There is no env var to override it; ensuring the registered URL matches your instance's webapp URL exactly is the operator's responsibility.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
|
||||||
|
### Enterprise Edition license
|
||||||
|
|
||||||
|
CSC mode is gated by the `instanceCscSigning` license flag. Without a valid Enterprise license, the transport refuses to start (`CSC_UNLICENSED`). See [Apply Your License Key](/docs/self-hosting/configuration/license) to activate one.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
|
||||||
|
### S3 storage (strongly recommended)
|
||||||
|
|
||||||
|
CSC produces multiple `DocumentData` rows per envelope item (one per recipient signature, plus the materialised and source rows). Database-backed storage base64-inflates each row by ~33% and is impractical at meaningful PDF sizes. Configure [S3 storage](/docs/self-hosting/configuration/storage) before enabling CSC.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||||
|
| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Set to `csc` | |
|
||||||
|
| `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | Base URL of the CSC provider's API | |
|
||||||
|
| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID` | OAuth client ID registered with the CSC provider | |
|
||||||
|
| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET` | OAuth client secret registered with the CSC provider | |
|
||||||
|
| `NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` | Default legal tier for new envelopes when the caller does not specify one. `AES` or `QES`. Explicit requests always pass through. | `AES` |
|
||||||
|
| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | **Required.** Comma-separated RFC 3161 TSA URLs. Always used for B-LTA archival timestamps at seal time, and also serves as the B-T sign-time fallback when the TSP does not expose `signatures/timestamp`. The instance refuses to start in CSC mode without it. See [Timestamp Authority Resolution](#timestamp-authority-resolution). | |
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
`NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC` is set automatically from
|
||||||
|
`NEXT_PRIVATE_SIGNING_TRANSPORT` at server startup. Do not set it manually — see
|
||||||
|
[Environment Variables](/docs/self-hosting/configuration/environment#derived-public-variables).
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Configuration Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXT_PRIVATE_SIGNING_TRANSPORT=csc
|
||||||
|
NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL=https://api.example-tsp.com/csc/v1
|
||||||
|
NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID=documenso-prod
|
||||||
|
NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET=...
|
||||||
|
NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL=QES
|
||||||
|
NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=http://timestamp.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Register `${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback` (e.g. `https://sign.example.com/api/csc/oauth/callback`) as the OAuth callback URL with the TSP.
|
||||||
|
|
||||||
|
## Default Signature Level
|
||||||
|
|
||||||
|
`NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` selects the legal tier applied to envelopes that do not specify one explicitly. It is a default, not a capability gate: callers may still create AES or QES envelopes explicitly regardless of this setting.
|
||||||
|
|
||||||
|
| Configured value | Caller passes nothing | Caller passes `AES` | Caller passes `QES` |
|
||||||
|
| ---------------- | --------------------- | ------------------- | ------------------- |
|
||||||
|
| `AES` (default) | Envelope is `AES` | Envelope is `AES` | Envelope is `QES` |
|
||||||
|
| `QES` | Envelope is `QES` | Envelope is `AES` | Envelope is `QES` |
|
||||||
|
|
||||||
|
Any value other than `AES` or `QES` causes the instance to refuse to start. This prevents silent qualified-to-advanced downgrades from a typo.
|
||||||
|
|
||||||
|
## Timestamp Authority Resolution
|
||||||
|
|
||||||
|
AES/QES envelopes use TSA-attested timestamps in two distinct phases. Resolution differs per phase.
|
||||||
|
|
||||||
|
### Sign time — PAdES B-T per recipient
|
||||||
|
|
||||||
|
Each recipient's CMS embeds a signature timestamp (CMS unsigned attribute) so proven time is bound to the recipient's signature itself. Resolution order:
|
||||||
|
|
||||||
|
1. If the TSP advertises `signatures/timestamp` in its `info` response (CSC §11.10), the TSP endpoint is used. The call is authorised with **this recipient's** service-scope bearer token — the same one authorising the `signatures/signHash` call alongside it.
|
||||||
|
2. Otherwise, the first URL from `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is used (RFC 3161 over HTTP).
|
||||||
|
|
||||||
|
Selection is made at boot from the discovered transport, not at runtime; there is no try-then-fall-through. If the chosen source fails, the recipient's sign attempt fails.
|
||||||
|
|
||||||
|
### Seal time — PAdES B-LTA archival
|
||||||
|
|
||||||
|
The seal-document job emits a single archival `/DocTimeStamp` over the fully-signed envelope (plus DSS for the existing signatures and the timestamp's own chain). This phase is **env-only**: the first URL from `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is always used.
|
||||||
|
|
||||||
|
The archival anchor is the operator's long-term trust anchor and SHOULD point at a dedicated qualified archival TSA (e.g. DigiCert) independent of the per-recipient TSP. We deliberately do not fall back to the TSP at seal time: archive longevity should not be coupled to a TSP that may rotate or revoke, and the seal-document job has no recipient context to carry a service-scope bearer.
|
||||||
|
|
||||||
|
### Boot-time guard
|
||||||
|
|
||||||
|
The instance refuses to start in CSC mode unless `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is set (`CSC_PROVIDER_NO_TSA` at transport construction). The env var is required unconditionally — even when the TSP advertises its own `signatures/timestamp`, seal-time B-LTA archival uses the env TSA. Catching this at boot prevents the failure mode where an envelope signs successfully at B-T and then hangs in `WAITING_FOR_SIGNATURE_COMPLETION` when the seal job throws.
|
||||||
|
|
||||||
|
## Switching Transports
|
||||||
|
|
||||||
|
`NEXT_PRIVATE_SIGNING_TRANSPORT` is a one-way operational migration. Existing envelopes route per the `signatureLevel` column they were created with — the runtime branching looks at the envelope, not the env var. After a switch:
|
||||||
|
|
||||||
|
- Envelopes already at `SES` continue to use the new transport for sealing, but the new transport's signer must produce SES-compatible signatures (only `local` and `gcloud-hsm` qualify).
|
||||||
|
- Envelopes already at `AES` / `QES` will fail at sign or seal time if the new transport is not `csc`.
|
||||||
|
|
||||||
|
Plan migrations during a quiet window with no in-flight envelopes.
|
||||||
|
|
||||||
|
## Behavioural Notes
|
||||||
|
|
||||||
|
CSC mode changes a number of envelope-authoring behaviours that operators should communicate to users.
|
||||||
|
|
||||||
|
### Mutation lock at distribution
|
||||||
|
|
||||||
|
For AES/QES envelopes, all authoring routes refuse mutations once the envelope leaves DRAFT. This locks the PDF before any recipient begins Strong Customer Authentication, closing the PDF-swap window that would otherwise allow an owner to replace the PDF between view and sign and break the legal "what you see is what you sign" guarantee.
|
||||||
|
|
||||||
|
In practice: edit envelope, recipients, fields, and items freely while DRAFT; once sent, no changes are accepted (including from the API).
|
||||||
|
|
||||||
|
### Sequential signing only
|
||||||
|
|
||||||
|
Parallel signing produces conflicting incremental updates over the same base PDF, breaking the per-recipient `/ByteRange` invariant. The signing order is forced to `SEQUENTIAL` on AES/QES envelopes — at the schema layer, at send time, and in the UI (the parallel-signing toggle is hidden).
|
||||||
|
|
||||||
|
### Assistant role and Dictate Next Signer disabled
|
||||||
|
|
||||||
|
Both features modify the recipient set after the envelope is sent, which is incompatible with the AES/QES mutation lock. They are hidden in the UI and rejected at the server schema layer.
|
||||||
|
|
||||||
|
### Sidecar PDFs at download
|
||||||
|
|
||||||
|
The signed PDF must remain byte-identical to what each recipient's TSP signature authorised — Documenso cannot decorate it after signing. Audit logs and the Certificate of Completion are generated on demand and delivered as separate PDFs:
|
||||||
|
|
||||||
|
- `GET /sign/{token}/download` returns the signed PDF only (or a ZIP for multi-item envelopes).
|
||||||
|
- `GET /sign/{token}/download?version=bundle` returns a ZIP containing the signed PDFs, audit log PDF, and Certificate of Completion.
|
||||||
|
- The completion email attaches all three.
|
||||||
|
|
||||||
|
## Recipient Flow
|
||||||
|
|
||||||
|
For context when supporting end users, here is what a recipient experiences on an AES/QES envelope:
|
||||||
|
|
||||||
|
1. Opens the email link, lands on the signing page.
|
||||||
|
2. Documenso redirects to the TSP for Strong Customer Authentication (first visit only; cached for the session lifetime).
|
||||||
|
3. Fills fields as normal.
|
||||||
|
4. Clicks Sign → redirected to the TSP for a second authentication round (issues a per-document Signature Activation Data token).
|
||||||
|
5. Returns to Documenso; the signing call completes within ~15 seconds.
|
||||||
|
6. Sees the standard completion screen.
|
||||||
|
|
||||||
|
If the TSP returns no eligible credentials for the recipient (e.g. they have not enrolled), they see a blocking page directing them to enrol with the TSP and retry.
|
||||||
|
|
||||||
|
## Error Codes
|
||||||
|
|
||||||
|
CSC-specific error codes surfaced through the standard error channels:
|
||||||
|
|
||||||
|
| Code | Meaning | Recovery |
|
||||||
|
| -------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------- |
|
||||||
|
| `CSC_UNLICENSED` | License flag absent at transport-create | Operator: enable Enterprise Edition, restart |
|
||||||
|
| `CSC_PROVIDER_INFO_FAILED` | `info` discovery failed at startup | Operator: check TSP availability and `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` |
|
||||||
|
| `CSC_PROVIDER_NO_TSA` | `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is unset | Operator: configure `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` |
|
||||||
|
| `CSC_CREDENTIAL_LIST_EMPTY`| TSP returned no credentials for the user | Recipient: enrol with the TSP |
|
||||||
|
| `CSC_CERT_INVALID` | Certificate refused at credential validation | Recipient: contact the TSP |
|
||||||
|
| `CSC_ALGORITHM_REFUSED` | Signature algorithm fails policy | Operator/recipient: TSP does not meet policy (see below) |
|
||||||
|
| `CSC_SAD_EXPIRED_PRE_SIGN` | Signature Activation Data expired before signing | Recipient: retry from Sign |
|
||||||
|
| `CSC_TSP_TIMEOUT` | 15-second synchronous timeout reached | Recipient: retry (idempotent — the TSP enforces single-use SAD binding) |
|
||||||
|
| `CSC_EMBED_FAILED` | Sign-time digest diverged from prep capture | Recipient: retry from Sign |
|
||||||
|
| `CSC_BASE_DOCUMENT_MUTATED`| Document data changed between prep and sign | Operator: investigate (structural guard violation) |
|
||||||
|
| `CSC_INSTANCE_MODE_MISMATCH`| Envelope created with wrong level for transport | Caller: use a level matching the instance transport |
|
||||||
|
| `CSC_REQUEST_FAILED` | TSP HTTP transport failure — network error, non-2xx, or malformed response | Operator: check TSP availability; carries the TSP HTTP status and error in the message |
|
||||||
|
|
||||||
|
## Algorithm Policy
|
||||||
|
|
||||||
|
Documenso refuses TSP credentials that do not meet the following minimums, at the OAuth callback boundary and again at sign time:
|
||||||
|
|
||||||
|
| Class | Allowed | Refused |
|
||||||
|
| ----- | ---------------------------------- | ------------------------------------------------------ |
|
||||||
|
| RSA | `key.len >= 2048` | Missing `key.len`, `key.len < 2048` |
|
||||||
|
| ECDSA | P-256, P-384, P-521 | Missing `key.curve`, P-192, P-224, other curves |
|
||||||
|
| Hash | SHA-256, SHA-384, SHA-512 | SHA-1, MD5 |
|
||||||
|
| Other | — | DSA |
|
||||||
|
|
||||||
|
This is the union of CSC v1.0.4.0 §11.5 requirements and current cryptographic guidance.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Signature Levels](/docs/compliance/signature-levels) — AES / QES legal framework
|
||||||
|
- [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) — overview of all signing transports
|
||||||
|
- [Environment Variables](/docs/self-hosting/configuration/environment) — full env reference
|
||||||
|
- [Enterprise Edition](/docs/policies/enterprise-edition) — license requirements
|
||||||
@@ -24,6 +24,11 @@ Self-hosted Documenso instances require a signing certificate. You can generate
|
|||||||
description="Hardware-based key protection with Google Cloud KMS."
|
description="Hardware-based key protection with Google Cloud KMS."
|
||||||
href="/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm"
|
href="/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm"
|
||||||
/>
|
/>
|
||||||
|
<Card
|
||||||
|
title="CSC (AES / QES)"
|
||||||
|
description="Route signing through a third-party Trust Service Provider for Advanced and Qualified Electronic Signatures."
|
||||||
|
href="/docs/self-hosting/configuration/signing-certificate/csc-qes"
|
||||||
|
/>
|
||||||
<Card
|
<Card
|
||||||
title="Timestamp Server"
|
title="Timestamp Server"
|
||||||
description="Add trusted timestamps and customise signature appearance."
|
description="Add trusted timestamps and customise signature appearance."
|
||||||
@@ -38,7 +43,7 @@ Self-hosted Documenso instances require a signing certificate. You can generate
|
|||||||
|
|
||||||
## Certificate Options
|
## Certificate Options
|
||||||
|
|
||||||
<Tabs items={['Self-Signed', 'CA-Issued', 'Google Cloud HSM']}>
|
<Tabs items={['Self-Signed', 'CA-Issued', 'Google Cloud HSM', 'CSC (AES / QES)']}>
|
||||||
<Tab value="Self-Signed">
|
<Tab value="Self-Signed">
|
||||||
|
|
||||||
A self-signed certificate is sufficient for most use cases where your industry has no special signing regulations.
|
A self-signed certificate is sufficient for most use cases where your industry has no special signing regulations.
|
||||||
@@ -79,6 +84,18 @@ For organisations requiring hardware-based key protection, Documenso supports Go
|
|||||||
|
|
||||||
See [Google Cloud HSM](/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm) for setup instructions.
|
See [Google Cloud HSM](/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm) for setup instructions.
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab value="CSC (AES / QES)">
|
||||||
|
|
||||||
|
For Advanced and Qualified Electronic Signatures under eIDAS, Documenso integrates with third-party Trust Service Providers via the Cloud Signature Consortium API. Each recipient authenticates directly with the TSP, which holds the private key and issues the signature.
|
||||||
|
|
||||||
|
- Per-recipient identity verification by an accredited TSP
|
||||||
|
- Legally equivalent to a handwritten signature within the EU (QES)
|
||||||
|
- Requires an [Enterprise Edition](/docs/policies/enterprise-edition) license
|
||||||
|
- Instance-wide setting; one CSC provider per Documenso install
|
||||||
|
|
||||||
|
See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes) for setup instructions.
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"title": "Signing Certificate",
|
"title": "Signing Certificate",
|
||||||
"pages": ["...index", "local", "google-cloud-hsm", "timestamp-server", "troubleshooting"]
|
"pages": ["...index", "local", "google-cloud-hsm", "csc-qes", "timestamp-server", "troubleshooting"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Storage Configuration
|
title: Storage Configuration
|
||||||
description: Configure file storage for uploaded documents and signed PDFs using database storage (default) or S3-compatible object storage.
|
description: Configure file storage for uploaded documents and signed PDFs using database storage (default), S3-compatible object storage, or Azure Blob Storage.
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||||
@@ -10,10 +10,11 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
|||||||
|
|
||||||
## Storage Options
|
## Storage Options
|
||||||
|
|
||||||
| Backend | Best For | Scalability | Configuration |
|
| Backend | Best For | Scalability | Configuration |
|
||||||
| ---------- | -------------------------------- | ----------- | ------------- |
|
| ------------ | --------------------------------------- | ----------- | ------------- |
|
||||||
| `database` | Small deployments, simplicity | Limited | None required |
|
| `database` | Small deployments, simplicity | Limited | None required |
|
||||||
| `s3` | Production, large files, backups | High | Required |
|
| `s3` | Production, large files, backups | High | Required |
|
||||||
|
| `azure-blob` | Production on Azure, native Blob access | High | Required |
|
||||||
|
|
||||||
Select the storage backend with the `NEXT_PUBLIC_UPLOAD_TRANSPORT` environment variable:
|
Select the storage backend with the `NEXT_PUBLIC_UPLOAD_TRANSPORT` environment variable:
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ NEXT_PUBLIC_UPLOAD_TRANSPORT=database
|
|||||||
|
|
||||||
# S3-compatible storage
|
# S3-compatible storage
|
||||||
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
|
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
|
||||||
|
|
||||||
|
# Azure Blob Storage (native)
|
||||||
|
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -283,6 +287,111 @@ NEXT_PRIVATE_UPLOAD_REGION=us-east-1
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Azure Blob Storage
|
||||||
|
|
||||||
|
Azure Blob Storage is supported as a native transport (not S3-compatible). Documenso uses the official `@azure/storage-blob` SDK and signs SAS URLs with the Storage Account key for browser uploads and downloads.
|
||||||
|
|
||||||
|
### Required Variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| --------------------------------------- | ------------------------------------------------- |
|
||||||
|
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Set to `azure-blob` |
|
||||||
|
| `NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME` | Azure Storage Account name |
|
||||||
|
| `NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY` | Azure Storage Account access key |
|
||||||
|
| `NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER` | Container name where uploads are stored |
|
||||||
|
|
||||||
|
### Optional Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||||
|
| `NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT` | Custom Blob endpoint URL. Useful for local development against Azurite (for example `http://127.0.0.1:10000`). | `https://<account>.blob.core.windows.net` |
|
||||||
|
|
||||||
|
### Azure Setup
|
||||||
|
|
||||||
|
{/* prettier-ignore */}
|
||||||
|
<Steps>
|
||||||
|
<Step>
|
||||||
|
### Create a Storage Account and Container
|
||||||
|
|
||||||
|
Create a Storage Account in the Azure Portal or via the Azure CLI, then create a container inside it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
az storage account create \
|
||||||
|
--name yourstorageaccount \
|
||||||
|
--resource-group your-rg \
|
||||||
|
--location eastus \
|
||||||
|
--sku Standard_LRS
|
||||||
|
|
||||||
|
az storage container create \
|
||||||
|
--name documenso-documents \
|
||||||
|
--account-name yourstorageaccount
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
### Configure CORS on the container
|
||||||
|
|
||||||
|
The browser uploads documents directly to Azure Blob using a SAS URL, and downloads them the same way, so the Storage Account needs CORS rules that allow your application origin:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
az storage cors add \
|
||||||
|
--services b \
|
||||||
|
--methods GET PUT \
|
||||||
|
--origins https://your-documenso-domain.com \
|
||||||
|
--allowed-headers "Content-Type" "x-ms-blob-type" "Authorization" \
|
||||||
|
--exposed-headers "*" \
|
||||||
|
--max-age 3600 \
|
||||||
|
--account-name yourstorageaccount
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step>
|
||||||
|
### Configure Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
|
||||||
|
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME=yourstorageaccount
|
||||||
|
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY=your-account-key
|
||||||
|
NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER=documenso-documents
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
### Local Development with Azurite
|
||||||
|
|
||||||
|
Azurite is the official Azure Storage emulator. It supports the Blob REST API with account-key authentication.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name azurite \
|
||||||
|
-p 10000:10000 -p 10001:10001 -p 10002:10002 \
|
||||||
|
mcr.microsoft.com/azure-storage/azurite
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the container against the well-known development account:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
az storage container create \
|
||||||
|
--name documenso-documents \
|
||||||
|
--connection-string "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure environment variables to point at the emulator:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
|
||||||
|
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME=devstoreaccount1
|
||||||
|
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||||
|
NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER=documenso-documents
|
||||||
|
NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT=http://127.0.0.1:10000
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
The Azurite key shown above is the public well-known development key, published by Microsoft for emulator use. Never reuse it in production.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## CloudFront CDN (Optional)
|
## CloudFront CDN (Optional)
|
||||||
|
|
||||||
Use Amazon CloudFront to serve documents with lower latency and reduced S3 costs. CloudFront integration uses signed URLs for secure access.
|
Use Amazon CloudFront to serve documents with lower latency and reduced S3 costs. CloudFront integration uses signed URLs for secure access.
|
||||||
|
|||||||
@@ -163,6 +163,19 @@ NEXT_PUBLIC_DISABLE_SIGNUP=false
|
|||||||
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=true
|
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=true
|
||||||
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=true
|
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=true
|
||||||
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org
|
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org
|
||||||
|
|
||||||
|
# Signin restrictions (optional)
|
||||||
|
# Master switch — disables every signin method
|
||||||
|
# NEXT_PUBLIC_DISABLE_SIGNIN=true
|
||||||
|
# Per-method switches (optional). Each disables that signin path.
|
||||||
|
# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN=true
|
||||||
|
# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN=true
|
||||||
|
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN=true
|
||||||
|
# NEXT_PUBLIC_DISABLE_OIDC_SIGNIN=true
|
||||||
|
|
||||||
|
# When OIDC is the only enabled transport, /signin auto-redirects to the provider.
|
||||||
|
# Set this to opt out and keep showing the signin page (optional).
|
||||||
|
# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT=true
|
||||||
```
|
```
|
||||||
|
|
||||||
<Callout type="info">Generate secure secrets using: `openssl rand -base64 32`</Callout>
|
<Callout type="info">Generate secure secrets using: `openssl rand -base64 32`</Callout>
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ See [Email Configuration](/docs/self-hosting/configuration/email) for other tran
|
|||||||
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft OAuth | `false` |
|
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft OAuth | `false` |
|
||||||
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal) | `false` |
|
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal) | `false` |
|
||||||
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
|
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch — disable all signin methods | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin only | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN` | Hide the Microsoft signin button | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable auto-redirect to OIDC when it is the only transport | `false` |
|
||||||
|
|
||||||
For the complete list, see [Environment Variables](/docs/self-hosting/configuration/environment).
|
For the complete list, see [Environment Variables](/docs/self-hosting/configuration/environment).
|
||||||
|
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ spec:
|
|||||||
```
|
```
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
Pin to a specific image tag (e.g., `documenso/documenso:1.5.0`) in production instead of `latest`
|
Pin to a specific image tag (e.g., `documenso/documenso:<version>`) in production instead of `latest`
|
||||||
to ensure predictable deployments.
|
to ensure predictable deployments.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Node.js 20 or later
|
- Node.js 22 or later
|
||||||
- npm
|
- npm 11 or later
|
||||||
- PostgreSQL 14 or later
|
- PostgreSQL 14 or later
|
||||||
- A Linux server (for systemd service setup)
|
- A Linux server (for systemd service setup)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Before deploying, you need:
|
|||||||
|
|
||||||
The fastest way to deploy Documenso on Railway is using the official template:
|
The fastest way to deploy Documenso on Railway is using the official template:
|
||||||
|
|
||||||
[](https://railway.app/template/bG6D4p)
|
[](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic)
|
||||||
|
|
||||||
This template automatically provisions:
|
This template automatically provisions:
|
||||||
|
|
||||||
@@ -159,6 +159,12 @@ NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@yourdomain.com
|
|||||||
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`| Block new accounts via Microsoft OAuth | `false` |
|
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`| Block new accounts via Microsoft OAuth | `false` |
|
||||||
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal)| `false` |
|
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal)| `false` |
|
||||||
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
|
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch — disable all signin methods | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin only | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN`| Hide the Microsoft signin button | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable auto-redirect to OIDC when it is the only transport | `false` |
|
||||||
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - |
|
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - |
|
||||||
| `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` |
|
| `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` |
|
||||||
|
|
||||||
|
|||||||
@@ -124,12 +124,16 @@ docker compose -f docker/development/compose.yml exec database \
|
|||||||
|
|
||||||
The quick start setup runs the following containers:
|
The quick start setup runs the following containers:
|
||||||
|
|
||||||
| Container | Purpose | Port |
|
| Container | Purpose | Port |
|
||||||
| ----------- | -------------------------------- | ----- |
|
| ----------- | ------------------------------------ | ----------------------------- |
|
||||||
| `documenso` | Main application | 3000 |
|
| `documenso` | Main application | 3000 |
|
||||||
| `database` | PostgreSQL database | 54320 |
|
| `database` | PostgreSQL database | 54320 |
|
||||||
| `maildev` | Local email testing server | 2500 |
|
| `inbucket` | Local email testing server | 9000 (web UI), 2500 (SMTP) |
|
||||||
| `minio` | S3-compatible storage (optional) | 9000 |
|
| `redis` | Cache and background job queue | 63790 |
|
||||||
|
| `minio` | S3-compatible storage | 9002 (API), 9001 (console) |
|
||||||
|
| `gotenberg` | Document conversion (optional) | 3005 |
|
||||||
|
|
||||||
|
The local email server is [Inbucket](https://www.inbucket.org/). Open its web UI at [http://localhost:9000](http://localhost:9000) to view emails Documenso sends during development. For your own deployment you can use any SMTP-compatible mailserver, such as Inbucket, [Mailpit](https://github.com/axllent/mailpit), or [Mailhog](https://github.com/mailhog/MailHog).
|
||||||
|
|
||||||
## Useful Commands
|
## Useful Commands
|
||||||
|
|
||||||
|
|||||||
@@ -141,8 +141,8 @@ If building from source (not using Docker images):
|
|||||||
|
|
||||||
| Requirement | Version |
|
| Requirement | Version |
|
||||||
| ----------- | ------- |
|
| ----------- | ------- |
|
||||||
| Node.js | 18+ |
|
| Node.js | 22+ |
|
||||||
| npm | 8+ |
|
| npm | 11+ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ Documenso runs on:
|
|||||||
| MySQL/MariaDB | PostgreSQL-specific features required |
|
| MySQL/MariaDB | PostgreSQL-specific features required |
|
||||||
| SQLite | Not suitable for production workloads |
|
| SQLite | Not suitable for production workloads |
|
||||||
| MongoDB | Relational database required |
|
| MongoDB | Relational database required |
|
||||||
| Node.js < 18 | Modern JavaScript features required |
|
| Node.js < 22 | Modern JavaScript features required |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ Use a specific version tag in production:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Good — predictable, reproducible
|
# Good — predictable, reproducible
|
||||||
docker pull documenso/documenso:1.8.0
|
docker pull documenso/documenso:<version>
|
||||||
|
|
||||||
# Risky — may pull breaking changes
|
# Risky — may pull breaking changes
|
||||||
docker pull documenso/documenso:latest
|
docker pull documenso/documenso:latest
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ import { Callout } from 'fumadocs-ui/components/callout';
|
|||||||
Please see all the [requirements](/docs/self-hosting/getting-started/requirements) before proceeding.
|
Please see all the [requirements](/docs/self-hosting/getting-started/requirements) before proceeding.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
<Callout type="warn">
|
||||||
|
**You are responsible for your own network security.** Documenso applies best-effort, non-exhaustive
|
||||||
|
checks to outbound requests such as webhooks, but these are not a complete SSRF mitigation and they
|
||||||
|
fail open. A self-hosted instance can reach internal addresses on your network. Restricting outbound
|
||||||
|
traffic, egress filtering, and blocking access to internal services and cloud metadata endpoints is
|
||||||
|
your responsibility through your firewall and network configuration.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deployment Options
|
## Deployment Options
|
||||||
@@ -133,7 +141,7 @@ See the [Quick Start guide](/docs/self-hosting/getting-started/quick-start) for
|
|||||||
|
|
||||||
Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed editor white label, or 21 CFR Part 11 compliance, you can activate them with a license key.
|
Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed editor white label, or 21 CFR Part 11 compliance, you can activate them with a license key.
|
||||||
|
|
||||||
See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison.
|
See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison. Already have a key? See [Apply Your License Key](/docs/self-hosting/configuration/license).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -165,10 +165,10 @@ See [Backups](/docs/self-hosting/maintenance/backups) for automated backup strat
|
|||||||
### Pull the new image
|
### Pull the new image
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull documenso/documenso:1.6.0
|
docker pull documenso/documenso:<version>
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace `1.6.0` with your target version.
|
Replace `<version>` with your target version.
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step>
|
<Step>
|
||||||
@@ -189,7 +189,7 @@ docker run -d \
|
|||||||
-p 3000:3000 \
|
-p 3000:3000 \
|
||||||
--env-file .env \
|
--env-file .env \
|
||||||
-v /path/to/cert.p12:/opt/documenso/cert.p12:ro \
|
-v /path/to/cert.p12:/opt/documenso/cert.p12:ro \
|
||||||
documenso/documenso:1.6.0
|
documenso/documenso:<version>
|
||||||
```
|
```
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
@@ -223,14 +223,14 @@ Edit `compose.yml` or your `.env` file to specify the new version:
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
documenso:
|
documenso:
|
||||||
image: documenso/documenso:1.6.0
|
image: documenso/documenso:<version>
|
||||||
```
|
```
|
||||||
|
|
||||||
Or if using environment variable substitution:
|
Or if using environment variable substitution:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# In .env
|
# In .env
|
||||||
DOCUMENSO_VERSION=1.6.0
|
DOCUMENSO_VERSION=<version>
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -283,7 +283,7 @@ Edit the deployment directly:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl set image deployment/documenso \
|
kubectl set image deployment/documenso \
|
||||||
documenso=documenso/documenso:1.6.0 \
|
documenso=documenso/documenso:<version> \
|
||||||
-n documenso
|
-n documenso
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: documenso
|
- name: documenso
|
||||||
image: documenso/documenso:1.6.0
|
image: documenso/documenso:<version>
|
||||||
```
|
```
|
||||||
|
|
||||||
Then apply:
|
Then apply:
|
||||||
@@ -421,12 +421,12 @@ To run migrations manually before upgrading:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Pull the new image
|
# Pull the new image
|
||||||
docker pull documenso/documenso:1.6.0
|
docker pull documenso/documenso:<version>
|
||||||
|
|
||||||
# Run migrations only
|
# Run migrations only
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-e NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@host:5432/documenso" \
|
-e NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@host:5432/documenso" \
|
||||||
documenso/documenso:1.6.0 \
|
documenso/documenso:<version> \
|
||||||
npx prisma migrate deploy
|
npx prisma migrate deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -516,7 +516,7 @@ docker run -d \
|
|||||||
-p 3000:3000 \
|
-p 3000:3000 \
|
||||||
--env-file .env \
|
--env-file .env \
|
||||||
-v /path/to/cert.p12:/opt/documenso/cert.p12:ro \
|
-v /path/to/cert.p12:/opt/documenso/cert.p12:ro \
|
||||||
documenso/documenso:1.5.0
|
documenso/documenso:<previous-version>
|
||||||
```
|
```
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ Navigate to [documen.so/free](https://documen.so/free) to create a free account.
|
|||||||
|
|
||||||
Provide your name, email address, and create a password. Alternatively, sign up with Google for faster access.
|
Provide your name, email address, and create a password. Alternatively, sign up with Google for faster access.
|
||||||
|
|
||||||
{/* TODO: Add screenshot of registration form */}
|
<img
|
||||||
|
src="/get-started-images/documenso-registration-form.webp"
|
||||||
|
alt="Documenso registration form with name, email, and password fields"
|
||||||
|
style={{width: '500px', height: '650px', objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import { Callout } from 'fumadocs-ui/components/callout';
|
|||||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||||
|
|
||||||
<Callout type="error">
|
<Callout type="error">
|
||||||
Account deletion is permanent and irreversible. All documents, signatures, templates, and account
|
Account deletion is permanent and irreversible. Your account, signatures, and personal data will be
|
||||||
data will be permanently removed. Any active subscription will be cancelled.
|
permanently removed, and any active subscription will be cancelled. How your organisations and
|
||||||
|
documents are handled is explained below.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
## Before Deleting
|
## Before Deleting
|
||||||
|
|
||||||
- Download any documents you need to keep
|
- Download any documents you need to keep
|
||||||
- Cancel any active subscriptions
|
|
||||||
- Disable two-factor authentication (required before deletion)
|
- Disable two-factor authentication (required before deletion)
|
||||||
|
|
||||||
## Delete Your Account
|
## Delete Your Account
|
||||||
@@ -36,6 +36,31 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
|
|||||||
If you have two-factor authentication enabled, you must disable it before deleting your account.
|
If you have two-factor authentication enabled, you must disable it before deleting your account.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
## What Happens to Your Organisations
|
||||||
|
|
||||||
|
When you delete your account, the organisations you **own** are permanently deleted along with all of
|
||||||
|
their teams. If an owned organisation has an active subscription, it is scheduled for cancellation at
|
||||||
|
the end of the current billing period.
|
||||||
|
|
||||||
|
Organisations that you are only a **member** of are not deleted. You are simply removed from them, and
|
||||||
|
the organisation continues to operate as normal.
|
||||||
|
|
||||||
|
## What Happens to Your Documents
|
||||||
|
|
||||||
|
The way your documents and templates are handled depends on whether you owned the organisation they
|
||||||
|
belong to:
|
||||||
|
|
||||||
|
- **Organisations you owned** — Completed and in-progress documents are retained in an anonymized form
|
||||||
|
(reassigned to an internal system account) so the other parties keep their records. Draft documents
|
||||||
|
and templates are permanently removed.
|
||||||
|
- **Organisations you were a member of** — Your documents and templates are transferred to the
|
||||||
|
organisation owner, so they remain accessible to the organisation after you leave.
|
||||||
|
|
||||||
|
<Callout type="warn">
|
||||||
|
Documents that are retained in anonymized form are no longer associated with your account and cannot
|
||||||
|
be recovered or accessed by you after deletion. Download anything you need to keep beforehand.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"@types/node": "^25.1.0",
|
"@types/node": "^25.1.0",
|
||||||
"@types/react": "^19.2.10",
|
"@types/react": "^19.2.10",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"postcss": "^8.5.14",
|
"postcss": "^8.5.19",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -83,7 +83,7 @@
|
|||||||
--accent: hsl(0 0% 27.8431%);
|
--accent: hsl(0 0% 27.8431%);
|
||||||
--accent-foreground: hsl(95.0847 71.0843% 67.451%);
|
--accent-foreground: hsl(95.0847 71.0843% 67.451%);
|
||||||
--destructive: hsl(0 86.5979% 61.9608%);
|
--destructive: hsl(0 86.5979% 61.9608%);
|
||||||
--destructive-foreground: hsl(0 87.6289% 19.0196%);
|
--destructive-foreground: hsl(0 0% 98.0392%);
|
||||||
--border: hsl(0 0% 27.8431%);
|
--border: hsl(0 0% 27.8431%);
|
||||||
--input: hsl(0 0% 27.8431%);
|
--input: hsl(0 0% 27.8431%);
|
||||||
--ring: hsl(95.0847 71.0843% 67.451%);
|
--ring: hsl(95.0847 71.0843% 67.451%);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Callout } from 'fumadocs-ui/components/callout';
|
||||||
|
|
||||||
|
const MIGRATION_GUIDE_HREF = '/docs/developers/api/migrate-to-envelopes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecation banner steering API consumers away from the legacy document and
|
||||||
|
* template create endpoints and towards the unified Envelope API.
|
||||||
|
*
|
||||||
|
* Registered globally in `mdx-components.tsx`, so it can be used in any MDX page
|
||||||
|
* as `<EnvelopeWarning />` without an explicit import.
|
||||||
|
*/
|
||||||
|
export function EnvelopeWarning() {
|
||||||
|
return (
|
||||||
|
<Callout type="error">
|
||||||
|
<strong>Documents and templates are being deprecated and replaced by envelopes.</strong>{' '}
|
||||||
|
<a href={MIGRATION_GUIDE_HREF}>Read the migration guide here.</a>
|
||||||
|
</Callout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as TabsComponents from 'fumadocs-ui/components/tabs';
|
import * as TabsComponents from 'fumadocs-ui/components/tabs';
|
||||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||||
import type { MDXComponents } from 'mdx/types';
|
import type { MDXComponents } from 'mdx/types';
|
||||||
|
import { EnvelopeWarning } from '@/components/mdx/envelope-warning';
|
||||||
import { Mermaid } from '@/components/mdx/mermaid';
|
import { Mermaid } from '@/components/mdx/mermaid';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -9,6 +10,7 @@ export function getMDXComponents(components?: MDXComponents): any {
|
|||||||
...defaultMdxComponents,
|
...defaultMdxComponents,
|
||||||
...TabsComponents,
|
...TabsComponents,
|
||||||
Mermaid,
|
Mermaid,
|
||||||
|
EnvelopeWarning,
|
||||||
...components,
|
...components,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,5 +73,12 @@ if [ -z "$NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET" ]; then
|
|||||||
echo "╚═════════════════════════════════════════════════════════════════════╝"
|
echo "╚═════════════════════════════════════════════════════════════════════╝"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL=$(load_env_var "NEXT_PUBLIC_WEBAPP_URL")
|
||||||
|
|
||||||
|
if [ -z "$NEXT_PUBLIC_WEBAPP_URL" ]; then
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
||||||
|
echo "[INFO]: NEXT_PUBLIC_WEBAPP_URL not set, defaulting to $NEXT_PUBLIC_WEBAPP_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[INFO]: Starting Stripe webhook listener..."
|
echo "[INFO]: Starting Stripe webhook listener..."
|
||||||
stripe listen --api-key "$NEXT_PRIVATE_STRIPE_API_KEY" --forward-to http://localhost:3000/api/stripe/webhook
|
stripe listen --api-key "$NEXT_PRIVATE_STRIPE_API_KEY" --forward-to "$NEXT_PUBLIC_WEBAPP_URL/api/stripe/webhook"
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
FROM node:20-alpine AS development-dependencies-env
|
|
||||||
COPY . /app
|
|
||||||
WORKDIR /app
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
FROM node:20-alpine AS production-dependencies-env
|
|
||||||
COPY ./package.json package-lock.json /app/
|
|
||||||
WORKDIR /app
|
|
||||||
RUN npm ci --omit=dev
|
|
||||||
|
|
||||||
FROM node:20-alpine AS build-env
|
|
||||||
COPY . /app/
|
|
||||||
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
|
||||||
WORKDIR /app
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
FROM node:20-alpine
|
|
||||||
COPY ./package.json package-lock.json /app/
|
|
||||||
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
|
||||||
COPY --from=build-env /app/build /app/build
|
|
||||||
WORKDIR /app
|
|
||||||
CMD ["npm", "run", "start"]
|
|
||||||
+7
-93
@@ -1,100 +1,14 @@
|
|||||||
# Welcome to React Router!
|
# @documenso/remix
|
||||||
|
|
||||||
A modern, production-ready template for building full-stack React applications using React Router.
|
The main Documenso web application. Built with [React Router v7](https://reactrouter.com/) and served by a [Hono](https://hono.dev/) server.
|
||||||
|
|
||||||
[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
|
This package is part of the Documenso monorepo and is not meant to be run standalone. Use the root scripts instead.
|
||||||
|
|
||||||
## Features
|
- Local development: see the [root README](../../README.md) and the [Local Development docs](https://docs.documenso.com/docs/developers/local-development).
|
||||||
|
- Self-hosting and deployment: see the [Self-Hosting docs](https://docs.documenso.com/docs/self-hosting).
|
||||||
- 🚀 Server-side rendering
|
- Architecture overview: see [ARCHITECTURE.md](../../ARCHITECTURE.md).
|
||||||
- ⚡️ Hot Module Replacement (HMR)
|
|
||||||
- 📦 Asset bundling and optimization
|
|
||||||
- 🔄 Data loading and mutations
|
|
||||||
- 🔒 TypeScript by default
|
|
||||||
- 🎉 TailwindCSS for styling
|
|
||||||
- 📖 [React Router docs](https://reactrouter.com/)
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
Install the dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
Start the development server with HMR:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# From the monorepo root
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Your application will be available at `http://localhost:5173`.
|
|
||||||
|
|
||||||
## Building for Production
|
|
||||||
|
|
||||||
Create a production build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### Docker Deployment
|
|
||||||
|
|
||||||
This template includes three Dockerfiles optimized for different package managers:
|
|
||||||
|
|
||||||
- `Dockerfile` - for npm
|
|
||||||
- `Dockerfile.pnpm` - for pnpm
|
|
||||||
- `Dockerfile.bun` - for bun
|
|
||||||
|
|
||||||
To build and run using Docker:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For npm
|
|
||||||
docker build -t my-app .
|
|
||||||
|
|
||||||
# For pnpm
|
|
||||||
docker build -f Dockerfile.pnpm -t my-app .
|
|
||||||
|
|
||||||
# For bun
|
|
||||||
docker build -f Dockerfile.bun -t my-app .
|
|
||||||
|
|
||||||
# Run the container
|
|
||||||
docker run -p 3000:3000 my-app
|
|
||||||
```
|
|
||||||
|
|
||||||
The containerized application can be deployed to any platform that supports Docker, including:
|
|
||||||
|
|
||||||
- AWS ECS
|
|
||||||
- Google Cloud Run
|
|
||||||
- Azure Container Apps
|
|
||||||
- Digital Ocean App Platform
|
|
||||||
- Fly.io
|
|
||||||
- Railway
|
|
||||||
|
|
||||||
### DIY Deployment
|
|
||||||
|
|
||||||
If you're familiar with deploying Node applications, the built-in app server is production-ready.
|
|
||||||
|
|
||||||
Make sure to deploy the output of `npm run build`
|
|
||||||
|
|
||||||
```
|
|
||||||
├── package.json
|
|
||||||
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
|
|
||||||
├── build/
|
|
||||||
│ ├── client/ # Static assets
|
|
||||||
│ └── server/ # Server-side code
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling
|
|
||||||
|
|
||||||
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Built with ❤️ using React Router.
|
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { Form, FormControl, FormField, FormItem } from '@documenso/ui/primitives/form/form';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export type AdminOrganisationSyncSubscriptionDialogProps = {
|
||||||
|
organisationId: string;
|
||||||
|
trigger?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZAdminOrganisationSyncSubscriptionFormSchema = z.object({
|
||||||
|
syncClaims: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAdminOrganisationSyncSubscriptionFormSchema = z.infer<typeof ZAdminOrganisationSyncSubscriptionFormSchema>;
|
||||||
|
|
||||||
|
export const AdminOrganisationSyncSubscriptionDialog = ({
|
||||||
|
organisationId,
|
||||||
|
trigger,
|
||||||
|
}: AdminOrganisationSyncSubscriptionDialogProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { t } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const form = useForm<TAdminOrganisationSyncSubscriptionFormSchema>({
|
||||||
|
resolver: zodResolver(ZAdminOrganisationSyncSubscriptionFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
syncClaims: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: syncSubscription } = trpc.admin.organisation.subscription.sync.useMutation();
|
||||||
|
|
||||||
|
const onFormSubmit = async (values: TAdminOrganisationSyncSubscriptionFormSchema) => {
|
||||||
|
try {
|
||||||
|
await syncSubscription({
|
||||||
|
organisationId,
|
||||||
|
syncClaims: values.syncClaims,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t`Subscription synced`,
|
||||||
|
description: t`The organisation subscription has been synced with Stripe.`,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await navigate(0);
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
} catch (err) {
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t`Failed to sync subscription`,
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: 10000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
}, [open, form]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{trigger ?? (
|
||||||
|
<Button variant="outline">
|
||||||
|
<Trans>Sync Stripe subscription</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent position="center">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Sync Stripe subscription</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans>Fetch the latest subscription data from Stripe and apply it to this organisation.</Trans>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||||
|
<fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="syncClaims"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
id="admin-sync-subscription-sync-claims"
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<label
|
||||||
|
htmlFor="admin-sync-subscription-sync-claims"
|
||||||
|
className="font-normal text-muted-foreground text-sm leading-snug"
|
||||||
|
>
|
||||||
|
<Trans>
|
||||||
|
Sync claims. This will overwrite the current claim with the one resolved from the Stripe
|
||||||
|
subscription.
|
||||||
|
</Trans>
|
||||||
|
</label>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||||
|
<Trans>Sync</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -67,7 +67,7 @@ export const AdminSwapSubscriptionDialog = ({
|
|||||||
|
|
||||||
const selectedOrg = eligibleOrgs.find((org) => org.id === selectedOrgId);
|
const selectedOrg = eligibleOrgs.find((org) => org.id === selectedOrgId);
|
||||||
|
|
||||||
const { mutateAsync: swapSubscription } = trpc.admin.organisation.swapSubscription.useMutation();
|
const { mutateAsync: swapSubscription } = trpc.admin.organisation.subscription.swap.useMutation();
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if (!selectedOrgId) {
|
if (!selectedOrgId) {
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export type BrandingPreferencesResetDialogProps = {
|
||||||
|
hasAdvancedBranding: boolean;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
onReset: () => Promise<void>;
|
||||||
|
trigger?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BrandingPreferencesResetDialog = ({
|
||||||
|
hasAdvancedBranding,
|
||||||
|
isSubmitting,
|
||||||
|
onReset,
|
||||||
|
trigger,
|
||||||
|
}: BrandingPreferencesResetDialogProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [isResetting, setIsResetting] = useState(false);
|
||||||
|
|
||||||
|
const isLoading = isSubmitting || isResetting;
|
||||||
|
|
||||||
|
const handleResetToDefaults = async () => {
|
||||||
|
setIsResetting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onReset();
|
||||||
|
setOpen(false);
|
||||||
|
} catch {
|
||||||
|
// The submit handler surfaces its own error toast. Keep the dialog open
|
||||||
|
// so the user can retry.
|
||||||
|
} finally {
|
||||||
|
setIsResetting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(value) => !isLoading && setOpen(value)}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{trigger ?? (
|
||||||
|
<Button variant="destructive" type="button" size="sm" disabled={isLoading}>
|
||||||
|
<Trans>Reset to defaults</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Reset branding preferences</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans>
|
||||||
|
This will reset all branding preferences to their default values and save the changes immediately.
|
||||||
|
</Trans>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertDescription>
|
||||||
|
<p>
|
||||||
|
<Trans>Once confirmed, the following will be reset:</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="mt-0.5 list-inside list-disc">
|
||||||
|
<li>
|
||||||
|
<Trans>Custom branding enabled setting</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Branding logo</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Brand website and brand details</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Brand colours, including background, foreground, primary, and border colours</Trans>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{hasAdvancedBranding && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<Trans>Border radius</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Custom CSS</Trans>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="secondary" disabled={isLoading}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
|
||||||
|
<Button type="button" variant="destructive" loading={isLoading} onClick={() => void handleResetToDefaults()}>
|
||||||
|
<Trans>Reset to defaults</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import type { TLicenseClaim } from '@documenso/lib/types/license';
|
|||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type { TFindSubscriptionClaimsResponse } from '@documenso/trpc/server/admin-router/find-subscription-claims.types';
|
import type { TFindSubscriptionClaimsResponse } from '@documenso/trpc/server/admin-router/find-subscription-claims.types';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -28,6 +29,7 @@ export const ClaimUpdateDialog = ({ claim, trigger, licenseFlags }: ClaimUpdateD
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [backportEmailTransport, setBackportEmailTransport] = useState(false);
|
||||||
|
|
||||||
const { mutateAsync: updateClaim, isPending } = trpc.admin.claims.update.useMutation({
|
const { mutateAsync: updateClaim, isPending } = trpc.admin.claims.update.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -67,19 +69,33 @@ export const ClaimUpdateDialog = ({ claim, trigger, licenseFlags }: ClaimUpdateD
|
|||||||
await updateClaim({
|
await updateClaim({
|
||||||
id: claim.id,
|
id: claim.id,
|
||||||
data,
|
data,
|
||||||
|
backportEmailTransport,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
licenseFlags={licenseFlags}
|
licenseFlags={licenseFlags}
|
||||||
formSubmitTrigger={
|
formSubmitTrigger={
|
||||||
<DialogFooter>
|
<>
|
||||||
<Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
|
<div className="flex items-center space-x-2">
|
||||||
<Trans>Cancel</Trans>
|
<Checkbox
|
||||||
</Button>
|
id="backport-email-transport"
|
||||||
|
checked={backportEmailTransport}
|
||||||
|
onCheckedChange={(checked) => setBackportEmailTransport(checked === true)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="backport-email-transport" className="text-muted-foreground text-sm">
|
||||||
|
<Trans>Backport email transport</Trans>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button type="submit" loading={isPending}>
|
<DialogFooter>
|
||||||
<Trans>Update Claim</Trans>
|
<Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
|
||||||
</Button>
|
<Trans>Cancel</Trans>
|
||||||
</DialogFooter>
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit" loading={isPending}>
|
||||||
|
<Trans>Update Claim</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -1,243 +0,0 @@
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
|
||||||
import { FolderType } from '@documenso/lib/types/folder-type';
|
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@documenso/ui/primitives/dialog';
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
|
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
|
||||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
||||||
import { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
|
||||||
|
|
||||||
export type DocumentMoveToFolderDialogProps = {
|
|
||||||
documentId: number;
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
currentFolderId?: string;
|
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
|
||||||
|
|
||||||
const ZMoveDocumentFormSchema = z.object({
|
|
||||||
folderId: z.string().nullable().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type TMoveDocumentFormSchema = z.infer<typeof ZMoveDocumentFormSchema>;
|
|
||||||
|
|
||||||
export const DocumentMoveToFolderDialog = ({
|
|
||||||
documentId,
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
currentFolderId,
|
|
||||||
...props
|
|
||||||
}: DocumentMoveToFolderDialogProps) => {
|
|
||||||
const { _ } = useLingui();
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const team = useCurrentTeam();
|
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
|
|
||||||
const form = useForm<TMoveDocumentFormSchema>({
|
|
||||||
resolver: zodResolver(ZMoveDocumentFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
folderId: currentFolderId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFoldersInternal.useQuery(
|
|
||||||
{
|
|
||||||
parentId: currentFolderId,
|
|
||||||
type: FolderType.DOCUMENT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: open,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync: updateDocument } = trpc.document.update.useMutation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
form.reset();
|
|
||||||
setSearchTerm('');
|
|
||||||
} else {
|
|
||||||
form.reset({ folderId: currentFolderId });
|
|
||||||
}
|
|
||||||
}, [open, currentFolderId, form]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: TMoveDocumentFormSchema) => {
|
|
||||||
try {
|
|
||||||
await updateDocument({
|
|
||||||
documentId,
|
|
||||||
data: {
|
|
||||||
folderId: data.folderId ?? null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const documentsPath = formatDocumentsPath(team.url);
|
|
||||||
|
|
||||||
if (data.folderId) {
|
|
||||||
await navigate(`${documentsPath}/f/${data.folderId}`);
|
|
||||||
} else {
|
|
||||||
await navigate(documentsPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: _(msg`Document moved`),
|
|
||||||
description: _(msg`The document has been moved successfully.`),
|
|
||||||
variant: 'default',
|
|
||||||
});
|
|
||||||
|
|
||||||
onOpenChange(false);
|
|
||||||
} catch (err) {
|
|
||||||
const error = AppError.parseError(err);
|
|
||||||
|
|
||||||
if (error.code === AppErrorCode.NOT_FOUND) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Error`),
|
|
||||||
description: _(msg`The folder you are trying to move the document to does not exist.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Error`),
|
|
||||||
description: _(msg`You are not allowed to move this document.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: _(msg`Error`),
|
|
||||||
description: _(msg`An error occurred while moving the document.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredFolders = folders?.data.filter((folder) =>
|
|
||||||
folder.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog {...props} open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
<Trans>Move Document to Folder</Trans>
|
|
||||||
</DialogTitle>
|
|
||||||
|
|
||||||
<DialogDescription>
|
|
||||||
<Trans>Select a folder to move this document to.</Trans>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute top-3 left-2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder={_(msg`Search folders...`)}
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-8"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="folderId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
<Trans>Folder</Trans>
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<div className="max-h-96 space-y-2 overflow-y-auto">
|
|
||||||
{isFoldersLoading ? (
|
|
||||||
<div className="flex h-10 items-center justify-center">
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={field.value === null ? 'default' : 'outline'}
|
|
||||||
className="w-full justify-start"
|
|
||||||
onClick={() => field.onChange(null)}
|
|
||||||
disabled={currentFolderId === null}
|
|
||||||
>
|
|
||||||
<HomeIcon className="mr-2 h-4 w-4" />
|
|
||||||
<Trans>Home (No Folder)</Trans>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{filteredFolders?.map((folder) => (
|
|
||||||
<Button
|
|
||||||
key={folder.id}
|
|
||||||
type="button"
|
|
||||||
variant={field.value === folder.id ? 'default' : 'outline'}
|
|
||||||
className="w-full justify-start"
|
|
||||||
onClick={() => field.onChange(folder.id)}
|
|
||||||
disabled={currentFolderId === folder.id}
|
|
||||||
>
|
|
||||||
<FolderIcon className="mr-2 h-4 w-4" />
|
|
||||||
{folder.name}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{searchTerm && filteredFolders?.length === 0 && (
|
|
||||||
<div className="px-2 py-2 text-center text-muted-foreground text-sm">
|
|
||||||
<Trans>No folders found</Trans>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
|
||||||
<Trans>Cancel</Trans>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={isFoldersLoading || form.formState.isSubmitting || currentFolderId === null}
|
|
||||||
>
|
|
||||||
<Trans>Move</Trans>
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export type DocumentPreferencesResetDialogProps = {
|
||||||
|
isSubmitting: boolean;
|
||||||
|
onReset: () => Promise<void>;
|
||||||
|
showAiFeatures?: boolean;
|
||||||
|
showDocumentVisibility?: boolean;
|
||||||
|
showIncludeSenderDetails?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DocumentPreferencesResetDialog = ({
|
||||||
|
isSubmitting,
|
||||||
|
onReset,
|
||||||
|
showAiFeatures = false,
|
||||||
|
showDocumentVisibility = false,
|
||||||
|
showIncludeSenderDetails = false,
|
||||||
|
}: DocumentPreferencesResetDialogProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [isResetting, setIsResetting] = useState(false);
|
||||||
|
|
||||||
|
const isLoading = isSubmitting || isResetting;
|
||||||
|
|
||||||
|
const handleResetToDefaults = async () => {
|
||||||
|
setIsResetting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onReset();
|
||||||
|
setOpen(false);
|
||||||
|
} catch {
|
||||||
|
// The submit handler surfaces its own error toast. Keep the dialog open
|
||||||
|
// so the user can retry.
|
||||||
|
} finally {
|
||||||
|
setIsResetting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(value) => !isLoading && setOpen(value)}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="destructive" type="button" size="sm" disabled={isLoading}>
|
||||||
|
<Trans>Reset to defaults</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Reset document preferences</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans>
|
||||||
|
This will reset all document preferences to their default values and save the changes immediately.
|
||||||
|
</Trans>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertDescription>
|
||||||
|
<p>
|
||||||
|
<Trans>Once confirmed, the following will be reset:</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="mt-0.5 list-inside list-disc">
|
||||||
|
{showDocumentVisibility && (
|
||||||
|
<li>
|
||||||
|
<Trans>Default document visibility</Trans>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li>
|
||||||
|
<Trans>Default document language</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Default date format</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Default time zone</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Default signature settings</Trans>
|
||||||
|
</li>
|
||||||
|
{showIncludeSenderDetails && (
|
||||||
|
<li>
|
||||||
|
<Trans>Send on behalf of team</Trans>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li>
|
||||||
|
<Trans>Include the signing certificate in the document</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Include the audit logs in the document</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Default recipients</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Delegate document ownership</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Default envelope expiration</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Default signing reminders</Trans>
|
||||||
|
</li>
|
||||||
|
{showAiFeatures && (
|
||||||
|
<li>
|
||||||
|
<Trans>AI features</Trans>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="secondary" disabled={isLoading}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
|
||||||
|
<Button type="button" variant="destructive" loading={isLoading} onClick={() => void handleResetToDefaults()}>
|
||||||
|
<Trans>Reset to defaults</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
|
||||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
|
||||||
import type { TRecipientLite } from '@documenso/lib/types/recipient';
|
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
|
||||||
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@documenso/ui/primitives/dialog';
|
|
||||||
import { DropdownMenuItem } from '@documenso/ui/primitives/dropdown-menu';
|
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel } from '@documenso/ui/primitives/form/form';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
|
||||||
import { SigningStatus, type Team, type User } from '@prisma/client';
|
|
||||||
import { History } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
|
||||||
import * as z from 'zod';
|
|
||||||
|
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
|
||||||
|
|
||||||
import { StackAvatar } from '../general/stack-avatar';
|
|
||||||
|
|
||||||
const FORM_ID = 'resend-email';
|
|
||||||
|
|
||||||
export type DocumentResendDialogProps = {
|
|
||||||
document: Pick<Document, 'id' | 'userId' | 'teamId' | 'status'> & {
|
|
||||||
user: Pick<User, 'id' | 'name' | 'email'>;
|
|
||||||
recipients: TRecipientLite[];
|
|
||||||
team: Pick<Team, 'id' | 'url'> | null;
|
|
||||||
};
|
|
||||||
recipients: TRecipientLite[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ZResendDocumentFormSchema = z.object({
|
|
||||||
recipients: z.array(z.number()).min(1, {
|
|
||||||
message: 'You must select at least one item.',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TResendDocumentFormSchema = z.infer<typeof ZResendDocumentFormSchema>;
|
|
||||||
|
|
||||||
export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => {
|
|
||||||
const { user } = useSession();
|
|
||||||
const team = useCurrentTeam();
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const isOwner = document.userId === user.id;
|
|
||||||
const isCurrentTeamDocument = team && document.team?.url === team.url;
|
|
||||||
|
|
||||||
const isDisabled =
|
|
||||||
(!isOwner && !isCurrentTeamDocument) ||
|
|
||||||
document.status !== 'PENDING' ||
|
|
||||||
!recipients.some((r) => r.signingStatus === SigningStatus.NOT_SIGNED);
|
|
||||||
|
|
||||||
const { mutateAsync: resendDocument } = trpcReact.document.redistribute.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<TResendDocumentFormSchema>({
|
|
||||||
resolver: zodResolver(ZResendDocumentFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
recipients: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
handleSubmit,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
} = form;
|
|
||||||
|
|
||||||
const selectedRecipients = useWatch({
|
|
||||||
control: form.control,
|
|
||||||
name: 'recipients',
|
|
||||||
});
|
|
||||||
|
|
||||||
const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
|
|
||||||
try {
|
|
||||||
await resendDocument({ documentId: document.id, recipients });
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: _(msg`Document re-sent`),
|
|
||||||
description: _(msg`Your document has been re-sent successfully.`),
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
setIsOpen(false);
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`This document could not be re-sent at this time. Please try again.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
duration: 7500,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem disabled={isDisabled} onSelect={(e) => e.preventDefault()}>
|
|
||||||
<History className="mr-2 h-4 w-4" />
|
|
||||||
<Trans>Resend</Trans>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent className="sm:max-w-sm" hideClose>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle asChild>
|
|
||||||
<h1 className="text-center text-xl">
|
|
||||||
<Trans>Who do you want to remind?</Trans>
|
|
||||||
</h1>
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form id={FORM_ID} onSubmit={handleSubmit(onFormSubmit)} className="px-3">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="recipients"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<>
|
|
||||||
{recipients.map((recipient) => (
|
|
||||||
<FormItem key={recipient.id} className="flex flex-row items-center justify-between gap-x-3">
|
|
||||||
<FormLabel
|
|
||||||
className={cn('my-2 flex items-center gap-2 font-normal', {
|
|
||||||
'opacity-50': !value.includes(recipient.id),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<StackAvatar
|
|
||||||
key={recipient.id}
|
|
||||||
type={getRecipientType(recipient)}
|
|
||||||
fallbackText={recipientAbbreviation(recipient)}
|
|
||||||
/>
|
|
||||||
{recipient.email}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
className="h-5 w-5 rounded-full border border-neutral-400"
|
|
||||||
value={recipient.id}
|
|
||||||
checked={value.includes(recipient.id)}
|
|
||||||
onCheckedChange={(checked: boolean) =>
|
|
||||||
checked
|
|
||||||
? onChange([...value, recipient.id])
|
|
||||||
: onChange(value.filter((v) => v !== recipient.id))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="flex-1 bg-black/5 hover:bg-black/10 dark:bg-muted dark:hover:bg-muted/80"
|
|
||||||
variant="secondary"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
<Trans>Cancel</Trans>
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="flex-1"
|
|
||||||
loading={isSubmitting}
|
|
||||||
type="submit"
|
|
||||||
form={FORM_ID}
|
|
||||||
disabled={isSubmitting || selectedRecipients.length === 0}
|
|
||||||
>
|
|
||||||
<Trans>Send reminder</Trans>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EmailTransportForm,
|
||||||
|
type EmailTransportFormValues,
|
||||||
|
emailTransportFormToConfig,
|
||||||
|
} from '../forms/email-transport-form';
|
||||||
|
|
||||||
|
export type EmailTransportCreateDialogProps = {
|
||||||
|
trigger?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmailTransportCreateDialog = ({ trigger }: EmailTransportCreateDialogProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { mutateAsync: createTransport, isPending } = trpc.admin.emailTransport.create.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: t`Transport created.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: t`Failed to create transport.`,
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async (values: EmailTransportFormValues) => {
|
||||||
|
await createTransport({
|
||||||
|
name: values.name,
|
||||||
|
fromName: values.fromName,
|
||||||
|
fromAddress: values.fromAddress,
|
||||||
|
config: emailTransportFormToConfig(values),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(value) => !isPending && setOpen(value)}>
|
||||||
|
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild>
|
||||||
|
{trigger ?? (
|
||||||
|
<Button className="flex-shrink-0">
|
||||||
|
<Trans>Add transport</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent className="scrollbar-hidden max-h-[90vh] overflow-y-auto sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Add Email Transport</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans>Fill in the details to create a new email transport.</Trans>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<EmailTransportForm
|
||||||
|
onFormSubmit={onFormSubmit}
|
||||||
|
formSubmitTrigger={
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit" loading={isPending}>
|
||||||
|
<Trans>Create</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Alert, AlertDescription } 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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
import { Plural, Trans, useLingui } from '@lingui/react/macro';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export type EmailTransportDeleteDialogProps = {
|
||||||
|
transportId: string;
|
||||||
|
transportName: string;
|
||||||
|
subscriptionClaimCount: number;
|
||||||
|
organisationClaimCount: number;
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmailTransportDeleteDialog = ({
|
||||||
|
transportId,
|
||||||
|
transportName,
|
||||||
|
subscriptionClaimCount,
|
||||||
|
organisationClaimCount,
|
||||||
|
trigger,
|
||||||
|
}: EmailTransportDeleteDialogProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const isInUse = subscriptionClaimCount + organisationClaimCount > 0;
|
||||||
|
|
||||||
|
const { mutateAsync: deleteTransport, isPending } = trpc.admin.emailTransport.delete.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: t`Transport deleted.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: t`Failed to delete transport.`,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(value) => !isPending && setOpen(value)}>
|
||||||
|
<DialogTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||||
|
{trigger}
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Delete Email Transport</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans>Are you sure you want to delete the following transport?</Trans>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Alert variant="neutral">
|
||||||
|
<AlertDescription className="text-center font-semibold">{transportName}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{isInUse && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans>Warning, this email transport is currently being used by:</Trans>
|
||||||
|
|
||||||
|
<ul className="mt-2 list-disc pl-5">
|
||||||
|
{subscriptionClaimCount > 0 && (
|
||||||
|
<li>
|
||||||
|
<Plural value={subscriptionClaimCount} one="# Subscription claim" other="# Subscription claims" />
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{organisationClaimCount > 0 && (
|
||||||
|
<li>
|
||||||
|
<Plural value={organisationClaimCount} one="# Organisation claim" other="# Organisation claims" />
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="secondary" onClick={() => setOpen(false)} disabled={isPending}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="destructive"
|
||||||
|
loading={isPending}
|
||||||
|
onClick={async () => deleteTransport({ id: transportId })}
|
||||||
|
>
|
||||||
|
<Trans>Delete</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const ZSendTestEmailFormSchema = z.object({
|
||||||
|
to: z.string().email(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type TSendTestEmailFormSchema = z.infer<typeof ZSendTestEmailFormSchema>;
|
||||||
|
|
||||||
|
export type EmailTransportSendTestDialogProps = {
|
||||||
|
transportId: string;
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmailTransportSendTestDialog = ({ transportId, trigger }: EmailTransportSendTestDialogProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { mutateAsync: sendTest } = trpc.admin.emailTransport.sendTest.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: t`Test email sent.`,
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast({
|
||||||
|
title: t`Test failed.`,
|
||||||
|
description: error.message,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<TSendTestEmailFormSchema>({
|
||||||
|
resolver: zodResolver(ZSendTestEmailFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
to: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ to }: TSendTestEmailFormSchema) => {
|
||||||
|
await sendTest({ id: transportId, to });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
}, [open, form]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
|
||||||
|
<DialogTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||||
|
{trigger}
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Send Test Email</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans>Send a test email using this transport to verify the configuration.</Trans>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||||
|
<fieldset disabled={form.formState.isSubmitting} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="to"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel required>
|
||||||
|
<Trans>Email</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="email" placeholder={t`test@example.com`} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
disabled={form.formState.isSubmitting}
|
||||||
|
>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||||
|
<Trans>Send</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type { TFindEmailTransportsResponse } from '@documenso/trpc/server/admin-router/email-transport/find-email-transports.types';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EmailTransportForm,
|
||||||
|
type EmailTransportFormValues,
|
||||||
|
emailTransportFormToConfig,
|
||||||
|
} from '../forms/email-transport-form';
|
||||||
|
|
||||||
|
export type EmailTransportUpdateDialogProps = {
|
||||||
|
transport: TFindEmailTransportsResponse['data'][number];
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmailTransportUpdateDialog = ({ transport, trigger }: EmailTransportUpdateDialogProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const { mutateAsync: updateTransport, isPending } = trpc.admin.emailTransport.update.useMutation();
|
||||||
|
|
||||||
|
const onFormSubmit = async (values: EmailTransportFormValues) => {
|
||||||
|
try {
|
||||||
|
await updateTransport({
|
||||||
|
id: transport.id,
|
||||||
|
data: {
|
||||||
|
name: values.name,
|
||||||
|
fromName: values.fromName,
|
||||||
|
fromAddress: values.fromAddress,
|
||||||
|
config: emailTransportFormToConfig(values),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t`Transport updated.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
} catch {
|
||||||
|
toast({
|
||||||
|
title: t`Failed to save transport.`,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(value) => !isPending && setOpen(value)}>
|
||||||
|
<DialogTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||||
|
{trigger}
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent className="scrollbar-hidden max-h-[90vh] overflow-y-auto sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Edit Email Transport</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans>Modify the details of the email transport.</Trans>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<EmailTransportForm
|
||||||
|
isEdit
|
||||||
|
defaultValues={{
|
||||||
|
// Pre-fill the non-secret connection settings; secrets stay blank
|
||||||
|
// and are preserved on save unless re-entered.
|
||||||
|
...(transport.config ?? {}),
|
||||||
|
name: transport.name,
|
||||||
|
fromName: transport.fromName,
|
||||||
|
fromAddress: transport.fromAddress,
|
||||||
|
type: transport.type,
|
||||||
|
}}
|
||||||
|
onFormSubmit={onFormSubmit}
|
||||||
|
formSubmitTrigger={
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit" loading={isPending}>
|
||||||
|
<Trans>Save changes</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export type EnvelopeCancelDialogProps = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
trigger?: React.ReactNode;
|
||||||
|
onCancel?: () => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EnvelopeCancelDialog = ({ id, title, trigger, onCancel }: EnvelopeCancelDialogProps) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { t } = useLingui();
|
||||||
|
const trpcUtils = trpcReact.useUtils();
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [reason, setReason] = useState('');
|
||||||
|
|
||||||
|
const { mutateAsync: cancelEnvelope, isPending } = trpcReact.envelope.cancel.useMutation({
|
||||||
|
onSuccess: async () => {
|
||||||
|
toast({
|
||||||
|
title: t`Document cancelled`,
|
||||||
|
description: t`"${title}" has been successfully cancelled`,
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await trpcUtils.document.findDocumentsInternal.invalidate();
|
||||||
|
|
||||||
|
await onCancel?.();
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: t`Something went wrong`,
|
||||||
|
description: t`This document could not be cancelled at this time. Please try again.`,
|
||||||
|
variant: 'destructive',
|
||||||
|
duration: 7500,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setReason('');
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(value) => !isPending && setOpen(value)}>
|
||||||
|
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Are you sure?</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans>
|
||||||
|
You are about to cancel <strong>"{title}"</strong>
|
||||||
|
</Trans>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Alert variant="warning" className="-mt-1">
|
||||||
|
<AlertDescription>
|
||||||
|
<p>
|
||||||
|
<Trans>Once confirmed, the following will occur:</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="mt-0.5 list-inside list-disc">
|
||||||
|
<li>
|
||||||
|
<Trans>The document signing process will be stopped</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Recipients will be notified that the document was cancelled</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>The document will remain in your dashboard marked as Cancelled</Trans>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="cancel-reason">
|
||||||
|
<Trans>Reason (optional)</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
id="cancel-reason"
|
||||||
|
value={reason}
|
||||||
|
onChange={(event) => setReason(event.target.value)}
|
||||||
|
placeholder={t`Add an optional reason for cancelling this document`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="secondary" disabled={isPending}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
loading={isPending}
|
||||||
|
onClick={() => void cancelEnvelope({ envelopeId: id, reason: reason || undefined })}
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
<Trans>Cancel document</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -166,7 +166,7 @@ export const EnvelopeDeleteDialog = ({
|
|||||||
</ul>
|
</ul>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
))
|
))
|
||||||
.with(P.union(DocumentStatus.COMPLETED, DocumentStatus.REJECTED), () => (
|
.with(P.union(DocumentStatus.COMPLETED, DocumentStatus.REJECTED, DocumentStatus.CANCELLED), () => (
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<p>
|
<p>
|
||||||
<Trans>By deleting this document, the following will occur:</Trans>
|
<Trans>By deleting this document, the following will occur:</Trans>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
|
||||||
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
|
import { hasOverlappingFields } from '@documenso/lib/utils/fields-overlap';
|
||||||
import { getRecipientsWithMissingFields } from '@documenso/lib/utils/recipients';
|
import { getRecipientsWithMissingFields } from '@documenso/lib/utils/recipients';
|
||||||
import { zEmail } from '@documenso/lib/utils/zod';
|
import { zEmail } from '@documenso/lib/utils/zod';
|
||||||
import { trpc, trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc, trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -31,12 +33,13 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { DocumentDistributionMethod, DocumentStatus, EnvelopeType } from '@prisma/client';
|
import { DocumentDistributionMethod, DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { InfoIcon } from 'lucide-react';
|
import { AlertTriangleIcon, InfoIcon } from 'lucide-react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
import { getDistributeErrorMessage } from '~/utils/toast-error-messages';
|
||||||
|
|
||||||
export type EnvelopeDistributeDialogProps = {
|
export type EnvelopeDistributeDialogProps = {
|
||||||
onDistribute?: () => Promise<void>;
|
onDistribute?: () => Promise<void>;
|
||||||
@@ -66,7 +69,7 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
const { envelope, syncEnvelope, isAutosaving, autosaveError } = useCurrentEnvelopeEditor();
|
const { envelope, syncEnvelope, isAutosaving, autosaveError } = useCurrentEnvelopeEditor();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { t } = useLingui();
|
const { t, i18n } = useLingui();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -136,6 +139,27 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
});
|
});
|
||||||
}, [recipientsWithIndex, envelope.authOptions]);
|
}, [recipientsWithIndex, envelope.authOptions]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether any fields significantly overlap each other. This is surfaced as a
|
||||||
|
* non-blocking warning since overlapping fields still allow sending, but can
|
||||||
|
* complicate the signing process or cause fields to behave unexpectedly.
|
||||||
|
*/
|
||||||
|
const hasOverlappingEnvelopeFields = useMemo(
|
||||||
|
() =>
|
||||||
|
hasOverlappingFields(
|
||||||
|
envelope.fields.map((field) => ({
|
||||||
|
id: field.id,
|
||||||
|
envelopeItemId: field.envelopeItemId,
|
||||||
|
page: field.page,
|
||||||
|
positionX: Number(field.positionX),
|
||||||
|
positionY: Number(field.positionY),
|
||||||
|
width: Number(field.width),
|
||||||
|
height: Number(field.height),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
[envelope.fields],
|
||||||
|
);
|
||||||
|
|
||||||
const invalidEnvelopeCode = useMemo(() => {
|
const invalidEnvelopeCode = useMemo(() => {
|
||||||
if (recipientsMissingSignatureFields.length > 0) {
|
if (recipientsMissingSignatureFields.length > 0) {
|
||||||
return 'MISSING_SIGNATURES';
|
return 'MISSING_SIGNATURES';
|
||||||
@@ -174,9 +198,13 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
|
const errorMessage = getDistributeErrorMessage(error.code);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t`Something went wrong`,
|
title: i18n._(errorMessage.title),
|
||||||
description: t`This envelope could not be distributed at this time. Please try again.`,
|
description: i18n._(errorMessage.description),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
duration: 7500,
|
duration: 7500,
|
||||||
});
|
});
|
||||||
@@ -200,6 +228,11 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Default the distribution method tab to the envelope's configured setting.
|
||||||
|
if (isOpen && envelope.documentMeta) {
|
||||||
|
setValue('meta.distributionMethod', envelope.documentMeta.distributionMethod);
|
||||||
|
}
|
||||||
|
|
||||||
// Resync the whole envelope if the envelope is mid saving.
|
// Resync the whole envelope if the envelope is mid saving.
|
||||||
if (isOpen && (isAutosaving || autosaveError)) {
|
if (isOpen && (isAutosaving || autosaveError)) {
|
||||||
void handleSync();
|
void handleSync();
|
||||||
@@ -229,6 +262,24 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<fieldset disabled={isSubmitting}>
|
<fieldset disabled={isSubmitting}>
|
||||||
|
{hasOverlappingEnvelopeFields && (
|
||||||
|
<Alert variant="warning" className="mb-4 flex flex-row items-start gap-3">
|
||||||
|
<AlertTriangleIcon className="mt-0.5 h-5 w-5 flex-shrink-0" />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans>Overlapping fields detected</Trans>
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans>
|
||||||
|
Some fields are placed on top of each other. This may complicate the signing process or cause
|
||||||
|
fields to not work as expected.
|
||||||
|
</Trans>
|
||||||
|
</AlertDescription>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
@@ -11,10 +12,12 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { EnvelopeType } from '@prisma/client';
|
import { EnvelopeType } from '@prisma/client';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
@@ -37,6 +40,15 @@ export const EnvelopeDuplicateDialog = ({ envelopeId, envelopeType, trigger }: E
|
|||||||
|
|
||||||
const isDocument = envelopeType === EnvelopeType.DOCUMENT;
|
const isDocument = envelopeType === EnvelopeType.DOCUMENT;
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
includeRecipients: true,
|
||||||
|
includeFields: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const includeRecipients = form.watch('includeRecipients');
|
||||||
|
|
||||||
const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } = trpc.envelope.duplicate.useMutation({
|
const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } = trpc.envelope.duplicate.useMutation({
|
||||||
onSuccess: async ({ id }) => {
|
onSuccess: async ({ id }) => {
|
||||||
toast({
|
toast({
|
||||||
@@ -55,8 +67,14 @@ export const EnvelopeDuplicateDialog = ({ envelopeId, envelopeType, trigger }: E
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onDuplicate = async () => {
|
const onDuplicate = async () => {
|
||||||
|
const { includeRecipients, includeFields } = form.getValues();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await duplicateEnvelope({ envelopeId });
|
await duplicateEnvelope({
|
||||||
|
envelopeId,
|
||||||
|
includeRecipients,
|
||||||
|
includeFields: includeRecipients && includeFields,
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: t`Something went wrong`,
|
title: t`Something went wrong`,
|
||||||
@@ -70,7 +88,20 @@ export const EnvelopeDuplicateDialog = ({ envelopeId, envelopeType, trigger }: E
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(value) => !isDuplicating && setOpen(value)}>
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(value) => {
|
||||||
|
if (isDuplicating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(value);
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -87,6 +118,49 @@ export const EnvelopeDuplicateDialog = ({ envelopeId, envelopeType, trigger }: E
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="includeRecipients"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="envelopeDuplicateIncludeRecipients"
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
field.onChange(checked === true);
|
||||||
|
|
||||||
|
if (!checked) {
|
||||||
|
form.setValue('includeFields', false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="envelopeDuplicateIncludeRecipients">
|
||||||
|
<Trans>Include Recipients</Trans>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="includeFields"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="envelopeDuplicateIncludeFields"
|
||||||
|
checked={field.value}
|
||||||
|
disabled={!includeRecipients}
|
||||||
|
onCheckedChange={(checked) => field.onChange(checked === true)}
|
||||||
|
/>
|
||||||
|
<Label htmlFor="envelopeDuplicateIncludeFields" className={!includeRecipients ? 'opacity-50' : ''}>
|
||||||
|
<Trans>Include Fields</Trans>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button type="button" variant="secondary" disabled={isDuplicating}>
|
<Button type="button" variant="secondary" disabled={isDuplicating}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||||
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||||
import type { TEnvelopeRecipientLite } from '@documenso/lib/types/recipient';
|
import type { TEnvelopeRecipientLite } from '@documenso/lib/types/recipient';
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
@@ -24,14 +25,16 @@ import { Trans, useLingui } from '@lingui/react/macro';
|
|||||||
import { DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
|
import { DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
import { getDistributeErrorMessage } from '~/utils/toast-error-messages';
|
||||||
import { StackAvatar } from '../general/stack-avatar';
|
import { StackAvatar } from '../general/stack-avatar';
|
||||||
|
|
||||||
export type EnvelopeRedistributeDialogProps = {
|
export type EnvelopeRedistributeDialogProps = {
|
||||||
envelope: Pick<TEnvelope, 'id' | 'userId' | 'teamId' | 'status' | 'type' | 'documentMeta'> & {
|
envelope: Pick<TEnvelope, 'id' | 'status' | 'type'> & {
|
||||||
recipients: TEnvelopeRecipientLite[];
|
recipients: TEnvelopeRecipientLite[];
|
||||||
};
|
};
|
||||||
|
envelopeType?: EnvelopeType;
|
||||||
trigger?: React.ReactNode;
|
trigger?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,11 +46,11 @@ export const ZEnvelopeRedistributeFormSchema = z.object({
|
|||||||
|
|
||||||
export type TEnvelopeRedistributeFormSchema = z.infer<typeof ZEnvelopeRedistributeFormSchema>;
|
export type TEnvelopeRedistributeFormSchema = z.infer<typeof ZEnvelopeRedistributeFormSchema>;
|
||||||
|
|
||||||
export const EnvelopeRedistributeDialog = ({ envelope, trigger }: EnvelopeRedistributeDialogProps) => {
|
export const EnvelopeRedistributeDialog = ({ envelope, envelopeType, trigger }: EnvelopeRedistributeDialogProps) => {
|
||||||
const recipients = envelope.recipients;
|
const recipients = envelope.recipients;
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { t } = useLingui();
|
const { t, i18n } = useLingui();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
@@ -69,17 +72,34 @@ export const EnvelopeRedistributeDialog = ({ envelope, trigger }: EnvelopeRedist
|
|||||||
try {
|
try {
|
||||||
await redistributeEnvelope({ envelopeId: envelope.id, recipients });
|
await redistributeEnvelope({ envelopeId: envelope.id, recipients });
|
||||||
|
|
||||||
|
const successMessage = match(envelopeType)
|
||||||
|
.with(EnvelopeType.DOCUMENT, () => ({
|
||||||
|
title: t`Document resent`,
|
||||||
|
description: t`Your document has been resent successfully.`,
|
||||||
|
}))
|
||||||
|
.with(EnvelopeType.TEMPLATE, () => ({
|
||||||
|
title: t`Template resent`,
|
||||||
|
description: t`Your template has been resent successfully.`,
|
||||||
|
}))
|
||||||
|
.otherwise(() => ({
|
||||||
|
title: t`Envelope resent`,
|
||||||
|
description: t`Your envelope has been resent successfully.`,
|
||||||
|
}));
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t`Envelope resent`,
|
title: successMessage.title,
|
||||||
description: t`Your envelope has been resent successfully.`,
|
description: successMessage.description,
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
const errorMessage = getDistributeErrorMessage(error.code);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t`Something went wrong`,
|
title: i18n._(errorMessage.title),
|
||||||
description: t`This envelope could not be resent at this time. Please try again.`,
|
description: i18n._(errorMessage.description),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
duration: 7500,
|
duration: 7500,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@documenso/ui/primitives/dialog';
|
||||||
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
import { plural } from '@lingui/core/macro';
|
||||||
|
import { Plural, Trans, useLingui } from '@lingui/react/macro';
|
||||||
|
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export type EnvelopesBulkCancelDialogProps = {
|
||||||
|
envelopeIds: string[];
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
|
export const EnvelopesBulkCancelDialog = ({
|
||||||
|
envelopeIds,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSuccess,
|
||||||
|
...props
|
||||||
|
}: EnvelopesBulkCancelDialogProps) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const trpcUtils = trpc.useUtils();
|
||||||
|
|
||||||
|
const [reason, setReason] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setReason('');
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const { mutateAsync: bulkCancelEnvelopes, isPending } = trpc.envelope.bulk.cancel.useMutation({
|
||||||
|
onSuccess: async (result) => {
|
||||||
|
await trpcUtils.document.findDocumentsInternal.invalidate();
|
||||||
|
|
||||||
|
if (result.failedIds.length > 0) {
|
||||||
|
toast({
|
||||||
|
title: t`Documents partially cancelled`,
|
||||||
|
description: t`${plural(result.cancelledCount, {
|
||||||
|
one: '# document cancelled.',
|
||||||
|
other: '# documents cancelled.',
|
||||||
|
})} ${plural(result.failedIds.length, {
|
||||||
|
one: '# document could not be cancelled.',
|
||||||
|
other: '# documents could not be cancelled.',
|
||||||
|
})}`,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
title: t`Documents cancelled`,
|
||||||
|
description: plural(result.cancelledCount, {
|
||||||
|
one: '# document has been cancelled.',
|
||||||
|
other: '# documents have been cancelled.',
|
||||||
|
}),
|
||||||
|
variant: 'default',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuccess?.();
|
||||||
|
onOpenChange(false);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: t`Error`,
|
||||||
|
description: t`An error occurred while cancelling the documents.`,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog {...props} open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans>Cancel Documents</Trans>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogDescription>
|
||||||
|
<Plural
|
||||||
|
value={envelopeIds.length}
|
||||||
|
one="You are about to cancel the selected document."
|
||||||
|
other="You are about to cancel # documents."
|
||||||
|
/>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Alert variant="warning">
|
||||||
|
<AlertDescription>
|
||||||
|
<p>
|
||||||
|
<Trans>Only pending documents you have permission to manage will be cancelled.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-1">
|
||||||
|
<Trans>Once confirmed, the following will occur:</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="mt-0.5 list-inside list-disc">
|
||||||
|
<li>
|
||||||
|
<Trans>The document signing process will be stopped</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>Recipients will be notified that the document was cancelled</Trans>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Trans>The documents will remain in your dashboard marked as Cancelled</Trans>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="bulk-cancel-reason">
|
||||||
|
<Trans>Reason (optional)</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
id="bulk-cancel-reason"
|
||||||
|
value={reason}
|
||||||
|
onChange={(event) => setReason(event.target.value)}
|
||||||
|
placeholder={t`Add an optional reason for cancelling these documents`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)} disabled={isPending}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
void bulkCancelEnvelopes({ envelopeIds, reason: reason || undefined });
|
||||||
|
}}
|
||||||
|
loading={isPending}
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
<Trans>Cancel documents</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -28,7 +28,7 @@ export type EnvelopesBulkMoveDialogProps = {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
currentFolderId?: string;
|
currentFolderId?: string;
|
||||||
onSuccess?: () => void;
|
onSuccess?: (folderId: string | null) => Promise<void> | void;
|
||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
const ZBulkMoveFormSchema = z.object({
|
const ZBulkMoveFormSchema = z.object({
|
||||||
@@ -99,11 +99,12 @@ export const EnvelopesBulkMoveDialog = ({
|
|||||||
await trpcUtils.template.findTemplates.invalidate();
|
await trpcUtils.template.findTemplates.invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await onSuccess?.(data.folderId);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
description: t`Selected items have been moved.`,
|
description: t`Selected items have been moved.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
onSuccess?.();
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = AppError.parseError(err);
|
const error = AppError.parseError(err);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ZNameSchema } from '@documenso/lib/types/name';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +24,7 @@ import { useParams } from 'react-router';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const ZCreateFolderFormSchema = z.object({
|
const ZCreateFolderFormSchema = z.object({
|
||||||
name: z.string().min(1, { message: 'Folder name is required' }),
|
name: ZNameSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
type TCreateFolderFormSchema = z.infer<typeof ZCreateFolderFormSchema>;
|
type TCreateFolderFormSchema = z.infer<typeof ZCreateFolderFormSchema>;
|
||||||
@@ -65,7 +66,7 @@ export const FolderCreateDialog = ({ type, trigger, parentFolderId, ...props }:
|
|||||||
toast({
|
toast({
|
||||||
description: t`Folder created successfully`,
|
description: t`Folder created successfully`,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (_err) {
|
||||||
toast({
|
toast({
|
||||||
title: t`Failed to create folder`,
|
title: t`Failed to create folder`,
|
||||||
description: t`An unknown error occurred while creating the folder.`,
|
description: t`An unknown error occurred while creating the folder.`,
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDelet
|
|||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans>
|
<Trans>
|
||||||
Confirm by typing:{' '}
|
Confirm by typing:{' '}
|
||||||
<span className="font-semibold font-sm text-destructive">{deleteMessage}</span>
|
<span className="font-semibold text-destructive text-sm">{deleteMessage}</span>
|
||||||
</Trans>
|
</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
|
||||||
|
import { ZNameSchema } from '@documenso/lib/types/name';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
|
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@@ -23,8 +24,6 @@ import { useEffect } from 'react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
|
||||||
|
|
||||||
export type FolderUpdateDialogProps = {
|
export type FolderUpdateDialogProps = {
|
||||||
folder: TFolderWithSubfolders | null;
|
folder: TFolderWithSubfolders | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -32,7 +31,7 @@ export type FolderUpdateDialogProps = {
|
|||||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||||
|
|
||||||
export const ZUpdateFolderFormSchema = z.object({
|
export const ZUpdateFolderFormSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: ZNameSchema,
|
||||||
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
visibility: z.nativeEnum(DocumentVisibility).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,7 +39,6 @@ export type TUpdateFolderFormSchema = z.infer<typeof ZUpdateFolderFormSchema>;
|
|||||||
|
|
||||||
export const FolderUpdateDialog = ({ folder, isOpen, onOpenChange }: FolderUpdateDialogProps) => {
|
export const FolderUpdateDialog = ({ folder, isOpen, onOpenChange }: FolderUpdateDialogProps) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const team = useOptionalCurrentTeam();
|
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { mutateAsync: updateFolder } = trpc.folder.updateFolder.useMutation();
|
const { mutateAsync: updateFolder } = trpc.folder.updateFolder.useMutation();
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ const BillingPlanForm = ({ value, onChange, plans, canCreateFreeOrganisation }:
|
|||||||
>
|
>
|
||||||
<div className="w-full text-left">
|
<div className="w-full text-left">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-medium">
|
<p className="font-medium">
|
||||||
<Trans context="Plan price">Free</Trans>
|
<Trans context="Plan price">Free</Trans>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const OrganisationEmailDomainDeleteDialog = ({
|
|||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans>
|
<Trans>
|
||||||
Confirm by typing{' '}
|
Confirm by typing{' '}
|
||||||
<span className="font-semibold font-sm text-destructive">{deleteMessage}</span>
|
<span className="font-semibold text-destructive text-sm">{deleteMessage}</span>
|
||||||
</Trans>
|
</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user