Compare commits

..

112 Commits

Author SHA1 Message Date
Timur Ercan
71818c0f1f chore: update readme to main version 2023-09-13 14:57:22 +02:00
Timur Ercan
b255eb21e5 Merge pull request #369 from documenso/fix/building-documenso-description
fix: update building documenso article description
2023-09-13 14:45:24 +02:00
Timur Ercan
9a58178ea5 Merge branch 'feat/refresh' into fix/building-documenso-description 2023-09-13 14:42:41 +02:00
Timur Ercan
3c36eedfba chore: phrasing 2023-09-13 14:42:27 +02:00
Timur Ercan
46dfaa70a3 Update apps/marketing/content/blog/building-documenso-pt1.mdx
Co-authored-by: Adithya Krishna  <aadithya794@gmail.com>
2023-09-13 14:39:01 +02:00
Lucas Smith
61da354a48 Merge pull request #361 from documenso/feat/admin-ui-metrics
feat: admin ui for metrics
2023-09-13 21:55:09 +10:00
Lucas Smith
fbb332fb35 Merge branch 'feat/refresh' into feat/admin-ui-metrics 2023-09-13 21:54:33 +10:00
Lucas Smith
7e1cce9155 Merge pull request #365 from documenso/feat/avatar-fallback
feat: add avatar email fallback
2023-09-13 21:51:42 +10:00
Mythie
599e857a1e fix: add removed layout guard 2023-09-12 17:53:38 +10:00
Lucas Smith
581f08c59b fix: update layout and wording 2023-09-12 07:25:44 +00:00
David Nguyen
24a2e9e6d4 feat: update document table layout (#371)
* feat: update document table layout

- Removed dashboard page
- Removed redundant ID column
- Moved date to first column
- Added estimated locales for SSR dates
2023-09-12 14:29:27 +10:00
David Nguyen
e8796a7d86 refactor: organise recipient utils 2023-09-12 12:33:04 +10:00
Mythie
db3f75c42f fix: data table links for recipients 2023-09-12 10:38:23 +10:00
Timur Ercan
e8b5b3b24a fix: update building documenso article description 2023-09-11 15:22:09 +02:00
Catalin Pit
00574325b9 chore: implemented feedback 2023-09-11 13:43:17 +03:00
Catalin Pit
99706e0ed6 chore: fix version in nextjs config 2023-09-11 11:34:10 +03:00
Catalin Pit
326743d8a1 chore: added app version 2023-09-11 10:59:50 +03:00
David Nguyen
3f67b0f27e Merge pull request #292 from documenso/feat/blog-post-next
fix: typo in blog post
2023-09-11 17:09:31 +10:00
flō
24036b0f24 fix typo 2023-09-11 17:03:14 +10:00
David Nguyen
fbf32404a6 feat: add avatar email fallback 2023-09-11 16:58:41 +10:00
Lucas Smith
975d52a07e Merge pull request #362 from documenso/fix/hide-user-selection
fix: hide popover when user selects a recipient
2023-09-11 12:27:50 +10:00
Ephraim Atta-Duncan
f8a193c0f8 refactor: replace whole implementation with a state 2023-09-09 10:56:45 +00:00
Ephraim Atta-Duncan
9186cb4d7b fix: hide popover when user selects a recipients 2023-09-09 10:42:03 +00:00
Lucas Smith
898f5a629c Merge branch 'feat/refresh' into feat/admin-ui-metrics 2023-09-09 15:49:56 +10:00
Mythie
933076fa3f fix: update devcontainer 2023-09-09 15:49:40 +10:00
Lucas Smith
27edcebef6 Merge branch 'feat/refresh' into feat/admin-ui-metrics 2023-09-09 15:44:34 +10:00
Mythie
abc91f7eac fix: update devcontainer 2023-09-09 15:44:10 +10:00
Lucas Smith
5862af3034 Merge branch 'feat/refresh' into feat/admin-ui-metrics 2023-09-09 15:16:03 +10:00
Mythie
35acf05997 feat: add devcontainer 2023-09-09 04:38:37 +00:00
Catalin Pit
5969f148c8 chore: changed the cards titles 2023-09-08 14:51:55 +03:00
Catalin Pit
660f5894a6 chore: feedback improvements 2023-09-08 12:56:44 +03:00
Catalin Pit
77058220a8 chore: rename files 2023-09-08 12:42:14 +03:00
Catalin Pit
6cdba45396 chore: implemented feedback 2023-09-08 12:39:13 +03:00
Catalin Pit
67571158e8 feat: add the admin page 2023-09-08 11:28:50 +03:00
Catalin Pit
171a5ba4ee feat: creating the admin ui for metrics 2023-09-08 09:16:31 +03:00
Lucas Smith
ff957a2f82 Merge pull request #353 from documenso/feat/disable-sign
feat: disable signing and editing for completed documents
2023-09-06 20:53:23 +10:00
Ephraim Atta-Duncan
6640f0496a feat: disable signing and editing for completed documents 2023-09-06 10:40:45 +00:00
Lucas Smith
de3ebe16ee Merge pull request #349 from documenso/feat/marketing-mobile-nav
feat: update marketing mobile nav
2023-09-05 18:20:53 +10:00
David Nguyen
84a2d3baf6 feat: update marketing mobile menu 2023-09-05 18:08:29 +10:00
Lucas Smith
74180defd1 Merge pull request #339 from G3root/feat-api-error
feat: add alert banner for errors in sigin page
2023-09-05 15:36:55 +10:00
Lucas Smith
aeeaaf0d8d Merge pull request #340 from G3root/fix-username-updateable
fix: user name not updatable
2023-09-05 15:33:19 +10:00
Lucas Smith
2b84293c4e Merge pull request #341 from documenso/feat/add-email-field
feat: add email field to document sign page
2023-09-05 13:25:29 +10:00
Lucas Smith
b38ef6c0a7 Merge pull request #346 from documenso/chore/remove-console-log-warn
chore: removed console logs and warn
2023-09-05 13:23:57 +10:00
Mythie
17af4d25bd fix: actually make timeouts clear 2023-09-05 11:33:49 +10:00
Mythie
6e095921e6 fix: tidy up code 2023-09-05 11:29:23 +10:00
nafees nazik
150c42b246 fix: value 2023-09-04 22:24:42 +05:30
Catalin Pit
aecf2f32b9 chore: removed one more console.log 2023-09-04 16:41:28 +03:00
Catalin Pit
b23967d777 chore: removed console.logs and warn 2023-09-04 16:08:22 +03:00
Ephraim Atta-Duncan
b3291c65bc chore: remove console.log 2023-09-02 22:20:57 +00:00
Ephraim Atta-Duncan
4b849e286c feat: add missing email field to document sign page 2023-09-02 22:08:19 +00:00
nafees nazik
7bcc26a987 fix: user name not updatable 2023-09-02 12:11:07 +05:30
nafees nazik
692722d32e revert: fix: component style 2023-09-02 11:55:44 +05:30
Nafees Nazik
e4f06d8e30 Merge branch 'feat/refresh' into feat-api-error 2023-09-02 11:53:18 +05:30
nafees nazik
c799380787 chore: add comments 2023-09-02 11:51:21 +05:30
nafees nazik
5540fcf0d2 fix: use toast 2023-09-02 11:46:12 +05:30
nafees nazik
d9da09c1e7 fix: typo 2023-09-01 16:25:49 +05:30
nafees nazik
fe90aa3b7b feat: add api error 2023-09-01 16:25:27 +05:30
nafees nazik
0c680e0111 fix: component style 2023-09-01 16:25:00 +05:30
Mythie
7bcf5fbd86 feat: store signature on signup 2023-09-01 19:46:44 +10:00
Mythie
7218b950fe feat: store profile signature 2023-09-01 18:43:53 +10:00
Lucas Smith
901013fdc6 Merge pull request #313 from adithyaakrishna/feat/improve-readability
feat: improve readability and rendering of some components
2023-08-31 14:08:52 +10:00
Lucas Smith
5c9017f3cd Merge branch 'feat/refresh' into feat/improve-readability 2023-08-31 14:06:43 +10:00
Mythie
34e962cc48 fix: minor updates 2023-08-31 14:06:19 +10:00
Lucas Smith
bf9254597a Merge pull request #321 from PeterKwesiAnsah/feat/Add-error-message-to-signature-pad
feat: add error message to signature pad
2023-08-31 13:59:41 +10:00
Mythie
b5efa0d3ea fix: fix eslint warnings 2023-08-31 13:33:13 +10:00
Lucas Smith
a2bdb46076 Merge pull request #333 from documenso/fix/redirect-signin-to-dashboard
fix: redirect signin page to dashboard when logged in
2023-08-31 13:14:52 +10:00
Lucas Smith
ed150d9574 Merge branch 'feat/refresh' into fix/redirect-signin-to-dashboard 2023-08-31 13:14:07 +10:00
Mythie
e756a21fda fix: retain redirect to documents rather than dashboard 2023-08-31 13:12:50 +10:00
Lucas Smith
13084049da Merge pull request #334 from documenso/feat/redirect-on-send
feat: redirect to dashboard when document is sent
2023-08-31 13:09:55 +10:00
Lucas Smith
055e723777 Merge pull request #335 from adithyaakrishna/chore/remove-cl
chore: removed unnecessary `console.log`
2023-08-31 13:09:03 +10:00
Lucas Smith
419318c151 Merge pull request #329 from documenso/feat/blog-og-image
feat: add blog og image
2023-08-31 13:04:15 +10:00
Mythie
7722e63e1b fix: tidying broke generation 2023-08-31 12:08:53 +10:00
Adithya Krishna
8529ac3ffe Merge branch 'feat/refresh' into feat/improve-readability 2023-08-30 20:34:08 +05:30
Adithya Krishna
7ec8e762b0 chore: removed console logs
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-30 18:49:13 +05:30
Ephraim Atta-Duncan
2acada6dc7 chore: unused console logs 2023-08-30 13:09:37 +00:00
Ephraim Atta-Duncan
d4d76dce03 feat: redirect to dashboard when document is sent 2023-08-30 12:31:33 +00:00
Ephraim Atta-Duncan
3832ce2c80 fix: redirect root direcotory to dashboard 2023-08-30 11:32:55 +00:00
Ephraim Atta-Duncan
fd36e39a38 fix: redirect signin page to dashboard when logged in 2023-08-30 11:32:08 +00:00
Mythie
14fd0eb906 fix: tidy code and expect jsx errors 2023-08-30 18:41:37 +10:00
Lucas Smith
af6c62d0bf Merge branch 'feat/refresh' into feat/blog-og-image 2023-08-30 18:28:21 +10:00
Mythie
8d7d6a19e7 fix: update import for feature-flag helpers 2023-08-30 18:06:41 +10:00
Mythie
463dc48ea6 fix: extract feature-flag zod schema to separate file 2023-08-30 17:31:23 +10:00
Lucas Smith
d8f6a25059 Merge pull request #332 from documenso/feat/billing-page
feat: make billing page functional
2023-08-30 16:41:39 +10:00
Mythie
93962625ed fix: further stash conflicts 2023-08-30 16:39:35 +10:00
Mythie
249211bd4f fix: add items from stash 2023-08-30 16:36:22 +10:00
Mythie
bfe0d50661 feat: make billing page functional 2023-08-30 16:15:13 +10:00
Mythie
5d4a07bcc5 fix: center align heading 2023-08-30 15:41:29 +10:00
Mythie
d28bb5de99 fix: use nextjs opengraph-image component 2023-08-30 15:32:44 +10:00
Lucas Smith
83a83164d4 Merge pull request #330 from documenso/feat/profile-password-form
feat: avoid user from updating password with the same password
2023-08-30 14:32:21 +10:00
Ephraim Atta-Duncan
1edfe9548d feat: add og image to blog posts 2023-08-30 02:50:02 +00:00
Lucas Smith
fead48c2f0 Merge pull request #314 from adithyaakrishna/fix/whitespace
fix: removed unnecessary whitespace before className
2023-08-30 12:36:58 +10:00
Mythie
0abd3da7fd fix: update eslint rules 2023-08-30 12:35:43 +10:00
Ephraim Atta-Duncan
2f78922421 feat: add blog og image 2023-08-30 02:33:22 +00:00
Lucas Smith
3df0f61947 Merge branch 'feat/refresh' into fix/whitespace 2023-08-30 12:02:01 +10:00
Adithya Krishna
3440c47c3c Merge branch 'feat/refresh' into feat/improve-readability 2023-08-29 12:43:30 +05:30
PeterKwesiAnsah
9257454a96 feat: add error message to signature pad 2023-08-28 14:11:53 +01:00
Adithya Krishna
c161a8109b fix: removed passHref and updated card
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:58:30 +05:30
Adithya Krishna
e340c4ed6f fix: reverted line change
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:54:55 +05:30
Adithya Krishna
b5f96ee2fc chore: made requested changes - v2
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:53:51 +05:30
Adithya Krishna
3c1790ba83 chore: made requested changes
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:45:23 +05:30
Adithya Krishna
f41c78e8e3 feat: updated rendeing of items using map
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:45:23 +05:30
Adithya Krishna
b8b8b4dbad chore: updated footer component
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:45:22 +05:30
Adithya Krishna
d195dc1a46 chore: updated signing fields
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:45:22 +05:30
Adithya Krishna
3ac29d8da3 chore: updated dashboard page
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:45:22 +05:30
Adithya Krishna
2418612507 chore: updated documents page
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:45:22 +05:30
Adithya Krishna
e8336ae9b4 chore: removed eslint-plugin-import
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:34:29 +05:30
Adithya Krishna
aad52a3e2e fix: updated eslint config
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:34:29 +05:30
Adithya Krishna
829122c486 feat: added eslint plugin dependencies
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:34:29 +05:30
Adithya Krishna
090752c539 feat: added new eslint rules
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:34:29 +05:30
Adithya Krishna
fad6414995 fix: duplicate import
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:34:29 +05:30
Adithya Krishna
c817c67a1c fix: removed more unnecessary whitespace
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:34:29 +05:30
Adithya Krishna
c7001e62f3 fix: removed unnecessary whitespace
Signed-off-by: Adithya Krishna <adikrish@redhat.com>
2023-08-28 12:34:29 +05:30
109 changed files with 1515 additions and 2327 deletions

View File

@@ -0,0 +1,20 @@
{
"name": "Documenso",
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"enableNonRootDocker": "true",
"moby": "true"
},
"ghcr.io/devcontainers/features/node:1": {}
},
"onCreateCommand": "./.devcontainer/on-create.sh",
"forwardPorts": [
3000,
54320,
9000,
2500,
1100
]
}

18
.devcontainer/on-create.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Start the database and mailserver
docker compose -f ./docker/compose-without-app.yml up -d
# Install dependencies
npm install
# Copy the env file
cp .env.example .env
# Source the env file, export the variables
set -a
source .env
set +a
# Run the migrations
npm run -w @documenso/prisma prisma:migrate-dev

3
.devcontainer/post-start.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
npm run dev

118
README.md
View File

@@ -1,13 +1,11 @@
<p align="center" style="margin-top: 120px">
<a href="https://github.com/documenso/documenso.com">
<a href="https://github.com/documenso/documenso">
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/cd7823ec-4baa-40b9-be78-4acb3b1c73cb" alt="Documenso Logo">
</a>
<h3 align="center">Open Source Signing Infrastructure</h3>
<p align="center">
The DocuSign Open Source Alternative.
<br />
The Open Source DocuSign Alternative.
<br>
<a href="https://documenso.com"><strong>Learn more »</strong></a>
<br />
<br />
@@ -22,12 +20,16 @@
</p>
<p align="center">
<a href="https://documen.so/discord"><img src="https://img.shields.io/badge/Discord-documen.so/discord-%235865F2" alt="Join Documenso on Discord"></a>
<a href="https://documen.so/discord"><img src="https://img.shields.io/badge/Discord-documen.so/discord-%235865F2" alt="Join Documenso on Discord"></a>
<a href="https://github.com/documenso/documenso/stargazers"><img src="https://img.shields.io/github/stars/documenso/documenso" alt="Github Stars"></a>
<a href="https://github.com/documenso/documenso/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
<a href="https://github.com/documenso/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
</p>
> **🚧 We're currently working on a large scale refactor which can be found on the [feat/refresh](https://github.com/documenso/documenso/tree/feat/refresh) branch.**
>
> **[Read more on why 👀](https://documenso.com/blog/why-were-doing-a-rewrite)**
# Documenso 0.9 - Developer Preview
<div>
@@ -63,18 +65,28 @@ Signing documents digitally is fast, easy and should be best practice for every
## Community and Next Steps 🎯
The current project goal is to <b>[release a production ready version](https://github.com/documenso/documenso/milestone/1)</b> for self-hosting as soon as possible. If you want to help making that happen you can:
We're currently working on a redesign of the application including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon.
- Check out the first source code release in this repository and test it
- Tell us what you think in the current [Discussions](https://github.com/documenso/documenso/discussions)
- Join the [Slack Channel](https://documen.so/slack) 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 to other community members
- ⭐ the repository to help us raise awareness
- Spread the word on Twitter, that Documenso is working towards a more open signing tool
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
## Contributing
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
- To contribute, please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
## Contact us
Contact us if you are interested in our Enterprise plan for large organizations that need extra flexibility and control.
<a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
## Activity
![Repository Activity](https://repobeats.axiom.co/api/embed/622a2e9aa709696f7226304b5b7178a5741b3868.svg)
# Tech
@@ -89,10 +101,6 @@ Documenso is built using awesome open source tech including:
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
- [Zod for schema declaration and validation](https://zod.dev/)
- [Lucide React for icons in React app](https://lucide.dev/)
- [Framer Motion for motion library](https://www.framer.com/motion/)
- [Radix UI for component library](https://www.radix-ui.com/)
- Check out `/package.json` and `/apps/web/package.json` for more
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
@@ -135,37 +143,47 @@ Your database will also be available on port `54320`. You can connect to it usin
## Developer Setup
### Manual Setup
Follow these steps to setup documenso on you local machine:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh
git clone https://github.com/documenso/documenso
```
- Run <code>npm i</code> in root directory
- Rename <code>.env.example</code> to <code>.env</code>
- Run `npm i` in root directory
- Rename `.env.example` to `.env`
- Set DATABASE_URL value in .env file
- You can use the provided test database url (may be wiped at any point)
- Or setup a local postgres sql instance (recommended)
- Create the database scheme by running <code>db-migrate:dev</code>
- Create the database scheme by running `db-migrate:dev`
- Setup your mail provider
- Set <code>SENDGRID_API_KEY</code> value in .env file
- Set `SENDGRID_API_KEY` value in .env file
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* variables</code> in your .env
- Run <code>npm run dev</code> root directory to start
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the `SMTP
\_
* variables` in your .env
- Run `npm run dev` root directory to start
- Register a new user at http://localhost:3000/signup
---
- Optional: Seed the database using <code>npm run db-seed</code> to create a test user and document
- Optional: Upload and sign <code>apps/web/resources/example.pdf</code> manually to test your setup
- Optional: Seed the database using `npm run db-seed` to create a test user and document
- Optional: Upload and sign `apps/web/resources/example.pdf` manually to test your setup
- Optional: Create your own signing certificate
- A demo certificate is provided in `/app/web/resources/certificate.p12`
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](#creating-your-own-signing-certificate)**.
### Run in Gitpod
- Click below to launch a ready-to-use Gitpod workspace in your browser.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/documenso/documenso)
## Updating
- If you pull the newest version from main, using <code>git pull</code>, it may be necessary to regenerate your database client
- If you pull the newest version from main, using `git pull`, it may be necessary to regenerate your database client
- You can do this by running the generate command in `/packages/prisma`:
```sh
npx prisma generate
@@ -176,16 +194,22 @@ Follow these steps to setup documenso on you local machine:
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:
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:\
<code>openssl genrsa -out private.key 2048</code>
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:
`openssl genrsa -out private.key 2048`
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`
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:\
<code>openssl req -new -x509 -key private.key -out certificate.crt -days 365</code> \
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 command to do this: \
<code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code>
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this:
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
5. Place the certificate <code>/apps/web/resources/certificate.p12</code>
5. Place the certificate `/apps/web/resources/certificate.p12`
# Docker
@@ -193,16 +217,42 @@ For the digital signature of your documents you need a signing certificate in .p
Want to create a production ready docker image? Follow these steps:
- Run `./docker/build.sh` in the root directory.
- Publish the image to your docker registry of choice.
- 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
# Deploying - Coming Soon™
```
docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest
```
- Docker support
- One-Click-Deploy on Render.com Deploy
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
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/DjrRRX)
## Render
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso)
# Troubleshooting
## I'm not receiving any emails when using the developer quickstart
When using the developer quickstart an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing email locally for you to view.
The Web UI can be found at http://localhost:9000 while the SMTP port will be on localhost:2500.
## Support IPv6
In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the NextJS start command

View File

@@ -1,98 +1,98 @@
---
title: 'Building Documenso — Part 1: Certificates'
description: In today's fast-paced world, productivity and efficiency are crucial for success, both in personal and professional endeavors. We all strive to make the most of our time and energy to achieve our goals effectively. However, it's not always easy to stay on track and maintain peak performance. In this blog post, we'll explore 10 valuable tips to help you boost productivity and efficiency in your daily life.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2023-06-23
tags:
- Open Source
- Document Signature
- Certificates
- Signing
---
<figure>
<MdxNextImage
src="/blog/blog-banner-building-documenso.webp"
width="1200"
height="675"
alt="Building Documenso blog banner"
/>
<figcaption className="text-center">
What actually is a signature?
</figcaption>
</figure>
> Disclaimer: Im not a lawyer and this isnt legal advice. We plan to publish a much more specific framework on the topic of signature validity.
This is the first installment of the new Building Documenso series, where I describe the challenges and design choices that we make while building the worlds most open signing platform.
As you may have heard, we launched the community-reviewed <a href="https://github.com/documenso/documenso" target="_blank">version 0.9 of Documenso on GitHub</a> recently and its now available through the early adopters plan. One of the most fundamental choices we had to make on this first release, was the choice of certificate. While its interesting to know what we opted for, this shall also serve as a guide for everyone facing the same choice for self-hosting Documenso.
> Question: Why do I need a document signing certificate to self-host?
>
> Short Answer: Inserting the images of a signature into the document is only part of the signing process.
To have an actual digitally signed document you need a document signing certificate that is used to create the digital signature that is inserted into the document, alongside the visible one¹.
When hosting a signature service yourself, as we do, there are four main choices for handling the certificate: Not using a certificate, creating your own, buying a trusted certificate, and becoming and trusted service provider to issue your own trusted certificate.
## 1\. No Certificate
A lot of signing services actually dont employ actual digital signatures besides the inserted image. The only insert and image of the signatures into the document you sign. This can be done and is legally acceptable in many cases. This option isnt directly supported by Documenso without changing the code.
## 2\. Create your own
Since the cryptography behind certificates is freely available as open source you could generate your own using OpenSSL for example. Since its hardly more work than option 1 (using Documenso at least), this would be my minimum effort recommendation. Having a self-created (“self-signed”) certificate doesnt add much in terms of regulation but it guarantees the documents integrity, meaning no changes have been made after signing². What this doesnt give you, is the famous green checkmark in Adobe Acrobat. Why? Because you arent on the list of providers Adobe “trusts”.³
## 3\. Buy a “trusted” certificate.
There are Certificate Authorities (CAs) that can sell you a certificate⁴. The service they provide is, that they validate your name (personal certificates) or your organizations name (corporate certificate) before creating your certificate for you, just like you did in option 2. The difference is, that they are listed on the previously mentioned trust lists (e.g. Adobes) and thus the resulting signatures get a nice, green checkmark in Adobe Reader⁵
## 4\. Becoming a Trusted Certificate Authority (CA) yourself and create your own certificate
This option is an incredibly complex endeavour, requiring a lot of effort and skill. It can be done, as there are multiple CAs around the world. Is it worth the effort? That depends a lot on what youre trying to accomplish.
<center>.&nbsp;&nbsp;.&nbsp;&nbsp;.</center>
## What we did
Having briefly introduced the options, here is what we did: Since we aim to raise the bar on digital signature proliferation and trust, we opted to buy an “Advanced Personal Certificates for Companies/Organisations” from WiseKey. Thus, documents signed with Documensos hosted version look like this:
<figure>
<MdxNextImage
src="/blog/blog-fig-building-documenso.webp"
width="1262"
height="481"
alt="Figure 1"
/>
<figcaption className="text-center">The famous green checkmark: Signed by hosted Documenso</figcaption>
</figure>
There werent any deeper reasons we choose WiseKey, other than they offered what we needed and there wasnt any reason to look much further. While I didnt map the entire certificate market offering (yet), Im pretty sure something similar could be found elsewhere. While we opted for option 3, choosing option 2 might be perfectly reasonable considering your use case.⁶
> While this is our setup, for now, we have a bigger plan for this topic. While globally trusted SSL Certificates have been available for free, courtesy of Lets Encrypt, for a while now, there is no such thing as document signing. And there should be. Not having free and trusted infrastructure for signing is blocking a completely new generation of signing products from being created. This is why well start working on option 4 when the time is right.
Do you have questions or thoughts about this? As always, let me know in the comments, on <a href="http://twitter.com/eltimuro" target="_blank">twitter.com/eltimuro</a>
or directly: <a href="https://documen.so/timur" target="_blank">documen.so/timur</a>
Join the self-hoster community here: <a href="https://documenso.slack.com/" target="_blank">https://documenso.slack.com/</a>
Best from Hamburg
Timur
\[1\] There are different approaches to signing a document. For the sake of simplicity, here we talk about a document with X inserted signature images, that is afterward signed once the by signing service, i.e. Documenso. If each visual signature should have its own digital one (e.g. QES — eIDAS Level 3), the case is a bit more complex.
\[2\] Of course, the signing service provider technically can change and resign the document, especially in the case mentioned in \[1\]. This can be countered by requiring actual digital signatures from each signer, that are bound to their identity/ account. Creating a completely trustless system in the context however is extremely hard to do and not the most pressing business need for the industry at this point, in my opinion. Though, this would be nice.
\[3\] Adobe, like the EU, has a list of organizations they trust. The Adobe green checkmark is powered by the Adobe trust list, if you want to be trusted by EU standards here: <a href="https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation" target="_blank">https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation</a>, you need to be on the EU trust list. Getting on each list is possible, though the latter is much more work.
\[4\] Technically, they sign your certificate creation request (created by you), containing your info with their certificate (which is trusted), making your certificate trusted. This way, everything you sign with your certificate is seen as trusted. They created their certificate just like you, the difference is they are on the lists, mentioned in \[3\]
\[5\] Why does Adobe get to say, what is trusted? They simply happen to have the most used pdf viewer. And since everyone checks there, whom they consider trusted carries weight. If it should be like this, is a different matter.
\[6\] Self-Signed signatures, even purely visual signatures, are fully legally binding. Why you use changes mainly your confidence in the signature and the burden of proof. Also, some industries require a certain level of signatures e.g. retail loans (QES/ eIDAS Level 3 in the EU).
---
title: 'Building Documenso — Part 1: Certificates'
description: This is the first part of the new Building Documenso series, where I describe the challenges and design choices that we make while building the worlds most open signing platform.
authorName: 'Timur Ercan'
authorImage: '/blog/blog-author-timur.jpeg'
authorRole: 'Co-Founder'
date: 2023-06-23
tags:
- Open Source
- Document Signature
- Certificates
- Signing
---
<figure>
<MdxNextImage
src="/blog/blog-banner-building-documenso.webp"
width="1200"
height="675"
alt="Building Documenso blog banner"
/>
<figcaption className="text-center">
What actually is a signature?
</figcaption>
</figure>
> Disclaimer: Im not a lawyer and this isnt legal advice. We plan to publish a much more specific framework on the topic of signature validity.
This is the first installment of the new Building Documenso series, where I describe the challenges and design choices that we make while building the worlds most open signing platform.
As you may have heard, we launched the community-reviewed <a href="https://github.com/documenso/documenso" target="_blank">version 0.9 of Documenso on GitHub</a> recently and its now available through the early adopters plan. One of the most fundamental choices we had to make on this first release, was the choice of certificate. While its interesting to know what we opted for, this shall also serve as a guide for everyone facing the same choice for self-hosting Documenso.
> Question: Why do I need a document signing certificate to self-host?
>
> Short Answer: Inserting the images of a signature into the document is only part of the signing process.
To have an actual digitally signed document you need a document signing certificate that is used to create the digital signature that is inserted into the document, alongside the visible one¹.
When hosting a signature service yourself, as we do, there are four main choices for handling the certificate: Not using a certificate, creating your own, buying a trusted certificate, and becoming and trusted service provider to issue your own trusted certificate.
## 1\. No Certificate
A lot of signing services actually dont employ actual digital signatures besides the inserted image. The only insert and image of the signatures into the document you sign. This can be done and is legally acceptable in many cases. This option isnt directly supported by Documenso without changing the code.
## 2\. Create your own
Since the cryptography behind certificates is freely available as open source you could generate your own using OpenSSL for example. Since its hardly more work than option 1 (using Documenso at least), this would be my minimum effort recommendation. Having a self-created (“self-signed”) certificate doesnt add much in terms of regulation but it guarantees the documents integrity, meaning no changes have been made after signing². What this doesnt give you, is the famous green checkmark in Adobe Acrobat. Why? Because you arent on the list of providers Adobe “trusts”.³
## 3\. Buy a “trusted” certificate.
There are Certificate Authorities (CAs) that can sell you a certificate⁴. The service they provide is, that they validate your name (personal certificates) or your organizations name (corporate certificate) before creating your certificate for you, just like you did in option 2. The difference is, that they are listed on the previously mentioned trust lists (e.g. Adobes) and thus the resulting signatures get a nice, green checkmark in Adobe Reader⁵
## 4\. Becoming a Trusted Certificate Authority (CA) yourself and create your own certificate
This option is an incredibly complex endeavour, requiring a lot of effort and skill. It can be done, as there are multiple CAs around the world. Is it worth the effort? That depends a lot on what youre trying to accomplish.
<center>.&nbsp;&nbsp;.&nbsp;&nbsp;.</center>
## What we did
Having briefly introduced the options, here is what we did: Since we aim to raise the bar on digital signature proliferation and trust, we opted to buy an “Advanced Personal Certificates for Companies/Organisations” from WiseKey. Thus, documents signed with Documensos hosted version look like this:
<figure>
<MdxNextImage
src="/blog/blog-fig-building-documenso.webp"
width="1262"
height="481"
alt="Figure 1"
/>
<figcaption className="text-center">The famous green checkmark: Signed by hosted Documenso</figcaption>
</figure>
There werent any deeper reasons we choose WiseKey, other than they offered what we needed and there wasnt any reason to look much further. While I didnt map the entire certificate market offering (yet), Im pretty sure something similar could be found elsewhere. While we opted for option 3, choosing option 2 might be perfectly reasonable considering your use case.⁶
> While this is our setup, for now, we have a bigger plan for this topic. While globally trusted SSL Certificates have been available for free, courtesy of Lets Encrypt, for a while now, there is no such thing as document signing. And there should be. Not having free and trusted infrastructure for signing is blocking a completely new generation of signing products from being created. This is why well start working on option 4 when the time is right.
Do you have questions or thoughts about this? As always, let me know in the comments, on <a href="http://twitter.com/eltimuro" target="_blank">twitter.com/eltimuro</a>
or directly: <a href="https://documen.so/timur" target="_blank">documen.so/timur</a>
Join the self-hoster community here: <a href="https://documen.so/discord" target="_blank">https://documen.so/discord</a>
Best from Hamburg
Timur
\[1\] There are different approaches to signing a document. For the sake of simplicity, here we talk about a document with X inserted signature images, that is afterward signed once the by signing service, i.e. Documenso. If each visual signature should have its own digital one (e.g. QES — eIDAS Level 3), the case is a bit more complex.
\[2\] Of course, the signing service provider technically can change and resign the document, especially in the case mentioned in \[1\]. This can be countered by requiring actual digital signatures from each signer, that are bound to their identity/ account. Creating a completely trustless system in the context however is extremely hard to do and not the most pressing business need for the industry at this point, in my opinion. Though, this would be nice.
\[3\] Adobe, like the EU, has a list of organizations they trust. The Adobe green checkmark is powered by the Adobe trust list, if you want to be trusted by EU standards here: <a href="https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation" target="_blank">https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation</a>, you need to be on the EU trust list. Getting on each list is possible, though the latter is much more work.
\[4\] Technically, they sign your certificate creation request (created by you), containing your info with their certificate (which is trusted), making your certificate trusted. This way, everything you sign with your certificate is seen as trusted. They created their certificate just like you, the difference is they are on the lists, mentioned in \[3\]
\[5\] Why does Adobe get to say, what is trusted? They simply happen to have the most used pdf viewer. And since everyone checks there, whom they consider trusted carries weight. If it should be like this, is a different matter.
\[6\] Self-Signed signatures, even purely visual signatures, are fully legally binding. Why you use changes mainly your confidence in the signature and the burden of proof. Also, some industries require a certain level of signatures e.g. retail loans (QES/ eIDAS Level 3 in the EU).

View File

@@ -12,7 +12,7 @@ tags:
Since we launched [Documenso 0.9 on Product Hunt](https://producthunt.com/products/documenso#documenso) last May, the team's been hard at work behind the scenes to ramp up development and design to deliver an excellent next version.
Last week, Lucas shared the reasoning how [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite).
Last week, Lucas shared the reasoning on [why we're doing a rewrite](https://documenso.com/blog/why-were-doing-a-rewrite).
Today, I'm pleased to share with you a preview of the next Documenso.

View File

@@ -0,0 +1,76 @@
import { ImageResponse } from 'next/server';
import { allBlogPosts } from 'contentlayer/generated';
export const runtime = 'edge';
export const size = {
width: 1200,
height: 630,
};
export const contentType = 'image/png';
type BlogPostOpenGraphImageProps = {
params: { post: string };
};
export default async function BlogPostOpenGraphImage({ params }: BlogPostOpenGraphImageProps) {
const blogPost = allBlogPosts.find((post) => post._raw.flattenedPath === `blog/${params.post}`);
if (!blogPost) {
return null;
}
// The long urls are needed for a compiler optimisation on the Next.js side, lifting this up
// to a constant will break og image generation.
const [interBold, interRegular, backgroundImage, logoImage] = await Promise.all([
fetch(new URL('./../../../../assets/inter-bold.ttf', import.meta.url)).then(async (res) =>
res.arrayBuffer(),
),
fetch(new URL('./../../../../assets/inter-regular.ttf', import.meta.url)).then(async (res) =>
res.arrayBuffer(),
),
fetch(new URL('./../../../../assets/background-blog-og.png', import.meta.url)).then(
async (res) => res.arrayBuffer(),
),
fetch(new URL('./../../../../../public/logo.png', import.meta.url)).then(async (res) =>
res.arrayBuffer(),
),
]);
return new ImageResponse(
(
<div tw="relative h-full w-full flex flex-col items-center justify-center text-center">
{/* @ts-expect-error Lack of typing from ImageResponse */}
<img src={backgroundImage} alt="og-background" tw="absolute inset-0 w-full h-full" />
{/* @ts-expect-error Lack of typing from ImageResponse */}
<img src={logoImage} alt="logo" tw="h-8" />
<h1 tw="mt-8 text-6xl text-center flex items-center justify-center w-full max-w-[800px] font-bold text-center mx-auto">
{blogPost.title}
</h1>
<p tw="font-normal">Written by {blogPost.authorName}</p>
</div>
),
{
...size,
fonts: [
{
name: 'Inter',
data: interRegular,
style: 'normal',
weight: 400,
},
{
name: 'Inter',
data: interBold,
style: 'normal',
weight: 700,
},
],
},
);
}

View File

@@ -17,7 +17,9 @@ export const generateMetadata = ({ params }: { params: { post: string } }) => {
notFound();
}
return { title: `Documenso - ${blogPost.title}` };
return {
title: `Documenso - ${blogPost.title}`,
};
};
const mdxComponents: MDXComponents = {

View File

@@ -27,7 +27,11 @@ export type ClaimedPlanPageProps = {
export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlanPageProps) {
const { sessionId } = searchParams;
const session = await stripe.checkout.sessions.retrieve(sessionId as string);
if (typeof sessionId !== 'string') {
redirect('/');
}
const session = await stripe.checkout.sessions.retrieve(sessionId);
const user = await prisma.user.findFirst({
where: {
@@ -157,7 +161,6 @@ export default async function ClaimedPlanPage({ searchParams = {} }: ClaimedPlan
</p>
<Link
// eslint-disable-next-line turbo/no-undeclared-env-vars
href={`${process.env.NEXT_PUBLIC_APP_URL}/login`}
target="_blank"
className="mt-4 block"

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -9,6 +9,22 @@ import { cn } from '@documenso/ui/lib/utils';
export type FooterProps = HTMLAttributes<HTMLDivElement>;
const SOCIAL_LINKS = [
{ href: 'https://twitter.com/documenso', icon: <Twitter className="h-6 w-6" /> },
{ href: 'https://github.com/documenso/documenso', icon: <Github className="h-6 w-6" /> },
{ href: 'https://documen.so/discord', icon: <MessagesSquare className="h-6 w-6" /> },
];
const FOOTER_LINKS = [
{ href: '/pricing', text: 'Pricing' },
{ href: '/blog', text: 'Blog' },
{ href: '/open', text: 'Open' },
{ href: 'https://shop.documenso.com', text: 'Shop', target: '_blank' },
{ href: 'https://status.documenso.com', text: 'Status', target: '_blank' },
{ href: 'mailto:support@documenso.com', text: 'Support' },
{ href: '/privacy', text: 'Privacy' },
];
export const Footer = ({ className, ...props }: FooterProps) => {
return (
<div className={cn('border-t py-12', className)} {...props}>
@@ -19,77 +35,25 @@ export const Footer = ({ className, ...props }: FooterProps) => {
</Link>
<div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-4 text-[#8D8D8D]">
<Link
href="https://twitter.com/documenso"
target="_blank"
className="hover:text-[#6D6D6D]"
>
<Twitter className="h-6 w-6" />
</Link>
<Link
href="https://github.com/documenso/documenso"
target="_blank"
className="hover:text-[#6D6D6D]"
>
<Github className="h-6 w-6" />
</Link>
<Link
href="https://documen.so/discord"
target="_blank"
className="hover:text-[#6D6D6D]"
>
<MessagesSquare className="h-6 w-6" />
</Link>
{SOCIAL_LINKS.map((link, index) => (
<Link key={index} href={link.href} target="_blank" className="hover:text-[#6D6D6D]">
{link.icon}
</Link>
))}
</div>
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2.5">
<Link
href="/pricing"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Pricing
</Link>
<Link href="/blog" className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]">
Blog
</Link>
<Link href="/open" className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]">
Open
</Link>
<Link
href="https://shop.documenso.com"
target="_blank"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Shop
</Link>
<Link
href="https://status.documenso.com"
target="_blank"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Status
</Link>
<Link
href="mailto:support@documenso.com"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Support
</Link>
<Link
href="/privacy"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Privacy
</Link>
{FOOTER_LINKS.map((link, index) => (
<Link
key={index}
href={link.href}
target={link.target}
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
{link.text}
</Link>
))}
</div>
</div>
<div className="mx-auto mt-4 w-full max-w-screen-xl px-8 md:mt-12 lg:mt-24">

View File

@@ -22,6 +22,10 @@ export const MENU_NAVIGATION_LINKS = [
href: '/pricing',
text: 'Pricing',
},
{
href: '/open',
text: 'Open',
},
{
href: 'https://status.documenso.com',
text: 'Status',
@@ -59,7 +63,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
initial="initial"
animate="animate"
transition={{
staggerChildren: 0.2,
staggerChildren: 0.03,
}}
>
{MENU_NAVIGATION_LINKS.map(({ href, text }) => (
@@ -75,6 +79,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
x: 0,
transition: {
duration: 0.5,
ease: 'backInOut',
},
},
}}

View File

@@ -22,7 +22,6 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
const event = usePlausible();
const [period, setPeriod] = useState<'MONTHLY' | 'YEARLY'>(() =>
// eslint-disable-next-line turbo/no-undeclared-env-vars
params?.get('planId') === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID
? 'YEARLY'
: 'MONTHLY',
@@ -30,11 +29,9 @@ export const PricingTable = ({ className, ...props }: PricingTableProps) => {
const planId = useMemo(() => {
if (period === 'MONTHLY') {
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
}
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID;
}, [period]);

View File

@@ -139,7 +139,6 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
setTimeout(resolve, 1000);
});
// eslint-disable-next-line turbo/no-undeclared-env-vars
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
const claimPlanInput = signatureDataUrl
@@ -147,7 +146,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
name,
email,
planId,
signatureDataUrl: signatureDataUrl!,
signatureDataUrl: signatureDataUrl,
signatureText: null,
}
: {
@@ -155,7 +154,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
email,
planId,
signatureDataUrl: null,
signatureText: signatureText!,
signatureText: signatureText ?? '',
};
const [result] = await Promise.all([claimPlan(claimPlanInput), delay]);

View File

@@ -43,7 +43,6 @@ export default async function handler(
if (user && user.Subscription.length > 0) {
return res.status(200).json({
// eslint-disable-next-line turbo/no-undeclared-env-vars
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/login`,
});
}
@@ -104,7 +103,6 @@ export default async function handler(
mode: 'subscription',
metadata,
allow_promotion_codes: true,
// eslint-disable-next-line turbo/no-undeclared-env-vars
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing?email=${encodeURIComponent(
email,

View File

@@ -17,14 +17,13 @@ import {
SigningStatus,
} from '@documenso/prisma/client';
const log = (...args: any[]) => console.log('[stripe]', ...args);
const log = (...args: unknown[]) => console.log('[stripe]', ...args);
export const config = {
api: { bodyParser: false },
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// eslint-disable-next-line turbo/no-undeclared-env-vars
// if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
// return res.status(500).json({
// success: false,
@@ -55,6 +54,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
log('event-type:', event.type);
if (event.type === 'checkout.session.completed') {
// This typecast is required since we don't want to create a guard for every event type
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const session = event.data.object as Stripe.Checkout.Session;
if (session.metadata?.source === 'landing') {

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const { version } = require('./package.json');
const { parsed: env } = require('dotenv').config({
path: path.join(__dirname, '../../.env.local'),
@@ -18,7 +19,10 @@ const config = {
'@documenso/ui',
'@documenso/email',
],
env,
env: {
...env,
APP_VERSION: version,
},
modularizeImports: {
'lucide-react': {
transform: 'lucide-react/dist/esm/icons/{{ kebabCase member }}',

View File

@@ -11,6 +11,7 @@
"copy:pdfjs": "node ../../scripts/copy-pdfjs.cjs"
},
"dependencies": {
"@documenso/ee": "*",
"@documenso/lib": "*",
"@documenso/prisma": "*",
"@documenso/tailwind-config": "*",

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { redirect } from 'next/navigation';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import { AdminNav } from './nav';
export type AdminSectionLayoutProps = {
children: React.ReactNode;
};
export default async function AdminSectionLayout({ children }: AdminSectionLayoutProps) {
const user = await getRequiredServerComponentSession();
if (!isAdmin(user)) {
redirect('/documents');
}
return (
<div className="mx-auto mt-16 w-full max-w-screen-xl px-4 md:px-8">
<div className="grid grid-cols-12 gap-x-8 md:mt-8">
<AdminNav className="col-span-12 md:col-span-3 md:flex" />
<div className="col-span-12 mt-12 md:col-span-9 md:mt-0">{children}</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,47 @@
'use client';
import { HTMLAttributes } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { BarChart3, User2 } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
export type AdminNavProps = HTMLAttributes<HTMLDivElement>;
export const AdminNav = ({ className, ...props }: AdminNavProps) => {
const pathname = usePathname();
return (
<div className={cn('flex gap-x-2.5 gap-y-2 md:flex-col', className)} {...props}>
<Button
variant="ghost"
className={cn(
'justify-start md:w-full',
pathname?.startsWith('/admin/stats') && 'bg-secondary',
)}
asChild
>
<Link href="/admin/stats">
<BarChart3 className="mr-2 h-5 w-5" />
Stats
</Link>
</Button>
<Button
variant="ghost"
className={cn(
'justify-start md:w-full',
pathname?.startsWith('/admin/users') && 'bg-secondary',
)}
disabled
>
<User2 className="mr-2 h-5 w-5" />
Users (Coming Soon)
</Button>
</div>
);
};

View File

@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';
export default function Admin() {
redirect('/admin/stats');
}

View File

@@ -0,0 +1,75 @@
import {
File,
FileCheck,
FileClock,
FileEdit,
Mail,
MailOpen,
PenTool,
User as UserIcon,
UserPlus2,
UserSquare2,
} from 'lucide-react';
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
import {
getUsersCount,
getUsersWithSubscriptionsCount,
} from '@documenso/lib/server-only/admin/get-users-stats';
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
export default async function AdminStatsPage() {
const [usersCount, usersWithSubscriptionsCount, docStats, recipientStats] = await Promise.all([
getUsersCount(),
getUsersWithSubscriptionsCount(),
getDocumentStats(),
getRecipientsStats(),
]);
return (
<div>
<h2 className="text-4xl font-semibold">Instance Stats</h2>
<div className="mt-8 grid flex-1 grid-cols-1 gap-4 md:grid-cols-4">
<CardMetric icon={UserIcon} title="Total Users" value={usersCount} />
<CardMetric icon={File} title="Total Documents" value={docStats.ALL} />
<CardMetric
icon={UserPlus2}
title="Active Subscriptions"
value={usersWithSubscriptionsCount}
/>
<CardMetric icon={UserPlus2} title="App Version" value={`v${process.env.APP_VERSION}`} />
</div>
<div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-2">
<div>
<h3 className="text-3xl font-semibold">Document metrics</h3>
<div className="mt-8 grid flex-1 grid-cols-2 gap-4">
<CardMetric icon={File} title="Total Documents" value={docStats.ALL} />
<CardMetric icon={FileEdit} title="Drafted Documents" value={docStats.DRAFT} />
<CardMetric icon={FileClock} title="Pending Documents" value={docStats.PENDING} />
<CardMetric icon={FileCheck} title="Completed Documents" value={docStats.COMPLETED} />
</div>
</div>
<div>
<h3 className="text-3xl font-semibold">Recipients metrics</h3>
<div className="mt-8 grid flex-1 grid-cols-2 gap-4">
<CardMetric
icon={UserSquare2}
title="Total Recipients"
value={recipientStats.TOTAL_RECIPIENTS}
/>
<CardMetric icon={Mail} title="Documents Received" value={recipientStats.SENT} />
<CardMetric icon={MailOpen} title="Documents Viewed" value={recipientStats.OPENED} />
<CardMetric icon={PenTool} title="Signatures Collected" value={recipientStats.SIGNED} />
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,109 +0,0 @@
import Link from 'next/link';
import { Clock, File, FileCheck } from 'lucide-react';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getStats } from '@documenso/lib/server-only/document/get-stats';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@documenso/ui/primitives/table';
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
import { DocumentStatus } from '~/components/formatter/document-status';
import { LocaleDate } from '~/components/formatter/locale-date';
import { UploadDocument } from './upload-document';
export default async function DashboardPage() {
const user = await getRequiredServerComponentSession();
const [stats, results] = await Promise.all([
getStats({
user,
}),
findDocuments({
userId: user.id,
perPage: 10,
}),
]);
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<h1 className="text-4xl font-semibold">Dashboard</h1>
<div className="mt-8 grid grid-cols-1 gap-4 md:grid-cols-3">
<Link href={'/documents?status=COMPLETED'} passHref>
<CardMetric icon={FileCheck} title="Completed" value={stats.COMPLETED} />
</Link>
<Link href={'/documents?status=DRAFT'} passHref>
<CardMetric icon={File} title="Drafts" value={stats.DRAFT} />
</Link>
<Link href={'/documents?status=PENDING'} passHref>
<CardMetric icon={Clock} title="Pending" value={stats.PENDING} />
</Link>
</div>
<div className="mt-12">
<UploadDocument />
<h2 className="mt-8 text-2xl font-semibold">Recent Documents</h2>
<div className="border-border mt-8 overflow-x-auto rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">ID</TableHead>
<TableHead>Title</TableHead>
<TableHead>Reciepient</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Created</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{results.data.map((document) => {
return (
<TableRow key={document.id}>
<TableCell className="font-medium">{document.id}</TableCell>
<TableCell>
<Link
href={`/documents/${document.id}`}
className="focus-visible:ring-ring ring-offset-background rounded-md font-medium hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
>
{document.title}
</Link>
</TableCell>
<TableCell>
<StackAvatarsWithTooltip recipients={document.Recipient} />
</TableCell>
<TableCell>
<DocumentStatus status={document.status} />
</TableCell>
<TableCell className="text-right">
<LocaleDate date={document.created} />
</TableCell>
</TableRow>
);
})}
{results.data.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
);
}

View File

@@ -130,7 +130,13 @@ export const EditDocumentForm = ({
},
});
router.refresh();
toast({
title: 'Document sent',
description: 'Your document has been sent successfully.',
duration: 5000,
});
router.push('/documents');
} catch (err) {
console.error(err);

View File

@@ -82,14 +82,14 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
<DropdownMenuContent className="w-52" align="start" forceMount>
<DropdownMenuLabel>Action</DropdownMenuLabel>
<DropdownMenuItem disabled={!recipient} asChild>
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
<Link href={`/sign/${recipient?.token}`}>
<Pencil className="mr-2 h-4 w-4" />
Sign
</Link>
</DropdownMenuItem>
<DropdownMenuItem disabled={!isOwner} asChild>
<DropdownMenuItem disabled={!isOwner || isComplete} asChild>
<Link href={`/documents/${row.id}`}>
<Edit className="mr-2 h-4 w-4" />
Edit

View File

@@ -0,0 +1,56 @@
'use client';
import Link from 'next/link';
import { useSession } from 'next-auth/react';
import { match } from 'ts-pattern';
import { Document, Recipient, User } from '@documenso/prisma/client';
export type DataTableTitleProps = {
row: Document & {
User: Pick<User, 'id' | 'name' | 'email'>;
Recipient: Recipient[];
};
};
export const DataTableTitle = ({ row }: DataTableTitleProps) => {
const { data: session } = useSession();
if (!session) {
return null;
}
const recipient = row.Recipient.find((recipient) => recipient.email === session.user.email);
const isOwner = row.User.id === session.user.id;
const isRecipient = !!recipient;
return match({
isOwner,
isRecipient,
})
.with({ isOwner: true }, () => (
<Link
href={`/documents/${row.id}`}
title={row.title}
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]"
>
{row.title}
</Link>
))
.with({ isRecipient: true }, () => (
<Link
href={`/sign/${recipient?.token}`}
title={row.title}
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]"
>
{row.title}
</Link>
))
.otherwise(() => (
<span className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]">
{row.title}
</span>
));
};

View File

@@ -2,9 +2,8 @@
import { useTransition } from 'react';
import Link from 'next/link';
import { Loader } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
import { FindResultSet } from '@documenso/lib/types/find-result-set';
@@ -18,6 +17,7 @@ import { LocaleDate } from '~/components/formatter/locale-date';
import { DataTableActionButton } from './data-table-action-button';
import { DataTableActionDropdown } from './data-table-action-dropdown';
import { DataTableTitle } from './data-table-title';
export type DocumentsDataTableProps = {
results: FindResultSet<
@@ -29,6 +29,7 @@ export type DocumentsDataTableProps = {
};
export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
const { data: session } = useSession();
const [isPending, startTransition] = useTransition();
const updateSearchParams = useUpdateSearchParams();
@@ -42,25 +43,22 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
});
};
if (!session) {
return null;
}
return (
<div className="relative">
<DataTable
columns={[
{
header: 'ID',
accessorKey: 'id',
header: 'Created',
accessorKey: 'created',
cell: ({ row }) => <LocaleDate date={row.getValue('created')} />,
},
{
header: 'Title',
cell: ({ row }) => (
<Link
href={`/documents/${row.original.id}`}
title={row.original.title}
className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]"
>
{row.original.title}
</Link>
),
cell: ({ row }) => <DataTableTitle row={row.original} />,
},
{
header: 'Recipient',
@@ -74,11 +72,6 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
accessorKey: 'status',
cell: ({ row }) => <DocumentStatus status={row.getValue('status')} />,
},
{
header: 'Created',
accessorKey: 'created',
cell: ({ row }) => <LocaleDate date={row.getValue('created')} />,
},
{
header: 'Actions',
cell: ({ row }) => (
@@ -95,7 +88,7 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => {
totalPages={results.totalPages}
onPaginationChange={onPaginationChange}
>
{(table) => <DataTablePagination table={table} />}
{(table) => <DataTablePagination additionalInformation="VisibleCount" table={table} />}
</DataTable>
{isPending && (

View File

@@ -11,8 +11,8 @@ import { PeriodSelector } from '~/components/(dashboard)/period-selector/period-
import { PeriodSelectorValue } from '~/components/(dashboard)/period-selector/types';
import { DocumentStatus } from '~/components/formatter/document-status';
import { UploadDocument } from '../dashboard/upload-document';
import { DocumentsDataTable } from './data-table';
import { UploadDocument } from './upload-document';
export type DocumentsPageProps = {
searchParams?: {
@@ -81,6 +81,7 @@ export default async function DocumentsPage({ searchParams = {} }: DocumentsPage
{value !== ExtendedDocumentStatus.ALL && (
<span className="ml-1 hidden opacity-50 md:inline-block">
{Math.min(stats[value], 99)}
{stats[value] > 99 && '+'}
</span>
)}
</Link>

View File

@@ -1,8 +1,14 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer';
import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session';
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
import { SubscriptionStatus } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { PasswordForm } from '~/components/forms/password';
import { LocaleDate } from '~/components/formatter/locale-date';
import { getServerComponentFlag } from '~/helpers/get-server-component-feature-flag';
export default async function BillingSettingsPage() {
@@ -15,17 +21,55 @@ export default async function BillingSettingsPage() {
redirect('/settings/profile');
}
let subscription = await getSubscriptionByUserId({ userId: user.id });
// If we don't have a customer record, create one as well as an empty subscription.
if (!subscription?.customerId) {
subscription = await createCustomer({ user });
}
let billingPortalUrl = '';
if (subscription?.customerId) {
billingPortalUrl = await getPortalSession({
customerId: subscription.customerId,
returnUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/settings/billing`,
});
}
return (
<div>
<h3 className="text-lg font-medium">Billing</h3>
<p className="mt-2 text-sm text-slate-500">
Here you can update and manage your subscription.
Your subscription is{' '}
{subscription.status !== SubscriptionStatus.INACTIVE ? 'active' : 'inactive'}.
{subscription?.periodEnd && (
<>
{' '}
Your next payment is due on{' '}
<span className="font-semibold">
<LocaleDate date={subscription.periodEnd} />
</span>
.
</>
)}
</p>
<hr className="my-4" />
<PasswordForm user={user} className="max-w-xl" />
{billingPortalUrl && (
<Button asChild>
<Link href={billingPortalUrl}>Manage Subscription</Link>
</Button>
)}
{!billingPortalUrl && (
<p className="max-w-[60ch] text-base text-slate-500">
You do not currently have a customer record, this should not happen. Please contact
support for assistance.
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,96 @@
'use client';
import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Loader } from 'lucide-react';
import { Recipient } from '@documenso/prisma/client';
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredSigningContext } from './provider';
import { SigningFieldContainer } from './signing-field-container';
export type EmailFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
};
export const EmailField = ({ field, recipient }: EmailFieldProps) => {
const router = useRouter();
const { toast } = useToast();
const { email: providedEmail } = useRequiredSigningContext();
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation();
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation();
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
const onSign = async () => {
try {
await signFieldWithToken({
token: recipient.token,
fieldId: field.id,
value: providedEmail ?? '',
isBase64: false,
});
startTransition(() => router.refresh());
} catch (err) {
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
variant: 'destructive',
});
}
};
const onRemove = async () => {
try {
await removeSignedFieldWithToken({
token: recipient.token,
fieldId: field.id,
});
startTransition(() => router.refresh());
} catch (err) {
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the signature.',
variant: 'destructive',
});
}
};
return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove}>
{isLoading && (
<div className="bg-background absolute inset-0 flex items-center justify-center">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
{!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground text-lg duration-200">Email</p>
)}
{field.inserted && <p className="text-muted-foreground duration-200">{field.customText}</p>}
</SigningFieldContainer>
);
};

View File

@@ -87,9 +87,6 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
className="h-44 w-full"
defaultValue={signature ?? undefined}
onChange={(value) => {
console.log({
signpadValue: value,
});
setSignature(value);
}}
/>

View File

@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation';
import { match } from 'ts-pattern';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
@@ -13,6 +14,7 @@ import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
import { DateField } from './date-field';
import { EmailField } from './email-field';
import { SigningForm } from './form';
import { NameField } from './name-field';
import { SigningProvider } from './provider';
@@ -42,10 +44,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
return notFound();
}
const user = await getServerComponentSession();
const documentUrl = `data:application/pdf;base64,${document.document}`;
return (
<SigningProvider email={recipient.email} fullName={recipient.name}>
<SigningProvider email={recipient.email} fullName={recipient.name} signature={user?.signature}>
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}>
{document.title}
@@ -84,6 +88,9 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
.with(FieldType.DATE, () => (
<DateField key={field.id} field={field} recipient={recipient} />
))
.with(FieldType.EMAIL, () => (
<EmailField key={field.id} field={field} recipient={recipient} />
))
.otherwise(() => null),
)}
</ElementVisible>

View File

@@ -28,9 +28,9 @@ export const useRequiredSigningContext = () => {
};
export interface SigningProviderProps {
fullName?: string;
email?: string;
signature?: string;
fullName?: string | null;
email?: string | null;
signature?: string | null;
children: React.ReactNode;
}

View File

@@ -63,11 +63,6 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
const onSign = async (source: 'local' | 'provider' = 'provider') => {
try {
console.log({
providedSignature,
localSignature,
});
if (!providedSignature && !localSignature) {
setShowSignatureModal(true);
return;
@@ -141,6 +136,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => {
{state === 'signed-text' && (
<p className="font-signature text-muted-foreground text-lg duration-200 sm:text-xl md:text-2xl lg:text-3xl">
{/* This optional chaining is intentional, we don't want to move the check into the condition above */}
{signature?.typedSignature}
</p>
)}

View File

@@ -2,6 +2,8 @@ import { Suspense } from 'react';
import { Caveat, Inter } from 'next/font/google';
import { LocaleProvider } from '@documenso/lib/client-only/providers/locale';
import { getLocale } from '@documenso/lib/server-only/headers/get-locale';
import { TrpcProvider } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Toaster } from '@documenso/ui/primitives/toaster';
@@ -45,6 +47,8 @@ export const metadata = {
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const flags = await getServerComponentAllFlags();
const locale = getLocale();
return (
<html
lang="en"
@@ -63,16 +67,18 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</Suspense>
<body>
<FeatureFlagProvider initialFlags={flags}>
<PlausibleProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<TooltipProvider>
<TrpcProvider>{children}</TrpcProvider>
</TooltipProvider>
</ThemeProvider>
</PlausibleProvider>
<Toaster />
</FeatureFlagProvider>
<LocaleProvider locale={locale}>
<FeatureFlagProvider initialFlags={flags}>
<PlausibleProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<TooltipProvider>
<TrpcProvider>{children}</TrpcProvider>
</TooltipProvider>
</ThemeProvider>
</PlausibleProvider>
<Toaster />
</FeatureFlagProvider>
</LocaleProvider>
</body>
</html>
);

View File

@@ -15,7 +15,7 @@ export type StackAvatarProps = {
type: 'unsigned' | 'waiting' | 'opened' | 'completed';
};
export const StackAvatar = ({ first, zIndex, fallbackText, type }: StackAvatarProps) => {
export const StackAvatar = ({ first, zIndex, fallbackText = '', type }: StackAvatarProps) => {
let classes = '';
let zIndexClass = '';
const firstClass = first ? '' : '-ml-3';
@@ -48,7 +48,7 @@ export const StackAvatar = ({ first, zIndex, fallbackText, type }: StackAvatarPr
${firstClass}
dark:border-border h-10 w-10 border-2 border-solid border-white`}
>
<AvatarFallback className={classes}>{fallbackText ?? 'UK'}</AvatarFallback>
<AvatarFallback className={classes}>{fallbackText}</AvatarFallback>
</Avatar>
);
};

View File

@@ -1,5 +1,5 @@
import { initials } from '@documenso/lib/client-only/recipient-initials';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import { Recipient } from '@documenso/prisma/client';
import {
Tooltip,
@@ -56,7 +56,7 @@ export const StackAvatarsWithTooltip = ({
first={true}
key={recipient.id}
type={getRecipientType(recipient)}
fallbackText={initials(recipient.name)}
fallbackText={recipientAbbreviation(recipient)}
/>
<span className="text-sm text-gray-500">{recipient.email}</span>
</div>
@@ -73,7 +73,7 @@ export const StackAvatarsWithTooltip = ({
first={true}
key={recipient.id}
type={getRecipientType(recipient)}
fallbackText={initials(recipient.name)}
fallbackText={recipientAbbreviation(recipient)}
/>
<span className="text-sm text-gray-500">{recipient.email}</span>
</div>
@@ -90,7 +90,7 @@ export const StackAvatarsWithTooltip = ({
first={true}
key={recipient.id}
type={getRecipientType(recipient)}
fallbackText={initials(recipient.name)}
fallbackText={recipientAbbreviation(recipient)}
/>
<span className="text-sm text-gray-500">{recipient.email}</span>
</div>
@@ -107,7 +107,7 @@ export const StackAvatarsWithTooltip = ({
first={true}
key={recipient.id}
type={getRecipientType(recipient)}
fallbackText={initials(recipient.name)}
fallbackText={recipientAbbreviation(recipient)}
/>
<span className="text-sm text-gray-500">{recipient.email}</span>
</div>

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { initials } from '@documenso/lib/client-only/recipient-initials';
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
import { Recipient } from '@documenso/prisma/client';
import { StackAvatar } from './stack-avatar';
@@ -26,7 +26,7 @@ export function StackAvatars({ recipients }: { recipients: Recipient[] }) {
first={first}
zIndex={String(zIndex - index * 10)}
type={lastItemText && index === 4 ? 'unsigned' : getRecipientType(recipient)}
fallbackText={lastItemText ? lastItemText : initials(recipient.name)}
fallbackText={lastItemText ? lastItemText : recipientAbbreviation(recipient)}
/>
);
});

View File

@@ -15,7 +15,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
{/* <Link
href="/documents"
className={cn(
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 ',
'text-muted-foreground focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
{
'text-foreground': pathname?.startsWith('/documents'),
},

View File

@@ -11,10 +11,13 @@ import {
Monitor,
Moon,
Sun,
UserCog,
} from 'lucide-react';
import { signOut } from 'next-auth/react';
import { useTheme } from 'next-themes';
import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
import { recipientInitials } from '@documenso/lib/utils/recipient-formatter';
import { User } from '@documenso/prisma/client';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
@@ -35,24 +38,21 @@ export type ProfileDropdownProps = {
export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
const { theme, setTheme } = useTheme();
const { getFlag } = useFeatureFlags();
const isUserAdmin = isAdmin(user);
const isBillingEnabled = getFlag('app_billing');
const initials =
user.name
?.split(' ')
.map((name: string) => name.slice(0, 1).toUpperCase())
.slice(0, 2)
.join('') ?? 'UK';
const avatarFallback = user.name
? recipientInitials(user.name)
: user.email.slice(0, 1).toUpperCase();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-10 w-10 rounded-full">
<Avatar className="h-10 w-10">
<AvatarFallback>{initials}</AvatarFallback>
<AvatarFallback>{avatarFallback}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
@@ -60,6 +60,19 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel>Account</DropdownMenuLabel>
{isUserAdmin && (
<>
<DropdownMenuItem asChild>
<Link href="/admin" className="cursor-pointer">
<UserCog className="mr-2 h-4 w-4" />
Admin
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem asChild>
<Link href="/settings/profile" className="cursor-pointer">
<LucideUser className="mr-2 h-4 w-4" />

View File

@@ -18,10 +18,10 @@ export const CardMetric = ({ icon: Icon, title, value, className }: CardMetricPr
)}
>
<div className="px-4 pb-6 pt-4 sm:px-4 sm:pb-8 sm:pt-4">
<div className="flex items-start">
{Icon && <Icon className="mr-2 h-4 w-4 text-slate-500" />}
<div className="flex items-center">
{Icon && <Icon className="text-muted-foreground mr-2 h-4 w-4" />}
<h3 className="flex items-end text-sm font-medium text-slate-500">{title}</h3>
<h3 className="text-primary-forground flex items-end text-sm font-medium">{title}</h3>
</div>
<p className="text-foreground mt-6 text-4xl font-semibold leading-8 md:mt-8">

View File

@@ -1,5 +1,6 @@
export type PeriodSelectorValue = '' | '7d' | '14d' | '30d';
export const isPeriodSelectorValue = (value: unknown): value is PeriodSelectorValue => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return ['', '7d', '14d', '30d'].includes(value as string);
};

View File

@@ -1,66 +0,0 @@
'use client';
import Link from 'next/link';
import { Github } from 'lucide-react';
import { usePlausible } from 'next-plausible';
import { Button } from '@documenso/ui/primitives/button';
export type CalloutProps = {
starCount?: number;
[key: string]: unknown;
};
export const Callout = ({ starCount }: CalloutProps) => {
const event = usePlausible();
const onSignUpClick = () => {
const el = document.getElementById('email');
if (el) {
const { top } = el.getBoundingClientRect();
window.scrollTo({
top: top - 120,
behavior: 'smooth',
});
setTimeout(() => {
el.focus();
}, 500);
}
};
return (
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-4">
<Button
type="button"
variant="outline"
className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick}
>
Get the Community Plan
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever!
</span>
</Button>
<Link
href="https://github.com/documenso/documenso"
target="_blank"
onClick={() => event('view-github')}
>
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
<Github className="mr-2 h-5 w-5" />
Star on Github
{starCount && starCount > 0 && (
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
{starCount.toLocaleString('en-US')}
</span>
)}
</Button>
</Link>
</div>
);
};

View File

@@ -1,150 +0,0 @@
'use client';
import React, { useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { Info, Loader } from 'lucide-react';
import { usePlausible } from 'next-plausible';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { claimPlan } from '~/api/claim-plan/fetcher';
import { FormErrorMessage } from '../form/form-error-message';
export const ZClaimPlanDialogFormSchema = z.object({
name: z.string().min(3),
email: z.string().email(),
});
export type TClaimPlanDialogFormSchema = z.infer<typeof ZClaimPlanDialogFormSchema>;
export type ClaimPlanDialogProps = {
className?: string;
planId: string;
children: React.ReactNode;
};
export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => {
const params = useSearchParams();
const { toast } = useToast();
const event = usePlausible();
const [open, setOpen] = useState(() => params?.get('cancelled') === 'true');
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<TClaimPlanDialogFormSchema>({
mode: 'onBlur',
defaultValues: {
name: params?.get('name') ?? '',
email: params?.get('email') ?? '',
},
resolver: zodResolver(ZClaimPlanDialogFormSchema),
});
const onFormSubmit = async ({ name, email }: TClaimPlanDialogFormSchema) => {
try {
const delay = new Promise<void>((resolve) => {
setTimeout(resolve, 1000);
});
const [redirectUrl] = await Promise.all([
claimPlan({ name, email, planId, signatureText: name, signatureDataUrl: null }),
delay,
]);
event('claim-plan-pricing');
window.location.href = redirectUrl;
} catch (error) {
event('claim-plan-failed');
toast({
title: 'Something went wrong',
description: error instanceof Error ? error.message : 'Please try again later.',
variant: 'destructive',
});
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Claim your plan</DialogTitle>
<DialogDescription className="mt-4">
We're almost there! Please enter your email address and name to claim your plan.
</DialogDescription>
</DialogHeader>
<form
className={cn('flex flex-col gap-y-4', className)}
onSubmit={handleSubmit(onFormSubmit)}
>
{params?.get('cancelled') === 'true' && (
<div className="rounded-lg border border-yellow-400 bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<Info className="h-5 w-5 text-yellow-400" />
</div>
<div className="ml-3">
<p className="text-sm leading-5 text-yellow-700">
You have cancelled the payment process. If you didn't mean to do this, please
try again.
</p>
</div>
</div>
</div>
)}
<div>
<Label className="text-slate-500">Name</Label>
<Input type="text" className="mt-2" {...register('name')} autoFocus />
<FormErrorMessage className="mt-1" error={errors.name} />
</div>
<div>
<Label className="text-slate-500">Email</Label>
<Input type="email" className="mt-2" {...register('email')} />
<FormErrorMessage className="mt-1" error={errors.email} />
</div>
<Button type="submit" size="lg" disabled={isSubmitting}>
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}
Claim the Community Plan ({/* eslint-disable-next-line turbo/no-undeclared-env-vars */}
{planId === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
? 'Monthly'
: 'Yearly'}
)
</Button>
</form>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,77 +0,0 @@
import { HTMLAttributes } from 'react';
import Image from 'next/image';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import backgroundPattern from '~/assets/background-pattern.png';
import cardBeautifulFigure from '~/assets/card-beautiful-figure.png';
import cardFastFigure from '~/assets/card-fast-figure.png';
import cardSmartFigure from '~/assets/card-smart-figure.png';
export type FasterSmarterBeautifulBentoProps = HTMLAttributes<HTMLDivElement>;
export const FasterSmarterBeautifulBento = ({
className,
...props
}: FasterSmarterBeautifulBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}
alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
/>
</div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
A 10x better signing experience.
<span className="block md:mt-0">Faster, smarter and more beautiful.</span>
</h2>
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
<Card className="col-span-2" degrees={45} gradient>
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
<p className="col-span-12 leading-relaxed text-[#555E67] lg:col-span-6">
<strong className="block">Fast.</strong>
When it comes to sending or receiving a contract, you can count on lightning-fast
speeds.
</p>
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
<Image src={cardFastFigure} alt="its fast" className="max-w-[80%] lg:max-w-none" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Beautiful.</strong>
Because signing should be celebrated. Thats why we care about the smallest detail in
our product.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardBeautifulFigure} alt="its fast" className="w-full max-w-xs" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Smart.</strong>
Our custom templates come with smart rules that can help you save time and energy.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardSmartFigure} alt="its fast" className="w-full max-w-[16rem]" />
</div>
</CardContent>
</Card>
</div>
</div>
);
};

View File

@@ -1,86 +0,0 @@
import { HTMLAttributes } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { Github, Slack, Twitter } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
export type FooterProps = HTMLAttributes<HTMLDivElement>;
export const Footer = ({ className, ...props }: FooterProps) => {
return (
<div className={cn('border-t py-12', className)} {...props}>
<div className="mx-auto flex w-full max-w-screen-xl flex-wrap items-start justify-between gap-8 px-8">
<div>
<Link href="/">
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image>
</Link>
<div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-4 text-[#8D8D8D]">
<Link
href="https://twitter.com/documenso"
target="_blank"
className="hover:text-[#6D6D6D]"
>
<Twitter className="h-6 w-6" />
</Link>
<Link
href="https://github.com/documenso/documenso"
target="_blank"
className="hover:text-[#6D6D6D]"
>
<Github className="h-6 w-6" />
</Link>
<Link
href="https://documenso.slack.com"
target="_blank"
className="hover:text-[#6D6D6D]"
>
<Slack className="h-6 w-6" />
</Link>
</div>
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2.5">
<Link
href="/pricing"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Pricing
</Link>
<Link
href="https://status.documenso.com"
target="_blank"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Status
</Link>
<Link
href="mailto:support@documenso.com"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Support
</Link>
{/* <Link
href="/privacy"
className="flex-shrink-0 text-sm text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Privacy
</Link> */}
</div>
</div>
<div className="mx-auto mt-4 w-full max-w-screen-xl px-8 md:mt-12 lg:mt-24">
<p className="text-sm text-[#8D8D8D]">
© {new Date().getFullYear()} Documenso, Inc. All rights reserved.
</p>
</div>
</div>
);
};

View File

@@ -1,32 +0,0 @@
import { HTMLAttributes } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { cn } from '@documenso/ui/lib/utils';
export type HeaderProps = HTMLAttributes<HTMLElement>;
export const Header = ({ className, ...props }: HeaderProps) => {
return (
<header className={cn('flex items-center justify-between', className)} {...props}>
<Link href="/">
<Image src="/logo.png" alt="Documenso Logo" width={170} height={0}></Image>
</Link>
<div className="flex items-center gap-x-6">
<Link href="/pricing" className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]">
Pricing
</Link>
<Link
href="https://app.documenso.com/login"
target="_blank"
className="text-sm font-semibold text-[#8D8D8D] hover:text-[#6D6D6D]"
>
Sign in
</Link>
</div>
</header>
);
};

View File

@@ -1,225 +0,0 @@
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { Variants, motion } from 'framer-motion';
import { Github } from 'lucide-react';
import { usePlausible } from 'next-plausible';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import backgroundPattern from '~/assets/background-pattern.png';
import { Widget } from './widget';
export type HeroProps = {
className?: string;
starCount?: number;
[key: string]: unknown;
};
const BackgroundPatternVariants: Variants = {
initial: {
opacity: 0,
},
animate: {
opacity: 1,
transition: {
delay: 1,
duration: 1.2,
},
},
};
const HeroTitleVariants: Variants = {
initial: {
opacity: 0,
y: 60,
},
animate: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
},
},
};
export const Hero = ({ className, starCount, ...props }: HeroProps) => {
const event = usePlausible();
const onSignUpClick = () => {
const el = document.getElementById('email');
if (el) {
const { top } = el.getBoundingClientRect();
window.scrollTo({
top: top - 120,
behavior: 'smooth',
});
requestAnimationFrame(() => {
el.focus();
});
}
};
return (
<motion.div className={cn('relative', className)} {...props}>
<div className="absolute -inset-24 -z-10">
<motion.div
className="flex h-full w-full origin-top-right items-center justify-center"
variants={BackgroundPatternVariants}
initial="initial"
animate="animate"
>
<Image
src={backgroundPattern}
alt="background pattern"
className="-mr-[50vw] -mt-[15vh] h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
/>
</motion.div>
</div>
<div className="relative">
<motion.h2
variants={HeroTitleVariants}
initial="initial"
animate="animate"
className="text-center text-4xl font-bold leading-tight tracking-tight lg:text-[64px]"
>
Document signing,
<span className="block" /> finally open source.
</motion.h2>
<motion.div
variants={HeroTitleVariants}
initial="initial"
animate="animate"
className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-4"
>
<Button
type="button"
variant="outline"
className="rounded-full bg-transparent backdrop-blur-sm"
onClick={onSignUpClick}
>
Get the Community Plan
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
$30/mo. forever!
</span>
</Button>
<Link href="https://github.com/documenso/documenso" onClick={() => event('view-github')}>
<Button variant="outline" className="rounded-full bg-transparent backdrop-blur-sm">
<Github className="mr-2 h-5 w-5" />
Star on Github
{starCount && starCount > 0 && (
<span className="bg-primary -mr-2.5 ml-2.5 rounded-full px-2 py-1.5 text-xs">
{starCount.toLocaleString('en-US')}
</span>
)}
</Button>
</Link>
</motion.div>
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-6">
<motion.div
variants={HeroTitleVariants}
initial="initial"
animate="animate"
className="mt-8 flex flex-col items-center justify-center gap-x-6 gap-y-4"
>
<Link
href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso"
target="_blank"
>
<img
src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily"
alt="Documenso - The open source DocuSign alternative | Product Hunt"
style={{ width: '250px', height: '54px' }}
/>
</Link>
</motion.div>
</div>
<motion.div
className="mt-12"
variants={{
initial: {
scale: 0.2,
opacity: 0,
},
animate: {
scale: 1,
opacity: 1,
transition: {
ease: 'easeInOut',
delay: 0.5,
duration: 0.8,
},
},
}}
initial="initial"
animate="animate"
>
<Widget className="mt-12">
<strong>Documenso Supporter Pledge</strong>
<p className="w-full max-w-[70ch]">
Our mission is to create an open signing infrastructure that empowers the world,
enabling businesses to embrace openness, cooperation, and transparency. We believe
that signing, as a fundamental act, should embody these values. By offering an
open-source signing solution, we aim to make document signing accessible, transparent,
and trustworthy.
</p>
<p className="w-full max-w-[70ch]">
Through our platform, called Documenso, we strive to earn your trust by allowing
self-hosting and providing complete visibility into its inner workings. We value
inclusivity and foster an environment where diverse perspectives and contributions are
welcomed, even though we may not implement them all.
</p>
<p className="w-full max-w-[70ch]">
At Documenso, we envision a web-enabled future for business and contracts, and we are
committed to being the leading provider of open signing infrastructure. By combining
exceptional product design with open-source principles, we aim to deliver a robust and
well-designed application that exceeds your expectations.
</p>
<p className="w-full max-w-[70ch]">
We understand that exceptional products are born from exceptional communities, and we
invite you to join our open-source community. Your contributions, whether technical or
non-technical, will help shape the future of signing. Together, we can create a better
future for everyone.
</p>
<p className="w-full max-w-[70ch]">
Today we invite you to join us on this journey: By signing this mission statement you
signal your support of Documenso's mission{' '}
<span className="bg-primary text-black">
(in a non-legally binding, but heartfelt way)
</span>{' '}
and lock in the early supporter plan for forever, including everything we build this
year.
</p>
<div className="flex h-24 items-center">
<p className={cn('text-5xl [font-family:var(--font-caveat)]')}>Timur & Lucas</p>
</div>
<div>
<strong>Timur Ercan & Lucas Smith</strong>
<p className="mt-1">Co-Founders, Documenso</p>
</div>
</Widget>
</motion.div>
</div>
</motion.div>
);
};

View File

@@ -1,74 +0,0 @@
import { HTMLAttributes } from 'react';
import Image from 'next/image';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import backgroundPattern from '~/assets/background-pattern.png';
import cardBuildFigure from '~/assets/card-build-figure.png';
import cardOpenFigure from '~/assets/card-open-figure.png';
import cardTemplateFigure from '~/assets/card-template-figure.png';
export type OpenBuildTemplateBentoProps = HTMLAttributes<HTMLDivElement>;
export const OpenBuildTemplateBento = ({ className, ...props }: OpenBuildTemplateBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}
alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
/>
</div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
Truly your own.
<span className="block md:mt-0">Customise and expand.</span>
</h2>
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
<Card className="col-span-2" degrees={45} gradient>
<CardContent className="grid grid-cols-12 gap-8 overflow-hidden p-6 lg:aspect-[2.5/1]">
<p className="col-span-12 leading-relaxed text-[#555E67] lg:col-span-6">
<strong className="block">Open Source or Hosted.</strong>
Its up to you. Either clone our repository or rely on our easy to use hosting
solution.
</p>
<div className="col-span-12 -my-6 -mr-6 flex items-end justify-end pt-12 lg:col-span-6">
<Image src={cardOpenFigure} alt="its fast" className="max-w-[80%] lg:max-w-full" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Build on top.</strong>
Make it your own through advanced customization and adjustability.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardBuildFigure} alt="its fast" className="w-full max-w-xs" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Template Store (Soon).</strong>
Choose a template from the community app store. Or submit your own template for others
to use.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardTemplateFigure} alt="its fast" className="w-full max-w-sm" />
</div>
</CardContent>
</Card>
</div>
</div>
);
};

View File

@@ -1,33 +0,0 @@
'use client';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useCopyToClipboard } from '~/hooks/use-copy-to-clipboard';
export type PasswordRevealProps = {
password: string;
};
export const PasswordReveal = ({ password }: PasswordRevealProps) => {
const { toast } = useToast();
const [, copy] = useCopyToClipboard();
const onCopyClick = () => {
void copy(password).then(() => {
toast({
title: 'Copied to clipboard',
description: 'Your password has been copied to your clipboard.',
});
});
};
return (
<button
type="button"
className="px-2 blur-sm hover:opacity-50 hover:blur-none"
onClick={onCopyClick}
>
{password}
</button>
);
};

View File

@@ -1,180 +0,0 @@
'use client';
import { HTMLAttributes, useMemo, useState } from 'react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { AnimatePresence, motion } from 'framer-motion';
import { usePlausible } from 'next-plausible';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { ClaimPlanDialog } from './claim-plan-dialog';
export type PricingTableProps = HTMLAttributes<HTMLDivElement>;
const SELECTED_PLAN_BAR_LAYOUT_ID = 'selected-plan-bar';
export const PricingTable = ({ className, ...props }: PricingTableProps) => {
const params = useSearchParams();
const event = usePlausible();
const [period, setPeriod] = useState<'MONTHLY' | 'YEARLY'>(() =>
// eslint-disable-next-line turbo/no-undeclared-env-vars
params?.get('planId') === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID
? 'YEARLY'
: 'MONTHLY',
);
const planId = useMemo(() => {
if (period === 'MONTHLY') {
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
}
// eslint-disable-next-line turbo/no-undeclared-env-vars
return process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID;
}, [period]);
return (
<div className={cn('', className)} {...props}>
<div className="flex items-center justify-center gap-x-6">
<AnimatePresence>
<motion.button
key="MONTHLY"
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', {
'text-slate-900': period === 'MONTHLY',
'hover:text-slate-900/80': period !== 'MONTHLY',
})}
onClick={() => setPeriod('MONTHLY')}
>
Monthly
{period === 'MONTHLY' && (
<motion.div
layoutId={SELECTED_PLAN_BAR_LAYOUT_ID}
className="bg-primary absolute bottom-0 left-0 h-[3px] w-full rounded-full"
/>
)}
</motion.button>
<motion.button
key="YEARLY"
className={cn('relative flex items-center gap-x-2.5 px-1 py-2.5 text-[#727272]', {
'text-slate-900': period === 'YEARLY',
'hover:text-slate-900/80': period !== 'YEARLY',
})}
onClick={() => setPeriod('YEARLY')}
>
Yearly
<div className="block rounded-full bg-slate-200 px-2 py-0.5 text-xs text-slate-700">
Save $60
</div>
{period === 'YEARLY' && (
<motion.div
layoutId={SELECTED_PLAN_BAR_LAYOUT_ID}
className="bg-primary absolute bottom-0 left-0 h-[3px] w-full rounded-full"
/>
)}
</motion.button>
</AnimatePresence>
</div>
<div className="mt-12 grid grid-cols-1 gap-x-6 gap-y-12 md:grid-cols-2 lg:grid-cols-3">
<div
data-plan="self-hosted"
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5"
>
<p className="text-4xl font-medium text-slate-900">Self Hosted</p>
<p className="text-primary mt-2.5 text-xl font-medium">Free</p>
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
For small teams and individuals who need a simple solution
</p>
<Button className="mt-6 rounded-full text-base">
<Link
href="https://github.com/documenso/documenso"
target="_blank"
onClick={() => event('view-github')}
>
View on Github
</Link>
</Button>
<div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Host your own instance</p>
<p className="py-4 text-slate-900">Full Control</p>
<p className="py-4 text-slate-900">Customizability</p>
<p className="py-4 text-slate-900">Docker Ready</p>
<p className="py-4 text-slate-900">Community Support</p>
<p className="py-4 text-slate-900">Free, Forever</p>
</div>
</div>
<div
data-plan="community"
className="border-primary flex flex-col items-center justify-center rounded-lg border-2 bg-white px-8 py-12 shadow-[0px_0px_0px_4px_#E3E3E380] shadow-slate-900/5"
>
<p className="text-4xl font-medium text-slate-900">Community</p>
<div className="text-primary mt-2.5 text-xl font-medium">
<AnimatePresence mode="wait">
{period === 'MONTHLY' && <motion.div layoutId="pricing">$30</motion.div>}
{period === 'YEARLY' && <motion.div layoutId="pricing">$300</motion.div>}
</AnimatePresence>
</div>
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
For fast-growing companies that aim to scale across multiple teams.
</p>
<ClaimPlanDialog planId={planId}>
<Button className="mt-6 rounded-full text-base">Signup Now</Button>
</ClaimPlanDialog>
<div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Documenso Early Adopter Deal:</p>
<p className="py-4 text-slate-900">Join the movement</p>
<p className="py-4 text-slate-900">Simple signing solution</p>
<p className="py-4 text-slate-900">Email and Slack assistance</p>
<p className="py-4 text-slate-900">
<strong>Includes all upcoming features</strong>
</p>
<p className="py-4 text-slate-900">Fixed, straightforward pricing</p>
</div>
</div>
<div
data-plan="enterprise"
className="flex flex-col items-center justify-center rounded-lg border bg-white px-8 py-12 shadow-lg shadow-slate-900/5"
>
<p className="text-4xl font-medium text-slate-900">Enterprise</p>
<p className="text-primary mt-2.5 text-xl font-medium">Pricing on request</p>
<p className="mt-4 max-w-[30ch] text-center text-slate-900">
For large organizations that need extra flexibility and control.
</p>
<Link
href="https://dub.sh/enterprise"
target="_blank"
className="mt-6"
onClick={() => event('enterprise-contact')}
>
<Button className="rounded-full text-base">Contact Us</Button>
</Link>
<div className="mt-8 flex w-full flex-col divide-y">
<p className="py-4 font-medium text-slate-900">Everything in Community, plus:</p>
<p className="py-4 text-slate-900">Custom Subdomain</p>
<p className="py-4 text-slate-900">Compliance Check</p>
<p className="py-4 text-slate-900">Guaranteed Uptime</p>
<p className="py-4 text-slate-900">Reporting & Analysis</p>
<p className="py-4 text-slate-900">24/7 Support</p>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,91 +0,0 @@
import { HTMLAttributes } from 'react';
import Image from 'next/image';
import { cn } from '@documenso/ui/lib/utils';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import backgroundPattern from '~/assets/background-pattern.png';
import cardConnectionsFigure from '~/assets/card-connections-figure.png';
import cardPaidFigure from '~/assets/card-paid-figure.png';
import cardSharingFigure from '~/assets/card-sharing-figure.png';
import cardWidgetFigure from '~/assets/card-widget-figure.png';
export type ShareConnectPaidWidgetBentoProps = HTMLAttributes<HTMLDivElement>;
export const ShareConnectPaidWidgetBento = ({
className,
...props
}: ShareConnectPaidWidgetBentoProps) => {
return (
<div className={cn('relative', className)} {...props}>
<div className="absolute inset-0 -z-10 flex items-center justify-center">
<Image
src={backgroundPattern}
alt="background pattern"
className="h-full scale-125 object-cover md:scale-150 lg:scale-[175%]"
/>
</div>
<h2 className="px-0 text-[22px] font-semibold md:px-12 md:text-4xl lg:px-24">
Integrates with all your favourite tools.
<span className="block md:mt-0">Send, connect, receive and embed everywhere.</span>
</h2>
<div className="mt-6 grid grid-cols-2 gap-8 md:mt-8">
<Card className="col-span-2 lg:col-span-1" degrees={120} gradient>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Easy Sharing (Soon).</strong>
Receive your personal link to share with everyone you care about.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardSharingFigure} alt="its fast" className="w-full max-w-xs" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Connections (Soon).</strong>
Create connections and automations with Zapier and more to integrate with your
favorite tools.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardConnectionsFigure} alt="its fast" className="w-full max-w-sm" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">Get paid (Soon).</strong>
Integrated payments with stripe so you dont have to worry about getting paid.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardPaidFigure} alt="its fast" className="w-full max-w-[14rem]" />
</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1" spotlight>
<CardContent className="grid grid-cols-1 gap-8 p-6">
<p className="leading-relaxed text-[#555E67]">
<strong className="block">React Widget (Soon).</strong>
Easily embed Documenso into your product. Simply copy and paste our react widget into
your application.
</p>
<div className="flex items-center justify-center p-8">
<Image src={cardWidgetFigure} alt="its fast" className="w-full max-w-xs" />
</div>
</CardContent>
</Card>
</div>
</div>
);
};

View File

@@ -1,402 +0,0 @@
'use client';
import { HTMLAttributes, KeyboardEvent, useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
import { Loader } from 'lucide-react';
import { usePlausible } from 'next-plausible';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { claimPlan } from '~/api/claim-plan/fetcher';
import { FormErrorMessage } from '../form/form-error-message';
const ZWidgetFormSchema = z
.object({
email: z.string().email({ message: 'Please enter a valid email address.' }),
name: z.string().min(3, { message: 'Please enter a valid name.' }),
})
.and(
z.union([
z.object({
signatureDataUrl: z.string().min(1),
signatureText: z.null().or(z.string().max(0)),
}),
z.object({
signatureDataUrl: z.null().or(z.string().max(0)),
signatureText: z.string().min(1),
}),
]),
);
export type TWidgetFormSchema = z.infer<typeof ZWidgetFormSchema>;
export type WidgetProps = HTMLAttributes<HTMLDivElement>;
export const Widget = ({ className, children, ...props }: WidgetProps) => {
const { toast } = useToast();
const event = usePlausible();
const [step, setStep] = useState<'EMAIL' | 'NAME' | 'SIGN'>('EMAIL');
const [showSigningDialog, setShowSigningDialog] = useState(false);
const [draftSignatureDataUrl, setDraftSignatureDataUrl] = useState<string | null>(null);
const {
control,
register,
handleSubmit,
setValue,
trigger,
watch,
formState: { errors, isSubmitting, isValid },
} = useForm<TWidgetFormSchema>({
mode: 'onChange',
defaultValues: {
email: '',
name: '',
signatureDataUrl: null,
signatureText: '',
},
resolver: zodResolver(ZWidgetFormSchema),
});
const signatureDataUrl = watch('signatureDataUrl');
const signatureText = watch('signatureText');
const stepsRemaining = useMemo(() => {
if (step === 'NAME') {
return 2;
}
if (step === 'SIGN') {
return 1;
}
return 3;
}, [step]);
const onNextStepClick = () => {
if (step === 'EMAIL') {
setStep('NAME');
setTimeout(() => {
document.querySelector<HTMLElement>('#name')?.focus();
}, 0);
}
if (step === 'NAME') {
setStep('SIGN');
setTimeout(() => {
document.querySelector<HTMLElement>('#signatureText')?.focus();
}, 0);
}
};
const onEnterPress = (callback: () => void) => {
return (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
callback();
}
};
};
const onSignatureConfirmClick = () => {
setValue('signatureDataUrl', draftSignatureDataUrl);
setValue('signatureText', '');
void trigger('signatureDataUrl');
setShowSigningDialog(false);
};
const onFormSubmit = async ({
email,
name,
signatureDataUrl,
signatureText,
}: TWidgetFormSchema) => {
try {
const delay = new Promise<void>((resolve) => {
setTimeout(resolve, 1000);
});
// eslint-disable-next-line turbo/no-undeclared-env-vars
const planId = process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID;
const claimPlanInput = signatureDataUrl
? {
name,
email,
planId,
signatureDataUrl: signatureDataUrl!,
signatureText: null,
}
: {
name,
email,
planId,
signatureDataUrl: null,
signatureText: signatureText!,
};
const [result] = await Promise.all([claimPlan(claimPlanInput), delay]);
event('claim-plan-widget');
window.location.href = result;
} catch (error) {
event('claim-plan-failed');
toast({
title: 'Something went wrong',
description: error instanceof Error ? error.message : 'Please try again later.',
variant: 'destructive',
});
}
};
return (
<>
<Card
className={cn('mx-auto w-full max-w-4xl rounded-3xl before:rounded-3xl', className)}
gradient
{...props}
>
<div className="grid grid-cols-12 gap-y-8 overflow-hidden p-2 lg:gap-x-8">
<div className="col-span-12 flex flex-col gap-y-4 p-4 text-xs leading-relaxed text-[#727272] lg:col-span-7">
{children}
</div>
<form
className="col-span-12 flex flex-col rounded-2xl bg-[#F7F7F7] p-6 lg:col-span-5"
onSubmit={handleSubmit(onFormSubmit)}
>
<h3 className="text-2xl font-semibold">Sign up for the community plan</h3>
<p className="mt-2 text-xs text-[#AFAFAF]">
with Timur Ercan & Lucas Smith from Documenso
</p>
<hr className="mb-6 mt-4" />
<AnimatePresence>
<motion.div key="email">
<label htmlFor="email" className="text-lg font-semibold text-slate-900 lg:text-xl">
Whats your email?
</label>
<Controller
control={control}
name="email"
render={({ field }) => (
<div className="relative mt-2">
<Input
id="email"
type="email"
placeholder=""
className="w-full bg-white pr-16"
disabled={isSubmitting}
onKeyDown={(e) =>
field.value !== '' &&
!errors.email?.message &&
onEnterPress(onNextStepClick)(e)
}
{...field}
/>
<div className="absolute inset-y-0 right-0 p-1.5">
<Button
type="button"
className="bg-primary h-full w-14 rounded"
disabled={!field.value || !!errors.email?.message}
onClick={() => onNextStepClick()}
>
Next
</Button>
</div>
</div>
)}
/>
<FormErrorMessage error={errors.email} className="mt-1" />
</motion.div>
{(step === 'NAME' || step === 'SIGN') && (
<motion.div
key="name"
className="mt-4"
animate={{
opacity: 1,
transform: 'translateX(0)',
}}
initial={{
opacity: 0,
transform: 'translateX(-25%)',
}}
exit={{
opacity: 0,
transform: 'translateX(25%)',
}}
>
<label htmlFor="name" className="text-lg font-semibold text-slate-900 lg:text-xl">
and your name?
</label>
<Controller
control={control}
name="name"
render={({ field }) => (
<div className="relative mt-2">
<Input
id="name"
type="text"
placeholder=""
className="w-full bg-white pr-16"
disabled={isSubmitting}
onKeyDown={(e) =>
field.value !== '' &&
!errors.name?.message &&
onEnterPress(onNextStepClick)(e)
}
{...field}
/>
<div className="absolute inset-y-0 right-0 p-1.5">
<Button
type="button"
className="bg-primary h-full w-14 rounded"
disabled={!field.value || !!errors.name?.message}
onClick={() => onNextStepClick()}
>
Next
</Button>
</div>
</div>
)}
/>
<FormErrorMessage error={errors.name} className="mt-1" />
</motion.div>
)}
</AnimatePresence>
<div className="mt-12 flex-1" />
<div className="flex items-center justify-between">
<p className="text-xs text-[#AFAFAF]">{stepsRemaining} step(s) until signed</p>
<p className="block text-xs text-[#AFAFAF] md:hidden">Minimise contract</p>
</div>
<div className="relative mt-2.5 h-[2px] w-full bg-[#E9E9E9]">
<div
className={cn('bg-primary/60 absolute inset-y-0 left-0 duration-200', {
'w-1/3': stepsRemaining === 3,
'w-2/3': stepsRemaining === 2,
'w-11/12': stepsRemaining === 1,
})}
/>
</div>
<Card id="signature" className="mt-4" degrees={-140} gradient>
<CardContent
role="button"
className="relative cursor-pointer pt-6"
onClick={() => setShowSigningDialog(true)}
>
<div className="flex h-28 items-center justify-center pb-6">
{!signatureText && signatureDataUrl && (
<img src={signatureDataUrl} alt="user signature" className="h-full" />
)}
{signatureText && (
<p
className={cn(
'text-4xl font-semibold text-slate-900 [font-family:var(--font-caveat)]',
)}
>
{signatureText}
</p>
)}
</div>
<div
className="absolute inset-x-0 bottom-0 flex cursor-auto items-center justify-between px-4 pb-2"
onClick={(e) => e.stopPropagation()}
>
<Input
id="signatureText"
className="border-none p-0 text-sm text-slate-700 placeholder:text-[#D6D6D6] focus-visible:ring-0"
placeholder="Draw or type name here"
disabled={isSubmitting}
{...register('signatureText', {
onChange: (e) => {
if (e.target.value !== '') {
setValue('signatureDataUrl', null);
}
},
})}
/>
<Button
type="submit"
className="h-8 disabled:bg-[#ECEEED] disabled:text-[#C6C6C6] disabled:hover:bg-[#ECEEED]"
disabled={!isValid || isSubmitting}
>
{isSubmitting && <Loader className="mr-2 h-4 w-4 animate-spin" />}
Sign
</Button>
</div>
</CardContent>
</Card>
</form>
</div>
</Card>
<Dialog open={showSigningDialog} onOpenChange={setShowSigningDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add your signature</DialogTitle>
</DialogHeader>
<DialogDescription>
By signing you signal your support of Documenso's mission in a <br></br>
<strong>non-legally binding, but heartfelt way</strong>. <br></br>
<br></br>You also unlock the option to purchase the early supporter plan including
everything we build this year for fixed price.
</DialogDescription>
<SignaturePad
className="aspect-video w-full rounded-md border"
onChange={setDraftSignatureDataUrl}
/>
<DialogFooter>
<Button variant="ghost" onClick={() => setShowSigningDialog(false)}>
Cancel
</Button>
<Button onClick={() => onSignatureConfirmClick()}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View File

@@ -2,16 +2,31 @@
import { HTMLAttributes, useEffect, useState } from 'react';
import { DateTime, DateTimeFormatOptions } from 'luxon';
import { useLocale } from '@documenso/lib/client-only/providers/locale';
export type LocaleDateProps = HTMLAttributes<HTMLSpanElement> & {
date: string | number | Date;
format?: DateTimeFormatOptions;
};
export const LocaleDate = ({ className, date, ...props }: LocaleDateProps) => {
const [localeDate, setLocaleDate] = useState(() => new Date(date).toISOString());
/**
* Formats the date based on the user locale.
*
* Will use the estimated locale from the user headers on SSR, then will use
* the client browser locale once mounted.
*/
export const LocaleDate = ({ className, date, format, ...props }: LocaleDateProps) => {
const { locale } = useLocale();
const [localeDate, setLocaleDate] = useState(() =>
DateTime.fromJSDate(new Date(date)).setLocale(locale).toLocaleString(format),
);
useEffect(() => {
setLocaleDate(new Date(date).toLocaleString());
}, [date]);
setLocaleDate(DateTime.fromJSDate(new Date(date)).toLocaleString(format));
}, [date, format]);
return (
<span className={className} {...props}>

View File

@@ -21,7 +21,7 @@ import { FormErrorMessage } from '../form/form-error-message';
export const ZProfileFormSchema = z.object({
name: z.string().min(1),
signature: z.string().min(1),
signature: z.string().min(1, 'Signature Pad cannot be empty'),
});
export type TProfileFormSchema = z.infer<typeof ZProfileFormSchema>;
@@ -44,7 +44,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
} = useForm<TProfileFormSchema>({
values: {
name: user.name ?? '',
signature: '',
signature: user.signature || '',
},
resolver: zodResolver(ZProfileFormSchema),
});
@@ -118,10 +118,12 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
render={({ field: { onChange } }) => (
<SignaturePad
className="h-44 w-full rounded-lg border bg-white backdrop-blur-sm dark:border-[#e2d7c5] dark:bg-[#fcf8ee]"
defaultValue={user.signature ?? undefined}
onChange={(v) => onChange(v ?? '')}
/>
)}
/>
<FormErrorMessage className="mt-1.5" error={errors.signature} />
</div>
</div>

View File

@@ -1,5 +1,9 @@
'use client';
import { useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { Loader } from 'lucide-react';
import { signIn } from 'next-auth/react';
@@ -7,12 +11,22 @@ import { useForm } from 'react-hook-form';
import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod';
import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
const ERROR_MESSAGES = {
[ErrorCode.CREDENTIALS_NOT_FOUND]: 'The email or password provided is incorrect',
[ErrorCode.INCORRECT_EMAIL_PASSWORD]: 'The email or password provided is incorrect',
[ErrorCode.USER_MISSING_PASSWORD]:
'This account appears to be using a social login method, please sign in using that method',
};
const LOGIN_REDIRECT_PATH = '/documents';
export const ZSignInFormSchema = z.object({
email: z.string().email().min(1),
password: z.string().min(6).max(72),
@@ -25,6 +39,8 @@ export type SignInFormProps = {
};
export const SignInForm = ({ className }: SignInFormProps) => {
const searchParams = useSearchParams();
const { toast } = useToast();
const {
@@ -39,17 +55,36 @@ export const SignInForm = ({ className }: SignInFormProps) => {
resolver: zodResolver(ZSignInFormSchema),
});
const errorCode = searchParams?.get('error');
useEffect(() => {
let timeout: NodeJS.Timeout | null = null;
if (isErrorCode(errorCode)) {
timeout = setTimeout(() => {
toast({
variant: 'destructive',
description: ERROR_MESSAGES[errorCode] ?? 'An unknown error occurred',
});
}, 0);
}
return () => {
if (timeout) {
clearTimeout(timeout);
}
};
}, [errorCode, toast]);
const onFormSubmit = async ({ email, password }: TSignInFormSchema) => {
try {
await signIn('credentials', {
email,
password,
callbackUrl: '/documents',
callbackUrl: LOGIN_REDIRECT_PATH,
}).catch((err) => {
console.error(err);
});
// throw new Error('Not implemented');
} catch (err) {
toast({
title: 'An unknown error occurred',
@@ -61,8 +96,7 @@ export const SignInForm = ({ className }: SignInFormProps) => {
const onSignInWithGoogleClick = async () => {
try {
await signIn('google', { callbackUrl: '/dashboard' });
// throw new Error('Not implemented');
await signIn('google', { callbackUrl: LOGIN_REDIRECT_PATH });
} catch (err) {
toast({
title: 'An unknown error occurred',

View File

@@ -3,13 +3,14 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Loader } from 'lucide-react';
import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
@@ -19,6 +20,7 @@ export const ZSignUpFormSchema = z.object({
name: z.string().min(1),
email: z.string().email().min(1),
password: z.string().min(6).max(72),
signature: z.string().min(1, { message: 'We need your signature to sign documents' }),
});
export type TSignUpFormSchema = z.infer<typeof ZSignUpFormSchema>;
@@ -31,6 +33,7 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
const { toast } = useToast();
const {
control,
register,
handleSubmit,
formState: { errors, isSubmitting },
@@ -39,15 +42,16 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
name: '',
email: '',
password: '',
signature: '',
},
resolver: zodResolver(ZSignUpFormSchema),
});
const { mutateAsync: signup } = trpc.auth.signup.useMutation();
const onFormSubmit = async ({ name, email, password }: TSignUpFormSchema) => {
const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
try {
await signup({ name, email, password });
await signup({ name, email, password, signature });
await signIn('credentials', {
email,
@@ -119,8 +123,19 @@ export const SignUpForm = ({ className }: SignUpFormProps) => {
</Label>
<div>
<SignaturePad className="mt-2 h-36 w-full rounded-lg border bg-white dark:border-[#e2d7c5] dark:bg-[#fcf8ee]" />
<Controller
control={control}
name="signature"
render={({ field: { onChange } }) => (
<SignaturePad
className="mt-2 h-36 w-full rounded-lg border bg-white dark:border-[#e2d7c5] dark:bg-[#fcf8ee]"
onChange={(v) => onChange(v ?? '')}
/>
)}
/>
</div>
<FormErrorMessage className="mt-1.5" error={errors.signature} />
</div>
<Button size="lg" disabled={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90">

View File

@@ -2,7 +2,7 @@ import { z } from 'zod';
import { LOCAL_FEATURE_FLAGS, isFeatureFlagEnabled } from '@documenso/lib/constants/feature-flags';
import { TFeatureFlagValue, ZFeatureFlagValueSchema } from '~/providers/feature-flag';
import { TFeatureFlagValue, ZFeatureFlagValueSchema } from '~/providers/feature-flag.types';
/**
* Evaluate whether a flag is enabled for the current user.

View File

@@ -1,25 +1,23 @@
import { NextRequest, NextResponse } from 'next/server';
export default function middleware(req: NextRequest) {
import { getToken } from 'next-auth/jwt';
export default async function middleware(req: NextRequest) {
if (req.nextUrl.pathname === '/') {
const redirectUrl = new URL('/documents', req.url);
return NextResponse.redirect(redirectUrl);
}
// if (req.nextUrl.pathname.startsWith('/dashboard')) {
// const token = await getToken({ req });
if (req.nextUrl.pathname.startsWith('/signin')) {
const token = await getToken({ req });
// console.log('token', token);
if (token) {
const redirectUrl = new URL('/documents', req.url);
// if (!token) {
// console.log('has no token', req.url);
// const redirectUrl = new URL('/signin', req.url);
// redirectUrl.searchParams.set('callbackUrl', req.url);
// return NextResponse.redirect(redirectUrl);
// }
// }
return NextResponse.redirect(redirectUrl);
}
}
return NextResponse.next();
}

View File

@@ -43,7 +43,6 @@ export default async function handler(
if (user && user.Subscription.length > 0) {
return res.status(200).json({
// eslint-disable-next-line turbo/no-undeclared-env-vars
redirectUrl: `${process.env.NEXT_PUBLIC_APP_URL}/login`,
});
}
@@ -104,7 +103,6 @@ export default async function handler(
mode: 'subscription',
metadata,
allow_promotion_codes: true,
// eslint-disable-next-line turbo/no-undeclared-env-vars
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/claimed?sessionId={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing?email=${encodeURIComponent(
email,

View File

@@ -1,7 +1,6 @@
import { NextApiRequest, NextApiResponse } from 'next';
import formidable from 'formidable';
import { type File } from 'formidable';
import formidable, { type File } from 'formidable';
import { readFileSync } from 'fs';
import { getServerSession } from '@documenso/lib/next-auth/get-server-session';

View File

@@ -17,14 +17,13 @@ import {
SigningStatus,
} from '@documenso/prisma/client';
const log = (...args: any[]) => console.log('[stripe]', ...args);
const log = (...args: unknown[]) => console.log('[stripe]', ...args);
export const config = {
api: { bodyParser: false },
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// eslint-disable-next-line turbo/no-undeclared-env-vars
// if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
// return res.status(500).json({
// success: false,
@@ -55,6 +54,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
log('event-type:', event.type);
if (event.type === 'checkout.session.completed') {
// This is required since we don't want to create a guard for every event type
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const session = event.data.object as Stripe.Checkout.Session;
if (session.metadata?.source === 'landing') {

View File

@@ -2,8 +2,6 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { z } from 'zod';
import {
FEATURE_FLAG_POLL_INTERVAL,
LOCAL_FEATURE_FLAGS,
@@ -12,14 +10,7 @@ import {
import { getAllFlags } from '~/helpers/get-feature-flag';
export const ZFeatureFlagValueSchema = z.union([
z.boolean(),
z.string(),
z.number(),
z.undefined(),
]);
export type TFeatureFlagValue = z.infer<typeof ZFeatureFlagValueSchema>;
import { TFeatureFlagValue } from './feature-flag.types';
export type FeatureFlagContextValue = {
getFlag: (_key: string) => TFeatureFlagValue;

View File

@@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZFeatureFlagValueSchema = z.union([
z.boolean(),
z.string(),
z.number(),
z.undefined(),
]);
export type TFeatureFlagValue = z.infer<typeof ZFeatureFlagValueSchema>;

613
package-lock.json generated
View File

@@ -1,10 +1,10 @@
{
"name": "documenso.next",
"name": "@documenso/root",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "documenso.next",
"name": "@documenso/root",
"workspaces": [
"apps/*",
"packages/*"
@@ -17,7 +17,7 @@
"@commitlint/config-conventional": "^17.7.0",
"dotenv": "^16.0.3",
"dotenv-cli": "^7.2.1",
"eslint": "^7.32.0",
"eslint": "^8.40.0",
"eslint-config-custom": "*",
"husky": "^8.0.0",
"lint-staged": "^14.0.0",
@@ -68,6 +68,7 @@
"version": "0.1.0",
"license": "AGPL-3.0",
"dependencies": {
"@documenso/ee": "*",
"@documenso/lib": "*",
"@documenso/prisma": "*",
"@documenso/tailwind-config": "*",
@@ -135,6 +136,7 @@
"version": "7.12.11",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
"integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
"dev": true,
"dependencies": {
"@babel/highlight": "^7.10.4"
}
@@ -1075,6 +1077,10 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@documenso/ee": {
"resolved": "packages/ee",
"link": true
},
"node_modules/@documenso/email": {
"resolved": "packages/email",
"link": true
@@ -1555,36 +1561,47 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
"integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
"integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.1.1",
"espree": "^7.3.0",
"globals": "^13.9.0",
"ignore": "^4.0.6",
"debug": "^4.3.2",
"espree": "^9.6.0",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^3.13.1",
"minimatch": "^3.0.4",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^10.12.0 || >=12.0.0"
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"engines": {
"node": ">= 4"
"node_modules/@eslint/eslintrc/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@eslint/js": {
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz",
"integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==",
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz",
"integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
@@ -1667,13 +1684,13 @@
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
"integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.0",
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
"minimatch": "^3.0.4"
"minimatch": "^3.0.5"
},
"engines": {
"node": ">=10.10.0"
@@ -4850,6 +4867,9 @@
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
@@ -5120,14 +5140,6 @@
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
"integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag=="
},
"node_modules/astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/astring": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz",
@@ -6809,6 +6821,29 @@
"resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz",
"integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ=="
},
"node_modules/disparity": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/disparity/-/disparity-3.2.0.tgz",
"integrity": "sha512-8cl9ouncFYE7OQsYwJNiy2e15S0xN80X1Jj/N/YkoiM+VGWSyg1YzPToecKyYx2DQiJapt5IC8yi43GW23TUHQ==",
"dependencies": {
"ansi-styles": "^4.2.1",
"diff": "^4.0.2"
},
"bin": {
"disparity": "bin/disparity"
},
"engines": {
"node": ">=8"
}
},
"node_modules/disparity/node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@@ -7011,6 +7046,9 @@
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
"integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"ansi-colors": "^4.1.1",
"strip-ansi": "^6.0.1"
@@ -7184,56 +7222,53 @@
}
},
"node_modules/eslint": {
"version": "7.32.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
"integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
"version": "8.48.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz",
"integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==",
"dependencies": {
"@babel/code-frame": "7.12.11",
"@eslint/eslintrc": "^0.4.3",
"@humanwhocodes/config-array": "^0.5.0",
"ajv": "^6.10.0",
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.2",
"@eslint/js": "8.48.0",
"@humanwhocodes/config-array": "^0.11.10",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.0.1",
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"enquirer": "^2.3.5",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^5.1.1",
"eslint-utils": "^2.1.0",
"eslint-visitor-keys": "^2.0.0",
"espree": "^7.3.1",
"esquery": "^1.4.0",
"eslint-scope": "^7.2.2",
"eslint-visitor-keys": "^3.4.3",
"espree": "^9.6.1",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"functional-red-black-tree": "^1.0.1",
"glob-parent": "^5.1.2",
"globals": "^13.6.0",
"ignore": "^4.0.6",
"import-fresh": "^3.0.0",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
"globals": "^13.19.0",
"graphemer": "^1.4.0",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"js-yaml": "^3.13.1",
"is-path-inside": "^3.0.3",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.1",
"progress": "^2.0.0",
"regexpp": "^3.1.0",
"semver": "^7.2.1",
"strip-ansi": "^6.0.0",
"strip-json-comments": "^3.1.0",
"table": "^6.0.9",
"text-table": "^0.2.0",
"v8-compile-cache": "^2.0.3"
"optionator": "^0.9.3",
"strip-ansi": "^6.0.1",
"text-table": "^0.2.0"
},
"bin": {
"eslint": "bin/eslint.js"
},
"engines": {
"node": "^10.12.0 || >=12.0.0"
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -7593,9 +7628,9 @@
}
},
"node_modules/eslint-plugin-import": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz",
"integrity": "sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q==",
"version": "2.28.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz",
"integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==",
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.findlastindex": "^1.2.2",
@@ -7606,13 +7641,12 @@
"eslint-import-resolver-node": "^0.3.7",
"eslint-module-utils": "^2.8.0",
"has": "^1.0.3",
"is-core-module": "^2.12.1",
"is-core-module": "^2.13.0",
"is-glob": "^4.0.3",
"minimatch": "^3.1.2",
"object.fromentries": "^2.0.6",
"object.groupby": "^1.0.0",
"object.values": "^1.1.6",
"resolve": "^1.22.3",
"semver": "^6.3.1",
"tsconfig-paths": "^3.14.2"
},
@@ -7642,22 +7676,6 @@
"node": ">=0.10.0"
}
},
"node_modules/eslint-plugin-import/node_modules/resolve": {
"version": "1.22.3",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz",
"integrity": "sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw==",
"dependencies": {
"is-core-module": "^2.12.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/eslint-plugin-import/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -7703,6 +7721,22 @@
"semver": "bin/semver.js"
}
},
"node_modules/eslint-plugin-package-json": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-package-json/-/eslint-plugin-package-json-0.1.4.tgz",
"integrity": "sha512-qdb9LUBFR3tM9OZM1AaYCkxoZnRx7PX2xqvLm49D0JbUu+EkbDur9ug+tCS2xlA1Lbt12Wff5qES81ttc/VBdg==",
"dependencies": {
"disparity": "^3.0.0",
"package-json-validator": "^0.6.3",
"requireindex": "^1.2.0"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependencies": {
"eslint": ">=4.7.0"
}
},
"node_modules/eslint-plugin-prettier": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
@@ -7836,32 +7870,10 @@
"node": ">=4.0"
}
},
"node_modules/eslint-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
"integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
"dependencies": {
"eslint-visitor-keys": "^1.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
}
},
"node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/eslint-visitor-keys": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz",
"integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==",
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -7869,41 +7881,73 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"node_modules/eslint/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/eslint/node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
"integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": ">=10"
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"node_modules/eslint/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">= 4"
"node": ">=10.13.0"
}
},
"node_modules/eslint/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/espree": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
"integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dependencies": {
"acorn": "^7.4.0",
"acorn-jsx": "^5.3.1",
"eslint-visitor-keys": "^1.3.0"
"acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^10.12.0 || >=12.0.0"
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/espree/node_modules/eslint-visitor-keys": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
"integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
"node_modules/espree/node_modules/acorn": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=4"
"node": ">=0.4.0"
}
},
"node_modules/esprima": {
@@ -8455,11 +8499,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/functional-red-black-tree": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
"integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g=="
},
"node_modules/functions-have-names": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
@@ -9347,9 +9386,9 @@
}
},
"node_modules/is-core-module": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
"integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
"integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
"dependencies": {
"has": "^1.0.3"
},
@@ -10200,11 +10239,6 @@
"integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==",
"dev": true
},
"node_modules/lodash.truncate": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
"integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="
},
"node_modules/lodash.uniq": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -12117,6 +12151,20 @@
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==",
"dependencies": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
}
},
"node_modules/optimist/node_modules/minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw=="
},
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -12170,6 +12218,17 @@
"node": ">=6"
}
},
"node_modules/package-json-validator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/package-json-validator/-/package-json-validator-0.6.3.tgz",
"integrity": "sha512-juKiFboV4UKUvWQ+OSxstnyukhuluyuEoFmgZw1Rx21XzmwlgDWLcbl3qzjA3789IRORYhVFs7cmAO0YFGwHCg==",
"dependencies": {
"optimist": "~0.6.0"
},
"bin": {
"pjv": "bin/pjv"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -12866,14 +12925,6 @@
"node": ">=16.13"
}
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -13572,17 +13623,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
}
},
"node_modules/rehype-stringify": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-9.0.3.tgz",
@@ -13713,10 +13753,19 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/requireindex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
"integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
"engines": {
"node": ">=0.10.5"
}
},
"node_modules/resolve": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
@@ -14261,22 +14310,6 @@
"node": ">=8"
}
},
"node_modules/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -14723,41 +14756,6 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/table": {
"version": "6.8.1",
"resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
"integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
"dependencies": {
"ajv": "^8.0.1",
"lodash.truncate": "^4.4.2",
"slice-ansi": "^4.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/table/node_modules/ajv": {
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/table/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/tailwind-merge": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz",
@@ -16019,11 +16017,6 @@
"node": ">=8"
}
},
"node_modules/v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA=="
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
@@ -16205,6 +16198,14 @@
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -16317,6 +16318,15 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"packages/ee": {
"name": "@documenso/ee",
"version": "1.0.0",
"license": "COMMERCIAL",
"dependencies": {
"@documenso/lib": "*",
"@documenso/prisma": "*"
}
},
"packages/email": {
"name": "@documenso/email",
"version": "1.0.0",
@@ -16344,168 +16354,12 @@
"eslint-config-next": "13.4.12",
"eslint-config-prettier": "^8.8.0",
"eslint-config-turbo": "^1.9.3",
"eslint-plugin-package-json": "^0.1.4",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"typescript": "^5.1.6"
}
},
"packages/eslint-config/node_modules/@eslint/eslintrc": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz",
"integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.6.0",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"packages/eslint-config/node_modules/@humanwhocodes/config-array": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
"integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
"minimatch": "^3.0.5"
},
"engines": {
"node": ">=10.10.0"
}
},
"packages/eslint-config/node_modules/acorn": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"packages/eslint-config/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"packages/eslint-config/node_modules/eslint": {
"version": "8.46.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz",
"integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.1",
"@eslint/js": "^8.46.0",
"@humanwhocodes/config-array": "^0.11.10",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.2.2",
"eslint-visitor-keys": "^3.4.2",
"espree": "^9.6.1",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
"globals": "^13.19.0",
"graphemer": "^1.4.0",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3",
"strip-ansi": "^6.0.1",
"text-table": "^0.2.0"
},
"bin": {
"eslint": "bin/eslint.js"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"packages/eslint-config/node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
"integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"packages/eslint-config/node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dependencies": {
"acorn": "^8.9.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"packages/eslint-config/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"packages/eslint-config/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"packages/lib": {
"name": "@documenso/lib",
"version": "1.0.0",
@@ -16563,7 +16417,6 @@
"license": "MIT",
"dependencies": {
"autoprefixer": "^10.4.13",
"eslint": "7.32.0",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.7",
"tailwindcss-animate": "^1.0.5"

View File

@@ -2,7 +2,7 @@
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/{web,marketing}",
"dev": "turbo run dev --filter=@documenso/web --filter=@documenso/marketing",
"start": "cd apps && cd web && next start",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,mdx}\"",
@@ -18,7 +18,7 @@
"@commitlint/config-conventional": "^17.7.0",
"dotenv": "^16.0.3",
"dotenv-cli": "^7.2.1",
"eslint": "^7.32.0",
"eslint": "^8.40.0",
"eslint-config-custom": "*",
"husky": "^8.0.0",
"lint-staged": "^14.0.0",

40
packages/ee/LICENSE Normal file
View File

@@ -0,0 +1,40 @@
The Documenso Commercial License (the “Commercial License”)
Copyright (c) 2023 Documenso, Inc
With regard to the Documenso Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, an agreement governing
the use of the Software, as mutually agreed by you and Documenso, Inc ("Documenso"),
and otherwise have a valid Documenso Enterprise Edition subscription ("Commercial Subscription").
Subject to the foregoing sentence, you are free to modify this Software and publish patches to the Software.
You agree that Documenso and/or its licensors (as applicable) retain all right, title and interest in
and to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Commercial Subscription for the correct number of hosts.
Notwithstanding the foregoing, you may copy and modify the Software for development
and testing purposes, without requiring a subscription. You agree that Documenso and/or
its licensors (as applicable) retain all right, title and interest in and to all such
modifications. You are not granted any other rights beyond what is expressly stated herein.
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.
This Commercial License applies only to the part of this Software that is not distributed under
the AGPLv3 license. Any part of this Software distributed under the MIT license or which
is served client-side as an image, font, cascading stylesheet (CSS), file which produces
or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or
in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall
be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
For all third party components incorporated into the Documenso Software, those
components are licensed under the original license provided by the owner of the
applicable component.

1
packages/ee/index.ts Normal file
View File

@@ -0,0 +1 @@
export {};

17
packages/ee/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "@documenso/ee",
"version": "1.0.0",
"main": "./index.ts",
"types": "./index.ts",
"license": "COMMERCIAL",
"files": [
"client-only/",
"server-only/",
"universal/"
],
"scripts": {},
"dependencies": {
"@documenso/lib": "*",
"@documenso/prisma": "*"
}
}

View File

@@ -0,0 +1,31 @@
import { stripe } from '@documenso/lib/server-only/stripe';
import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id';
import { prisma } from '@documenso/prisma';
import { User } from '@documenso/prisma/client';
export type CreateCustomerOptions = {
user: User;
};
export const createCustomer = async ({ user }: CreateCustomerOptions) => {
const existingSubscription = await getSubscriptionByUserId({ userId: user.id });
if (existingSubscription) {
throw new Error('User already has a subscription');
}
const customer = await stripe.customers.create({
name: user.name ?? undefined,
email: user.email,
metadata: {
userId: user.id,
},
});
return await prisma.subscription.create({
data: {
userId: user.id,
customerId: customer.id,
},
});
};

View File

@@ -0,0 +1,19 @@
'use server';
import { stripe } from '@documenso/lib/server-only/stripe';
export type GetPortalSessionOptions = {
customerId: string;
returnUrl: string;
};
export const getPortalSession = async ({ customerId, returnUrl }: GetPortalSessionOptions) => {
'use server';
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: returnUrl,
});
return session.url;
};

View File

@@ -0,0 +1,5 @@
{
"extends": "@documenso/tsconfig/react-library.json",
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"],
"exclude": ["dist", "build", "node_modules"]
}

View File

@@ -6,9 +6,10 @@ module.exports = {
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'plugin:package-json/recommended',
],
plugins: ['prettier'],
plugins: ['prettier', 'package-json'],
env: {
node: true,
@@ -34,6 +35,20 @@ module.exports = {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-duplicate-imports': 'error',
'no-multi-spaces': [
'error',
{
ignoreEOLComments: false,
exceptions: {
BinaryExpression: false,
VariableDeclarator: false,
ImportDeclaration: false,
Property: false,
},
},
],
// Safety with promises so we aren't running with scissors
'no-promise-executor-return': 'error',
'prefer-promise-reject-errors': 'error',

View File

@@ -9,9 +9,10 @@
"eslint": "^8.40.0",
"eslint-config-next": "13.4.12",
"eslint-config-prettier": "^8.8.0",
"eslint-config-turbo": "^1.9.3",
"eslint-plugin-package-json": "^0.1.4",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-config-turbo": "^1.9.3",
"typescript": "^5.1.6"
}
}

View File

@@ -0,0 +1,37 @@
'use client';
import { createContext, useContext } from 'react';
export type LocaleContextValue = {
locale: string;
};
export const LocaleContext = createContext<LocaleContextValue | null>(null);
export const useLocale = () => {
const context = useContext(LocaleContext);
if (!context) {
throw new Error('useLocale must be used within a LocaleProvider');
}
return context;
};
export function LocaleProvider({
children,
locale,
}: {
children: React.ReactNode;
locale: string;
}) {
return (
<LocaleContext.Provider
value={{
locale: locale,
}}
>
{children}
</LocaleContext.Provider>
);
}

View File

@@ -1,6 +0,0 @@
export const initials = (text: string) =>
text
?.split(' ')
.map((name: string) => name.slice(0, 1).toUpperCase())
.slice(0, 2)
.join('') ?? 'UK';

View File

@@ -7,7 +7,7 @@ import GoogleProvider, { GoogleProfile } from 'next-auth/providers/google';
import { prisma } from '@documenso/prisma';
import { getUserByEmail } from '../server-only/user/get-user-by-email';
import { ErrorCodes } from './error-codes';
import { ErrorCode } from './error-codes';
export const NEXT_AUTH_OPTIONS: AuthOptions = {
adapter: PrismaAdapter(prisma),
@@ -24,23 +24,23 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
},
authorize: async (credentials, _req) => {
if (!credentials) {
throw new Error(ErrorCodes.CredentialsNotFound);
throw new Error(ErrorCode.CREDENTIALS_NOT_FOUND);
}
const { email, password } = credentials;
const user = await getUserByEmail({ email }).catch(() => {
throw new Error(ErrorCodes.IncorrectEmailPassword);
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
});
if (!user.password) {
throw new Error(ErrorCodes.UserMissingPassword);
throw new Error(ErrorCode.USER_MISSING_PASSWORD);
}
const isPasswordsSame = await compare(password, user.password);
if (!isPasswordsSame) {
throw new Error(ErrorCodes.IncorrectEmailPassword);
throw new Error(ErrorCode.INCORRECT_EMAIL_PASSWORD);
}
return {

View File

@@ -1,5 +1,11 @@
export const ErrorCodes = {
IncorrectEmailPassword: 'incorrect-email-password',
UserMissingPassword: 'missing-password',
CredentialsNotFound: 'credentials-not-found',
export const isErrorCode = (code: unknown): code is ErrorCode => {
return typeof code === 'string' && code in ErrorCode;
};
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
export const ErrorCode = {
INCORRECT_EMAIL_PASSWORD: 'INCORRECT_EMAIL_PASSWORD',
USER_MISSING_PASSWORD: 'USER_MISSING_PASSWORD',
CREDENTIALS_NOT_FOUND: 'CREDENTIALS_NOT_FOUND',
} as const;

View File

@@ -0,0 +1,5 @@
import { Role, User } from '@documenso/prisma/client';
const isAdmin = (user: User) => user.roles.includes(Role.ADMIN);
export { isAdmin };

View File

@@ -0,0 +1,26 @@
import { prisma } from '@documenso/prisma';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
export const getDocumentStats = async () => {
const counts = await prisma.document.groupBy({
by: ['status'],
_count: {
_all: true,
},
});
const stats: Record<Exclude<ExtendedDocumentStatus, 'INBOX'>, number> = {
[ExtendedDocumentStatus.DRAFT]: 0,
[ExtendedDocumentStatus.PENDING]: 0,
[ExtendedDocumentStatus.COMPLETED]: 0,
[ExtendedDocumentStatus.ALL]: 0,
};
counts.forEach((stat) => {
stats[stat.status] = stat._count._all;
stats.ALL += stat._count._all;
});
return stats;
};

View File

@@ -0,0 +1,29 @@
import { prisma } from '@documenso/prisma';
import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client';
export const getRecipientsStats = async () => {
const results = await prisma.recipient.groupBy({
by: ['readStatus', 'signingStatus', 'sendStatus'],
_count: true,
});
const stats = {
TOTAL_RECIPIENTS: 0,
[ReadStatus.OPENED]: 0,
[ReadStatus.NOT_OPENED]: 0,
[SigningStatus.SIGNED]: 0,
[SigningStatus.NOT_SIGNED]: 0,
[SendStatus.SENT]: 0,
[SendStatus.NOT_SENT]: 0,
};
results.forEach((result) => {
const { readStatus, signingStatus, sendStatus, _count } = result;
stats[readStatus] += _count;
stats[signingStatus] += _count;
stats[sendStatus] += _count;
stats.TOTAL_RECIPIENTS += _count;
});
return stats;
};

View File

@@ -0,0 +1,18 @@
import { prisma } from '@documenso/prisma';
import { SubscriptionStatus } from '@documenso/prisma/client';
export const getUsersCount = async () => {
return await prisma.user.count();
};
export const getUsersWithSubscriptionsCount = async () => {
return await prisma.user.count({
where: {
Subscription: {
some: {
status: SubscriptionStatus.ACTIVE,
},
},
},
});
};

View File

@@ -83,10 +83,7 @@ export const completeDocumentWithToken = async ({
},
});
console.log('documents', documents);
if (documents.count > 0) {
console.log('sealing document');
await sealDocument({ documentId: document.id });
}
};

View File

@@ -53,10 +53,6 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => {
const doc = await PDFDocument.load(pdfData);
for (const field of fields) {
console.log('inserting field', {
...field,
Signature: null,
});
await insertFieldInPDF(doc, field);
}

View File

@@ -84,6 +84,8 @@ export const setFieldsForDocument = async ({
})
: prisma.field.create({
data: {
// TODO: Rewrite this entire transaction because this is a mess
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
type: field.type!,
page: field.pageNumber,
positionX: field.pageX,

View File

@@ -35,15 +35,6 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const fieldX = pageWidth * (Number(field.positionX) / 100);
const fieldY = pageHeight * (Number(field.positionY) / 100);
console.log({
fieldWidth,
fieldHeight,
fieldX,
fieldY,
pageWidth,
pageHeight,
});
const font = await pdf.embedFont(isSignatureField ? fontCaveat : StandardFonts.Helvetica);
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
@@ -75,15 +66,6 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
// Invert the Y axis since PDFs use a bottom-left coordinate system
imageY = pageHeight - imageY - imageHeight;
console.log({
initialDimensions,
scalingFactor,
imageWidth,
imageHeight,
imageX,
imageY,
});
page.drawImage(image, {
x: imageX,
y: imageY,
@@ -107,17 +89,6 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const textX = fieldX + (fieldWidth - textWidth) / 2;
let textY = fieldY + (fieldHeight - textHeight) / 2;
console.log({
initialDimensions,
scalingFactor,
textWidth,
textHeight,
textX,
textY,
pageWidth,
pageHeight,
});
// Invert the Y axis since PDFs use a bottom-left coordinate system
textY = pageHeight - textY - textHeight;

View File

@@ -1,7 +1,6 @@
import Stripe from 'stripe';
// eslint-disable-next-line turbo/no-undeclared-env-vars
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY!, {
export const stripe = new Stripe(process.env.NEXT_PRIVATE_STRIPE_API_KEY ?? '', {
apiVersion: '2022-11-15',
typescript: true,
});

View File

@@ -0,0 +1,15 @@
'use server';
import { prisma } from '@documenso/prisma';
export type GetSubscriptionByUserIdOptions = {
userId: number;
};
export const getSubscriptionByUserId = ({ userId }: GetSubscriptionByUserIdOptions) => {
return prisma.subscription.findFirst({
where: {
userId,
},
});
};

View File

@@ -9,9 +9,10 @@ export interface CreateUserOptions {
name: string;
email: string;
password: string;
signature?: string | null;
}
export const createUser = async ({ name, email, password }: CreateUserOptions) => {
export const createUser = async ({ name, email, password, signature }: CreateUserOptions) => {
const hashedPassword = await hash(password, SALT_ROUNDS);
const userExists = await prisma.user.findFirst({
@@ -29,6 +30,7 @@ export const createUser = async ({ name, email, password }: CreateUserOptions) =
name,
email: email.toLowerCase(),
password: hashedPassword,
signature,
identityProvider: IdentityProvider.DOCUMENSO,
},
});

View File

@@ -19,11 +19,13 @@ export const updatePassword = async ({ userId, password }: UpdatePasswordOptions
const hashedPassword = await hash(password, SALT_ROUNDS);
// Compare the new password with the old password
const isSamePassword = await compare(password, user.password as string);
if (user.password) {
// Compare the new password with the old password
const isSamePassword = await compare(password, user.password);
if (isSamePassword) {
throw new Error('Your new password cannot be the same as your old password.');
if (isSamePassword) {
throw new Error('Your new password cannot be the same as your old password.');
}
}
const updatedUser = await prisma.user.update({

View File

@@ -6,12 +6,7 @@ export type UpdateProfileOptions = {
signature: string;
};
export const updateProfile = async ({
userId,
name,
// TODO: Actually use signature
signature: _signature,
}: UpdateProfileOptions) => {
export const updateProfile = async ({ userId, name, signature }: UpdateProfileOptions) => {
// Existence check
await prisma.user.findFirstOrThrow({
where: {
@@ -25,7 +20,7 @@ export const updateProfile = async ({
},
data: {
name,
// signature,
signature,
},
});

View File

@@ -1,5 +1,5 @@
export type FindResultSet<T> = {
data: T extends Array<any> ? T : T[];
data: T extends Array<unknown> ? T : T[];
count: number;
currentPage: number;
perPage: number;

View File

@@ -1,5 +1,6 @@
import { DocumentStatus } from '@documenso/prisma/client';
export const isDocumentStatus = (value: unknown): value is DocumentStatus => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return Object.values(DocumentStatus).includes(value as DocumentStatus);
};

View File

@@ -0,0 +1,12 @@
import { Recipient } from '@documenso/prisma/client';
export const recipientInitials = (text: string) =>
text
.split(' ')
.map((name: string) => name.slice(0, 1).toUpperCase())
.slice(0, 2)
.join('');
export const recipientAbbreviation = (recipient: Recipient) => {
return recipientInitials(recipient.name) || recipient.email.slice(0, 1).toUpperCase();
};

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "signature" TEXT;

View File

@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER');
-- AlterTable
ALTER TABLE "User" ADD COLUMN "roles" "Role"[] DEFAULT ARRAY['USER']::"Role"[];

View File

@@ -13,6 +13,11 @@ enum IdentityProvider {
GOOGLE
}
enum Role {
ADMIN
USER
}
model User {
id Int @id @default(autoincrement())
name String?
@@ -20,6 +25,8 @@ model User {
emailVerified DateTime?
password String?
source String?
signature String?
roles Role[] @default([USER])
identityProvider IdentityProvider @default(DOCUMENSO)
accounts Account[]
sessions Session[]

Some files were not shown because too many files have changed in this diff Show More