Compare commits

...

90 Commits

Author SHA1 Message Date
Timur Ercan
51c63715d6 unused imports 2023-04-04 21:15:52 +02:00
Timur Ercan
a9ad586035 Merge pull request #32 from litaesther10/DOC-190-Dashboard-Metrics
Updated Dashboard Metrics
2023-04-04 17:54:30 +02:00
Timur Ercan
f3b68772a6 Merge branch 'main' into DOC-190-Dashboard-Metrics 2023-04-04 17:51:47 +02:00
Timur Ercan
2d0fb2879d doc-135 2023-04-04 17:46:23 +02:00
Timur Ercan
2291744580 typo 2023-04-04 14:22:25 +02:00
Timur Ercan
0c3305b11d setup STMP from .env 2023-04-04 14:20:36 +02:00
Timur Ercan
4031faec46 seed example pdf 2023-04-04 13:45:24 +02:00
Timur Ercan
a9cd5e0d94 Any PDFs hint 2023-04-04 13:19:46 +02:00
Timur Ercan
24e044097c example pdf 2023-04-04 12:31:28 +02:00
Timur Ercan
2bdfb884ec added database init script 2023-04-04 12:02:09 +02:00
Timur Ercan
ff85c294b2 empty state label consistency 2023-04-04 11:38:28 +02:00
Esther Lizardo
67328b4504 Merge branch 'main' into DOC-190-Dashboard-Metrics 2023-03-28 16:06:37 +02:00
litaesther10
900ec32697 Updated Dashboard Metrics 2023-03-28 16:04:22 +02:00
Timur Ercan
0344ac324c Merge pull request #31 from dephraiim/doc-82
Make dialog into a component
2023-03-28 15:58:48 +02:00
Ephraim Atta-Duncan
b3e89b16bc Add types to Dialog component 2023-03-28 12:55:20 +00:00
Ephraim Atta-Duncan
a9befd342c Use dynamic values for title and icon for dialog 2023-03-28 12:47:40 +00:00
Ephraim Atta-Duncan
16f6da01c0 Move dialog into a seperate component 2023-03-28 12:42:00 +00:00
Ephraim Atta-Duncan
f8f941a9cd add inital component to @documenso/ui 2023-03-28 12:15:45 +00:00
Timur Ercan
e3059cfb34 add recipient to add fields hint 2023-03-27 13:07:35 +02:00
Timur Ercan
e79a622ddd block non pdf upload 2023-03-27 12:58:17 +02:00
Timur Ercan
9945cfb2c7 change upload to add 2023-03-27 12:53:03 +02:00
Timur Ercan
f32c3d999a background fixes firefox 2023-03-27 12:47:16 +02:00
Timur Ercan
4bb5064477 allow only pdf upload (clientside) 2023-03-26 20:07:58 +02:00
Timur Ercan
c655cb52ad Merge pull request #30 from documenso/doc-184
bugfix click on signing on mobile
2023-03-26 20:05:06 +02:00
Timur Ercan
3d0d7d1245 bugfix click on signing on mobile 2023-03-26 20:03:46 +02:00
Timur Ercan
f96bf757e2 Merge pull request #28 from litaesther10/DOC-186-Page-Margins
Added margins and borders
2023-03-23 16:22:18 +01:00
Timur Ercan
3f897abffa Merge branch 'main' into DOC-186-Page-Margins 2023-03-23 13:00:22 +01:00
Timur Ercan
df238e2be3 Merge pull request #29 from dephraiim/active-nav-bug
fix settings active bug
2023-03-23 12:59:46 +01:00
litaesther10
2b83e28e6d md screens margins and items center 2023-03-23 12:42:29 +01:00
Ephraim Atta-Duncan
6f31dacd74 fix settings active bug 2023-03-22 18:44:03 +00:00
litaesther10
d4324538cc Inline items, left aligned 2023-03-22 17:12:15 +01:00
Esther Lizardo
2f2b708bfe Merge branch 'main' into DOC-186-Page-Margins 2023-03-22 11:13:44 +01:00
litaesther10
7656d4259e Added margins and borders 2023-03-22 11:10:01 +01:00
Timur Ercan
d509a6178f Merge pull request #27 from dephraiim/doc-187
Close panel on when user clicks on a nav link
2023-03-22 11:05:46 +01:00
Ephraim Atta-Duncan
2fed1a7034 Close nav on profile button click 2023-03-22 09:45:15 +00:00
Ephraim Atta-Duncan
de3c500fea Close panel on when user clicks on a nav link 2023-03-21 23:13:56 +00:00
Timur Ercan
dc0c78f270 Merge pull request #25 from dephraiim/email-null-bug
Custom message if name is defined or null in email template
2023-03-21 20:03:14 +01:00
Ephraim Atta-Duncan
a3e17e9f3e Custom message if name is defined or null 2023-03-21 18:44:45 +00:00
Timur Ercan
7d79e10587 Merge pull request #24 from dephraiim/settings-navbar-bug
Route to Profile on Settings
2023-03-21 19:16:07 +01:00
Ephraim Atta-Duncan
1a37998f39 Merge branch 'main' into settings-navbar-bug 2023-03-21 18:12:49 +00:00
Timur Ercan
2d69783ca1 Merge pull request #21 from litaesther10/DOC-185-buttons-alignment
Optimising for mobile
2023-03-21 19:11:11 +01:00
Ephraim Atta-Duncan
b7cc4aed9b Route to profile on settings click on navbar 2023-03-21 17:53:47 +00:00
litaesther10
3dfa8fc597 Fixed broken css 2023-03-21 18:11:55 +01:00
Esther Lizardo
d5d3b17623 Merge branch 'main' into DOC-185-buttons-alignment 2023-03-21 15:24:43 +01:00
litaesther10
4710176f78 Optimising for mobile 2023-03-21 15:03:59 +01:00
Timur Ercan
f22d4ebeab Merge pull request #20 from documenso/doc-162
Doc 162
2023-03-21 14:31:04 +01:00
Timur Ercan
d32b9871db revert regression 2023-03-21 14:30:40 +01:00
Timur Ercan
0e9aa4ab62 Merge branch 'main' into doc-162 2023-03-21 14:16:47 +01:00
Timur Ercan
9e536e95b6 default allow signup 2023-03-21 09:21:57 +01:00
Timur Ercan
a57e0b2b57 coming soon hint 2023-03-20 15:45:57 +01:00
Timur Ercan
a1736afc62 Merge pull request #15 from documenso/doc-182
Doc 182
2023-03-20 15:15:02 +01:00
Timur Ercan
91b206e3d7 add token to download link to allow download without user 2023-03-20 15:12:51 +01:00
Timur Ercan
d37dd000af get document without relying on logged in user 2023-03-20 15:12:13 +01:00
Timur Ercan
7d6bd00a22 allow adding field via recipient token for signing 2023-03-20 15:11:20 +01:00
Timur Ercan
8505b9cd10 Merge pull request #14 from documenso/doc-167
Doc 167
2023-03-19 15:07:24 +01:00
Timur Ercan
dd67e1a6f0 qoc 2023-03-19 15:06:01 +01:00
Timur Ercan
9009506bb6 explicit true criteria 2023-03-19 15:05:33 +01:00
Timur Ercan
025e6a4eb1 feature flag for signup 2023-03-19 14:59:10 +01:00
Timur Ercan
72914c49c4 build fix 2023-03-19 14:20:28 +01:00
Timur Ercan
7d0c91e565 Merge pull request #13 from documenso/doc-130
Doc 130
2023-03-19 14:07:57 +01:00
Timur Ercan
36526119b2 bugfix 401 redirect 2023-03-19 14:05:32 +01:00
Timur Ercan
156b7a69e7 bugfix doc-130 document does not render because there is no user and the request is still valid 2023-03-19 14:05:20 +01:00
Timur Ercan
4eb4759381 401 redirect fix 2023-03-19 13:59:32 +01:00
Timur Ercan
1bfac711ac build fix 2023-03-19 12:43:10 +01:00
Timur Ercan
cb2d77c609 doc-171 2023-03-19 12:31:54 +01:00
Timur Ercan
253e5cfcfa show all fields while signing, date field design 2023-03-19 12:13:13 +01:00
Timur Ercan
ff16972646 hide free_signature field in editor 2023-03-19 11:54:26 +01:00
Timur Ercan
3ba6afabfc cleanup debug 2023-03-19 11:53:36 +01:00
Timur Ercan
3961402c70 comment 2023-03-19 11:48:32 +01:00
Timur Ercan
7fc228a562 Merge branch 'development' 2023-03-19 11:34:19 +01:00
Timur Ercan
899dd205f2 Merge branch 'main' of https://github.com/documenso/documenso 2023-03-19 11:34:14 +01:00
Timur Ercan
4a915134e4 Update CONTRIBUTING.md 2023-03-19 11:31:12 +01:00
Timur Ercan
266ecf0f8d bugfix racecondition in adding field to ui in parallel 2023-03-19 11:17:04 +01:00
Timur Ercan
071398273a bugfix multiple fields added after field type change: removed "drag drop" feeling handlers 2023-03-19 11:08:15 +01:00
Timur Ercan
6419d22155 Merge branch 'development' into doc-162 2023-03-19 10:52:57 +01:00
Timur Ercan
738c798dbd block editing completed documents 2023-03-19 10:52:01 +01:00
Timur Ercan
cd5f6fde32 remove debug statement 2023-03-19 10:28:15 +01:00
Timur Ercan
93654ccae5 check for already inserted fields 2023-03-19 10:23:55 +01:00
Timur Ercan
7c830d3607 bugfix don't show half created recipient 2023-03-19 10:21:19 +01:00
Timur Ercan
559432cc15 update package log 2023-03-17 19:43:25 +01:00
Timur Ercan
2400c34c71 update package lock 2023-03-17 15:37:46 +01:00
Timur Ercan
ff977c6bff nextauth security update 2023-03-17 15:35:54 +01:00
Timur Ercan
526be3b906 security update for next-auth github.com/documenso/documenso/security/dependabot/4 2023-03-17 15:31:37 +01:00
Timur Ercan
9f700ad0b2 cosmetics 2023-03-17 15:26:41 +01:00
Timur Ercan
f1dc5687d7 remove unused dep, move signature canvas types to deps 2023-03-17 15:24:37 +01:00
Timur Ercan
793902ae54 docs 2023-03-17 15:08:06 +01:00
Timur Ercan
f77f101e67 unused deps 2023-03-17 15:05:38 +01:00
Timur Ercan
49f36a103b move website to documenso org repo 2023-03-17 15:04:32 +01:00
Timur Ercan
6c7ee3edf4 Update README.md
badge fix, remove gitmoji
2023-03-17 13:43:42 +01:00
Timur Ercan
c819ed3cfb Update README.md
📑
2023-03-15 15:50:00 +01:00
38 changed files with 1853 additions and 1340 deletions

View File

@@ -1,7 +1,7 @@
# Database # Database
# You use the provided remote test database, courtesy of the documenso team: postgres://documenso_test_user:GnmLG14u12sd9zHsd4vVWwP40WneFJMo@dpg-cf2hljh4reb5o45oqpq0-a.oregon-postgres.render.com/documenso_test_e2i3 # Option 1: You can use the provided remote test database, courtesy of the documenso team: postgres://documenso_test_user:GnmLG14u12sd9zHsd4vVWwP40WneFJMo@dpg-cf2hljh4reb5o45oqpq0-a.oregon-postgres.render.com/documenso_test_e2i3
# It is however recommend, that you set up a local Postgres SQL instance # Option 2: Set up a local Postgres SQL instance (RECOMMENDED)
# ⚠ WARNING: The test database can be resetted or taken offline at any point # ⚠ WARNING: The test database can be resetted or taken offline at any point.
# ⚠ WARNING: Please be aware that nothing written to the test databae is private. # ⚠ WARNING: Please be aware that nothing written to the test databae is private.
DATABASE_URL='' DATABASE_URL=''
@@ -13,9 +13,21 @@ NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
NEXTAUTH_SECRET='lorem ipsum sit dolor random string for encryption this could literally be anything' NEXTAUTH_SECRET='lorem ipsum sit dolor random string for encryption this could literally be anything'
NEXTAUTH_URL='http://localhost:3000' NEXTAUTH_URL='http://localhost:3000'
# MAIL # MAIL (NODEMAILER)
# SENDGRID
# Get a Sendgrid Api key here: https://signup.sendgrid.com # Get a Sendgrid Api key here: https://signup.sendgrid.com
# You can also configure you own SMTP server using Nodemailer in sendMailts. (currently not possible via config)
SENDGRID_API_KEY='' SENDGRID_API_KEY=''
# SMTP
# Set SMTP credentials to use SMTP instead of the Sendgrid API.
SMTP_MAIL_HOST=''
SMTP_MAIL_PORT=''
SMTP_MAIL_USER=''
SMTP_MAIL_PASSWORD=''
# Sender for signing requests and completion mails. # Sender for signing requests and completion mails.
MAIL_FROM='' MAIL_FROM='documenso@localhost.com'
#FEATURE FLAGS
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
ALLOW_SIGNUP=true

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "apps/website/documenso/website"] [submodule "apps/website/documenso/website"]
path = apps/website/documenso/website path = apps/website/documenso/website
url = http://github.com/eltimuro/website.git url = http://github.com/documenso/website.git

View File

@@ -1,31 +1,37 @@
# Contributing to Documenso # Contributing to Documenso
If you plan to contribute to Documenso, please take a moment to feel awesome ✨ People like you are what open source is about ♥. Any contributions, no matter how big or small, are highly appreciated. If you plan to contribute to Documenso, please take a moment to feel awesome ✨ People like you are what open source is about ♥. Any contributions, no matter how big or small, are highly appreciated.
## Before getting started ## Before getting started
- Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission. - Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission.
- Select and issue from [here](https://github.com/documenso/documenso/issues) or create a new one - Select and issue from [here](https://github.com/documenso/documenso/issues) or create a new one
- Consider the results from the discussion in the issue - Consider the results from the discussion in the issue
## Developing ## Developing
The development branch is <code>development</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w).
The development branch is <code>main</code>. All pull request should be made against this branch. If you need help getting started, [join us on Slack](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w).
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
own GitHub account and then own GitHub account and then
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Create a new branch: 2. Create a new branch:
- Create a new branch (include the issue id and somthing readable): - Create a new branch (include the issue id and somthing readable):
```sh ```sh
git checkout -b doc-999-my-feature-or-fix git checkout -b doc-999-my-feature-or-fix
``` ```
3. See the [Developer Setup](https://github.com/documenso/documenso/blob/main/README.md#developer-setup) for more setup details. 3. See the [Developer Setup](https://github.com/documenso/documenso/blob/main/README.md#developer-setup) for more setup details.
## Building
## Building
> **Note**
> Please be sure that you can make a full production build before pushing code or creating PRs.
You can build the project with: You can build the project with:
```bash ```bash
npm run build npm run build
``` ```
> **Note**
> Please be sure that you can make a full production build before pushing code or creating PRs.

View File

@@ -25,7 +25,7 @@
<a href="https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w"><img src="https://img.shields.io/badge/Slack-documenso.slack.com-%234A154B" alt="Join Documenso on Slack"></a> <a href="https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w"><img src="https://img.shields.io/badge/Slack-documenso.slack.com-%234A154B" alt="Join Documenso on Slack"></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/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/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
<a href="https://github.com/documenso/documensom/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></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> </p>
# Documenso 0.9 - Developer Preview # Documenso 0.9 - Developer Preview
@@ -57,6 +57,7 @@
Signing documents digitally is fast, easy and should be best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure. Signing documents digitally is fast, easy and should be best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure.
## Community and Next Steps 🎯 ## 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: 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:
- Check out the first source code release in this repository and test it - Check out the first source code release in this repository and test it
@@ -67,13 +68,11 @@ The current project goal is to <b>[release a production ready version](https://g
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release - Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
## Contributing ## Contributing
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md). - To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
## Tools ## Tools
- This repos uses 📝 https://gitmoji.dev/ for more expressive commit messages.
- Use 🧹 for quality of code (eg remove comments, debug output, remove unused code)
# Tech # Tech
Documenso is built using awesome open source tech including: Documenso is built using awesome open source tech including:
@@ -105,32 +104,37 @@ To run Documenso locally you need
Follow these steps to setup documenso on you local machnine: Follow these steps to setup documenso on you local machnine:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device. - [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh ```sh
git clone https://github.com/documenso/documenso git clone https://github.com/documenso/documenso
``` ```
- Run <code>npm i</code> in root directory - Run <code>npm i</code> in root directory
- Rename .env.example to .env - Rename <code>.env.example</code> to <code>.env</code>
- Set DATABASE_URL value in .env file - Set DATABASE_URL value in .env file
- You can use the provided test database url (may be wiped at any point) - You can use the provided test database url (may be wiped at any point)
- Or setup a local postgres sql instance (recommened) - Or setup a local postgres sql instance (recommened)
- Set SENDGRID_API_KEY value in .env file - Create the database scheme by running <code>db-migrate:dev</code>
- You need SendGrid account, which you can create [here](https://signup.sendgrid.com/). - Setup your mail provider
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own smtp server - Set <code>SENDGRID_API_KEY</code> 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\_\* varibles</code> in your .env
- Run <code>npm run dev</code> root directory to start - Run <code>npm run dev</code> root directory to start
- Register a new user at http://localhost:3000/signup - 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\ressources\example.pdf</code> manually to test your setup
- Optional: Create your own signing certificate - Optional: Create your own signing certificate
- A demo certificate is provided in /app/web/ressources/certificate.p12 - A demo certificate is provided in /app/web/ressources/certificate.p12
- To generate you own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signging certificate**. - To generate you own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signging certificate**.
## Updating ## Updating
- If you pull the newest version from main, using <code>git pull</code>, it may be neccessary to regenerate your database client - If you pull the newest version from main, using <code>git pull</code>, it may be neccessary to regenerate your database client
- You can do this by running the generate command in /packages/prisma: - You can do this by running the generate command in /packages/prisma:
```sh ```sh
npx prisma generate npx prisma generate
``` ```
- This is not neccessary on first clone - This is not neccessary on first clone
# Creating your own signging certificate # Creating your own signging certificate
@@ -144,7 +148,7 @@ For the digital signature of you documents you need a signign certificate in .p1
<code>openssl req -new -x509 -key private.key -out certificate.crt -days 365</code> \ <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. 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: \ 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> <code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code>
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**) 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/ressource/certificate.p12</code> 5. Place the certificate <code>/apps/web/ressource/certificate.p12</code>

View File

@@ -25,9 +25,6 @@ export default function FieldTypeSelector(props: any) {
onChange={(e: any) => { onChange={(e: any) => {
setSelectedFieldType(e); setSelectedFieldType(e);
}} }}
onMouseDown={(e: any) => {
if (e.button === 0) props.setAdding(true);
}}
> >
<div className="space-y-4"> <div className="space-y-4">
{fieldTypes.map((fieldType) => ( {fieldTypes.map((fieldType) => (

View File

@@ -6,6 +6,8 @@ import { createOrUpdateField, deleteField } from "@documenso/lib/api";
import { createField } from "@documenso/features/editor"; import { createField } from "@documenso/features/editor";
import RecipientSelector from "./recipient-selector"; import RecipientSelector from "./recipient-selector";
import FieldTypeSelector from "./field-type-selector"; import FieldTypeSelector from "./field-type-selector";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
const stc = require("string-to-color"); const stc = require("string-to-color");
const PDFViewer = dynamic(() => import("./pdf-viewer"), { const PDFViewer = dynamic(() => import("./pdf-viewer"), {
@@ -17,8 +19,9 @@ export default function PDFEditor(props: any) {
const [fields, setFields] = useState<any[]>(props.document.Field); const [fields, setFields] = useState<any[]>(props.document.Field);
const [selectedRecipient, setSelectedRecipient]: any = useState(); const [selectedRecipient, setSelectedRecipient]: any = useState();
const [selectedFieldType, setSelectedFieldType] = useState(); const [selectedFieldType, setSelectedFieldType] = useState();
const noRecipients = props?.document.Recipient.length === 0; const noRecipients =
const [adding, setAdding] = useState(false); props?.document.Recipient.length === 0 ||
props?.document.Recipient.every((e: any) => !e.email);
function onPositionChangedHandler(position: any, id: any) { function onPositionChangedHandler(position: any, id: any) {
if (!position) return; if (!position) return;
@@ -47,9 +50,41 @@ export default function PDFEditor(props: any) {
return ( return (
<> <>
<div> <div>
<div hidden={!noRecipients} className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-yellow-400"
aria-hidden="true"
/>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-yellow-700">
This document does not have any recipients. Add recipients to
create fields.
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<Link
href={
NEXT_PUBLIC_WEBAPP_URL +
"/documents/" +
props.document.id +
"/recipients"
}
className="whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600"
>
Add Recipients
<span aria-hidden="true"> &rarr;</span>
</Link>
</p>
</div>
</div>
</div>
<PDFViewer <PDFViewer
style={{ style={{
cursor: `url("https://place-hold.it/110x64/37f095/FFFFFF&text=${selectedFieldType}") 55 32, auto`, cursor: !noRecipients
? `url("https://place-hold.it/110x64/37f095/FFFFFF&text=${selectedFieldType}") 55 32, auto`
: "",
}} }}
readonly={false} readonly={false}
document={props.document} document={props.document}
@@ -60,11 +95,6 @@ export default function PDFEditor(props: any) {
onMouseUp={(e: any, page: number) => { onMouseUp={(e: any, page: number) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
console.log(adding);
if (adding) {
addField(e, page);
setAdding(false);
}
}} }}
onMouseDown={(e: any, page: number) => { onMouseDown={(e: any, page: number) => {
if (e.button === 0) addField(e, page); if (e.button === 0) addField(e, page);
@@ -80,7 +110,6 @@ export default function PDFEditor(props: any) {
/> />
<hr className="m-3 border-slate-300"></hr> <hr className="m-3 border-slate-300"></hr>
<FieldTypeSelector <FieldTypeSelector
setAdding={setAdding}
selectedRecipient={selectedRecipient} selectedRecipient={selectedRecipient}
onChange={setSelectedFieldType} onChange={setSelectedFieldType}
/> />
@@ -92,6 +121,7 @@ export default function PDFEditor(props: any) {
function addField(e: any, page: number) { function addField(e: any, page: number) {
if (!selectedRecipient) return; if (!selectedRecipient) return;
if (!selectedFieldType) return; if (!selectedFieldType) return;
if (noRecipients) return;
const signatureField = createField( const signatureField = createField(
e, e,
@@ -101,7 +131,7 @@ export default function PDFEditor(props: any) {
); );
createOrUpdateField(props?.document, signatureField).then((res) => { createOrUpdateField(props?.document, signatureField).then((res) => {
setFields(fields.concat(res)); setFields((prevState) => [...prevState, res]);
}); });
} }
} }

View File

@@ -9,7 +9,6 @@ import {
CheckBadgeIcon, CheckBadgeIcon,
InformationCircleIcon, InformationCircleIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import toast from "react-hot-toast";
import { FieldType } from "@prisma/client"; import { FieldType } from "@prisma/client";
import { import {
createOrUpdateField, createOrUpdateField,
@@ -70,7 +69,7 @@ export default function PDFSigner(props: any) {
); );
const signedField = { ...dialogField }; const signedField = { ...dialogField };
signedField.signature = signature; signedField.signature = signature;
setFields(fields.concat(signedField)); setFields((prevState) => [...prevState, signedField]);
setOpen(false); setOpen(false);
setDialogField(null); setDialogField(null);
} }
@@ -174,8 +173,12 @@ export default function PDFSigner(props: any) {
FieldType.FREE_SIGNATURE FieldType.FREE_SIGNATURE
); );
createOrUpdateField(props.document, freeSignatureField).then((res) => { createOrUpdateField(
setFields(fields.concat(res)); props.document,
freeSignatureField,
recipient.token
).then((res) => {
setFields((prevState) => [...prevState, res]);
setDialogField(res); setDialogField(res);
setOpen(true); setOpen(true);
}); });

View File

@@ -3,6 +3,7 @@ import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
import EditableField from "./editable-field"; import EditableField from "./editable-field";
import SignableField from "./signable-field"; import SignableField from "./signable-field";
import short from "short-uuid"; import short from "short-uuid";
import { FieldType } from "@prisma/client";
export default function PDFViewer(props) { export default function PDFViewer(props) {
const [numPages, setNumPages] = useState(null); const [numPages, setNumPages] = useState(null);
@@ -71,21 +72,25 @@ export default function PDFViewer(props) {
onRenderError={() => setLoading(false)} onRenderError={() => setLoading(false)}
></Page> ></Page>
{props?.fields {props?.fields
.filter((item) => item.page === index) .filter((field) => field.page === index)
.map((item) => .map((field) =>
props.readonly ? ( props.readonly ? (
<SignableField <SignableField
onClick={props.onClick} onClick={props.onClick}
key={item.id} key={field.id}
field={item} field={field}
className="absolute" className="absolute"
onDelete={onDeleteHandler} onDelete={onDeleteHandler}
></SignableField> ></SignableField>
) : ( ) : (
<EditableField <EditableField
hidden={item.Signature || item.inserted} hidden={
key={item.id} field.Signature ||
field={item} field.inserted ||
field.type === FieldType.FREE_SIGNATURE
}
key={field.id}
field={field}
className="absolute" className="absolute"
onPositionChanged={onPositionChangedHandler} onPositionChanged={onPositionChangedHandler}
onDelete={onDeleteHandler} onDelete={onDeleteHandler}

View File

@@ -2,6 +2,7 @@ import React, { useState } from "react";
import Draggable from "react-draggable"; import Draggable from "react-draggable";
import { IconButton } from "@documenso/ui"; import { IconButton } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/20/solid"; import { XCircleIcon } from "@heroicons/react/20/solid";
import { classNames } from "@documenso/lib";
const stc = require("string-to-color"); const stc = require("string-to-color");
type FieldPropsType = { type FieldPropsType = {
@@ -34,22 +35,28 @@ export default function SignableField(props: FieldPropsType) {
defaultPosition={{ x: 0, y: 0 }} defaultPosition={{ x: 0, y: 0 }}
cancel="div" cancel="div"
onMouseDown={(e: any) => { onMouseDown={(e: any) => {
e.preventDefault(); // e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}} }}
> >
<div <div
onClick={() => { onClick={(e: any) => {
if (!field?.signature) props.onClick(props.field); if (!field?.signature) props.onClick(props.field);
}} }}
ref={nodeRef} ref={nodeRef}
className="cursor-pointer opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none hover:brightness-50" className={classNames(
"opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none",
field.type === "SIGNATURE"
? "cursor-pointer hover:brightness-50"
: "cursor-not-allowed"
)}
style={{ style={{
background: stc(props.field.Recipient.email), background: stc(props.field.Recipient.email),
}} }}
> >
<div hidden={field?.signature} className="font-medium my-4"> <div hidden={field?.signature} className="font-medium my-4">
{field.type === "SIGNATURE" ? "SIGN HERE" : ""} {field.type === "SIGNATURE" ? "SIGN HERE" : ""}
{field.type === "DATE" ? <small>Date (filled on sign)</small> : ""}
</div> </div>
<div <div
hidden={!field?.signature} hidden={!field?.signature}

View File

@@ -17,12 +17,11 @@ interface LoginValues {
csrfToken: string; csrfToken: string;
} }
export default function Login() { export default function Login(props: any) {
const router = useRouter(); const router = useRouter();
const methods = useForm<LoginValues>(); const methods = useForm<LoginValues>();
const { register, formState } = methods; const { register, formState } = methods;
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
let callbackUrl = let callbackUrl =
typeof router.query?.callbackUrl === "string" typeof router.query?.callbackUrl === "string"
? router.query.callbackUrl ? router.query.callbackUrl
@@ -117,7 +116,6 @@ export default function Login() {
/> />
</div> </div>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-sm"> <div className="text-sm">
<a href="#" className="font-medium text-neon hover:text-neon"> <a href="#" className="font-medium text-neon hover:text-neon">
@@ -125,7 +123,6 @@ export default function Login() {
</a> </a>
</div> </div>
</div> </div>
<div> <div>
<Button <Button
type="submit" type="submit"
@@ -152,15 +149,27 @@ export default function Login() {
<div className="relative flex justify-center"></div> <div className="relative flex justify-center"></div>
</div> </div>
</div> </div>
<p className="mt-2 text-center text-sm text-gray-600"> {props.allowSignup ? (
Are you new here?{" "} <p className="mt-2 text-center text-sm text-gray-600">
<Link Are you new here?{" "}
href="/signup" <Link
className="font-medium text-neon hover:text-neon" href="/signup"
> className="font-medium text-neon hover:text-neon"
Create a new Account >
</Link> Create a new Account
</p> </Link>
</p>
) : (
<p className="mt-2 text-center text-sm text-gray-600">
Like Documenso{" "}
<Link
href="https://documenso.com"
className="font-medium text-neon hover:text-neon"
>
Hosted Documenso will be availible soon
</Link>
</p>
)}
</form> </form>
</FormProvider> </FormProvider>
</div> </div>

View File

@@ -34,13 +34,18 @@ const navigation = [
}, },
{ {
name: "Settings", name: "Settings",
href: "/settings", href: "/settings/profile",
current: true, current: true,
icon: WrenchIcon, icon: WrenchIcon,
}, },
]; ];
const userNavigation = [ const userNavigation = [
{ name: "Your Profile", href: "/settings/profile", icon: UserCircleIcon }, {
name: "Your Profile",
href: "/settings/profile",
icon: UserCircleIcon,
},
{ {
name: "Sign out", name: "Sign out",
href: "", href: "",
@@ -95,13 +100,15 @@ export default function TopNavigation() {
}, [session]); }, [session]);
navigation.forEach((element) => { navigation.forEach((element) => {
element.current = router.route.endsWith("/" + element.href.split("/")[1]); element.current =
router.route.endsWith("/" + element.href.split("/")[1]) ||
router.route.includes(element.href.split("/")[1]);
}); });
return ( return (
<> <>
<Disclosure as="nav" className="border-b border-gray-200 bg-white"> <Disclosure as="nav" className="border-b border-gray-200 bg-white">
{({ open }) => ( {({ open, close }) => (
<> <>
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 justify-between"> <div className="flex h-16 justify-between">
@@ -151,10 +158,7 @@ export default function TopNavigation() {
<div <div
key={user?.email} key={user?.email}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: avatarFromInitials( __html: avatarFromInitials(user?.name || "" || "", 40),
user?.name || "" || "",
40
),
}} }}
/> />
</Menu.Button> </Menu.Button>
@@ -220,6 +224,9 @@ export default function TopNavigation() {
"block pl-3 pr-4 py-2 border-l-4 text-base font-medium" "block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
)} )}
aria-current={item.current ? "page" : undefined} aria-current={item.current ? "page" : undefined}
onClick={() => {
close();
}}
> >
{item.name} {item.name}
</Link> </Link>
@@ -236,19 +243,21 @@ export default function TopNavigation() {
/> />
</div> </div>
<div className="ml-3"> <div className="ml-3">
<div className="text-base font-medium text-gray-800"> <div className="text-base font-medium text-gray-800">{user?.name || ""}</div>
{user?.name || ""} <div className="text-sm font-medium text-gray-500">{user?.email}</div>
</div>
<div className="text-sm font-medium text-gray-500">
{user?.email}
</div>
</div> </div>
</div> </div>
<div className="mt-3 space-y-1"> <div className="mt-3 space-y-1">
{userNavigation.map((item) => ( {userNavigation.map((item) => (
<Link <Link
key={item.name} key={item.name}
onClick={item.click} onClick={
item.href.includes("/settings/profile")
? () => {
close();
}
: item.click
}
href={item.href} href={item.href}
className="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800" className="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800"
> >

View File

@@ -115,9 +115,13 @@ export default function Setttings() {
</aside> </aside>
<form <form
className="divide-y divide-gray-200 lg:col-span-9" className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
action="#" action="#"
method="POST" method="POST"
hidden={
subNavigation.filter((e) => e.current)[0]?.name !==
subNavigation[0].name
}
> >
{/* Profile section */} {/* Profile section */}
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
@@ -163,13 +167,33 @@ export default function Setttings() {
name="first-name" name="first-name"
id="first-name" id="first-name"
autoComplete="given-name" autoComplete="given-name"
className="mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm" className="mt-1 block w-full rounded-md border disabled:bg-neutral-100 border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
/> />
</div> </div>
</div> </div>
<Button onClick={() => updateUser(user)}>Save</Button> <Button onClick={() => updateUser(user)}>Save</Button>
</div> </div>
</form> </form>
<div
hidden={
subNavigation.filter((e) => e.current)[0]?.name !==
subNavigation[1].name
}
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
>
{/* Passwords section */}
<div className="py-6 px-4 sm:p-6 lg:pb-8">
<div>
<h2 className="text-lg font-medium leading-6 text-gray-900">
Password
</h2>
<p className="mt-1 text-sm text-gray-500">
Forgot your passwort? Email <b>hi@documenso.com</b> to reset
it.
</p>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -30,7 +30,7 @@
"formidable": "^3.2.5", "formidable": "^3.2.5",
"install": "^0.13.0", "install": "^0.13.0",
"next": "13.0.3", "next": "13.0.3",
"next-auth": "^4.18.3", "next-auth": ">=4.20.1",
"next-transpile-modules": "^10.0.0", "next-transpile-modules": "^10.0.0",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"node-signpdf": "^1.5.0", "node-signpdf": "^1.5.0",

View File

@@ -18,10 +18,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
} }
let user = null; let user = null;
let recipient = null;
if (recipientToken) { if (recipientToken) {
// Request from signing page without login // Request from signing page without login
const recipient = await prisma.recipient.findFirst({ recipient = await prisma.recipient.findFirst({
where: { where: {
token: recipientToken?.toString(), token: recipientToken?.toString(),
}, },
@@ -37,7 +37,14 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
if (!user) return res.status(401).end(); if (!user) return res.status(401).end();
const document: PrismaDocument = await getDocument(+documentId, req, res); let document: PrismaDocument | null = null;
if (recipientToken) {
document = await prisma.document.findFirst({
where: { id: recipient?.Document?.id },
});
} else {
document = await getDocument(+documentId, req, res);
}
if (!document) if (!document)
res.status(404).end(`No document with id ${documentId} found.`); res.status(404).end(`No document with id ${documentId} found.`);
@@ -45,16 +52,18 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const signaturesCount = await prisma.signature.count({ const signaturesCount = await prisma.signature.count({
where: { where: {
Field: { Field: {
documentId: document.id, documentId: document?.id,
}, },
}, },
}); });
let signedDocumentAsBase64 = document.document; let signedDocumentAsBase64 = document?.document || "";
// No need to add a signature, if no one signed yet. // No need to add a signature, if no one signed yet.
if (signaturesCount > 0) { if (signaturesCount > 0) {
signedDocumentAsBase64 = await addDigitalSignature(document.document); signedDocumentAsBase64 = await addDigitalSignature(
document?.document || ""
);
} }
const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64"); const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64");
@@ -62,7 +71,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
res.setHeader("Content-Length", buffer.length); res.setHeader("Content-Length", buffer.length);
res.setHeader( res.setHeader(
"Content-Disposition", "Content-Disposition",
`attachment; filename=${document.title}` `attachment; filename=${document?.title}`
); );
return res.status(200).send(buffer); return res.status(200).send(buffer);

View File

@@ -36,8 +36,10 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
} }
async function postHandler(req: NextApiRequest, res: NextApiResponse) { async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res); const { token: recipientToken } = req.query;
const { id: documentId } = req.query; let user = null;
if (!recipientToken) user = await getUserFromToken(req, res);
if (!user && !recipientToken) return res.status(401).end();
const body: { const body: {
id: number; id: number;
type: FieldType; type: FieldType;
@@ -48,18 +50,30 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
customText: string; customText: string;
} = req.body; } = req.body;
if (!user) return; const { id: documentId } = req.query;
if (!documentId) { if (!documentId) {
res.status(400).send("Missing parameter documentId."); return res.status(400).send("Missing parameter documentId.");
return;
} }
const document: PrismaDocument = await getDocument(+documentId, req, res); if (recipientToken) {
const recipient = await prisma.recipient.findFirst({
where: { token: recipientToken?.toString() },
});
// todo entity ownerships checks if (!recipient || recipient?.documentId !== +documentId)
if (document.userId !== user.id) { return res
return res.status(401).send("User does not have access to this document."); .status(401)
.send("Recipient does not have access to this document.");
}
if (user) {
const document: PrismaDocument = await getDocument(+documentId, req, res);
// todo entity ownerships checks
if (document.userId !== user.id) {
return res
.status(401)
.send("User does not have access to this document.");
}
} }
const field = await prisma.field.upsert({ const field = await prisma.field.upsert({

View File

@@ -1,8 +1,4 @@
import { import { defaultHandler, defaultResponder } from "@documenso/lib/server";
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma"; import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { SigningStatus, DocumentStatus } from "@prisma/client"; import { SigningStatus, DocumentStatus } from "@prisma/client";
@@ -12,7 +8,6 @@ import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
import { sendSigningDoneMail } from "@documenso/lib/mail"; import { sendSigningDoneMail } from "@documenso/lib/mail";
async function postHandler(req: NextApiRequest, res: NextApiResponse) { async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const existingUser = await getUserFromToken(req, res);
const { token: recipientToken } = req.query; const { token: recipientToken } = req.query;
const { signatures: signaturesFromBody }: { signatures: any[] } = req.body; const { signatures: signaturesFromBody }: { signatures: any[] } = req.body;
@@ -29,11 +24,19 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
return res.status(401).send("Recipient not found."); return res.status(401).send("Recipient not found.");
} }
const document: PrismaDocument = await getDocument( const document: PrismaDocument = await prisma.document.findFirstOrThrow({
recipient.documentId, where: {
req, id: recipient.documentId,
res },
); include: {
Recipient: {
orderBy: {
id: "asc",
},
},
Field: { include: { Recipient: true, Signature: true } },
},
});
if (!document) res.status(404).end(`No document found.`); if (!document) res.status(404).end(`No document found.`);
@@ -70,6 +73,8 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
}, },
}); });
// Don't check for inserted, because currently no "sign again" scenarios exist and
// this is probably the expected behaviour in unclean states.
const nonSignatureFields = await prisma.field.findMany({ const nonSignatureFields = await prisma.field.findMany({
where: { where: {
documentId: document.id, documentId: document.id,

View File

@@ -1,9 +1,4 @@
import { import { defaultHandler, defaultResponder } from "@documenso/lib/server";
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { Document as PrismaDocument } from "@prisma/client"; import { Document as PrismaDocument } from "@prisma/client";
import { getDocument } from "@documenso/lib/query"; import { getDocument } from "@documenso/lib/query";

View File

@@ -19,6 +19,7 @@ import {
import { getUserFromToken } from "@documenso/lib/server"; import { getUserFromToken } from "@documenso/lib/server";
import { getDocumentsForUserFromToken } from "@documenso/lib/query"; import { getDocumentsForUserFromToken } from "@documenso/lib/query";
import { truncate } from "fs"; import { truncate } from "fs";
import { Tooltip as ReactTooltip } from "react-tooltip";
type FormValues = { type FormValues = {
document: File; document: File;
@@ -58,18 +59,18 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
Dashboard Dashboard
</h1> </h1>
</header> </header>
<dl className="mt-8 grid grid-cols-3 xs:grid-cols-2 gap-5"> <dl className="grid gap-5 mt-8 md:grid-cols-3 ">
{stats.map((item) => ( {stats.map((item) => (
<Link href={item.link} key={item.name}> <Link href={item.link} key={item.name}>
<div className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6 "> <div className="px-4 py-3 overflow-hidden bg-white rounded-lg shadow md:p-6 sm:py-5">
<dt className="truncate text-sm font-medium text-gray-500 "> <dt className="text-sm font-medium text-gray-500 truncate ">
<item.icon <item.icon
className="flex-shrink-0 mr-3 h-6 w-6 inline text-neon" className="flex-shrink-0 inline w-5 h-5 mr-3 text-neon sm:w-6 sm:h-6"
aria-hidden="true" aria-hidden="true"
></item.icon> ></item.icon>
{item.name} {item.name}
</dt> </dt>
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900"> <dd className="mt-1 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
{getStat(item.name, props)} {getStat(item.name, props)}
</dd> </dd>
</div> </div>
@@ -80,6 +81,7 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
<input <input
id="fileUploadHelper" id="fileUploadHelper"
type="file" type="file"
accept="application/pdf"
onChange={(event: any) => { onChange={(event: any) => {
uploadDocument(event); uploadDocument(event);
}} }}
@@ -90,10 +92,10 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
onClick={() => { onClick={() => {
document?.getElementById("fileUploadHelper")?.click(); document?.getElementById("fileUploadHelper")?.click();
}} }}
className="cursor-pointer relative block w-full rounded-lg border-2 border-dashed border-gray-300 p-12 text-center hover:border-neon focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" className="relative block w-full p-12 text-center border-2 border-gray-300 border-dashed rounded-lg cursor-pointer hover:border-neon focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
> >
<svg <svg
className="mx-auto h-12 w-12 text-gray-400" className="w-12 h-12 mx-auto text-gray-400"
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"
viewBox="0 00 20 25" viewBox="0 00 20 25"
@@ -105,11 +107,18 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/> />
</svg> </svg>
<span
<span className="mt-2 block text-sm font-medium text-neon"> id="add_document"
Upload a new PDF document className="mt-2 block text-sm font-medium text-neon"
>
Add a new PDF document.
</span> </span>
</div> </div>
<ReactTooltip
anchorId="add_document"
place="bottom"
content="No preparation needed. Any PDF will do."
/>
</div> </div>
</> </>
); );

View File

@@ -16,10 +16,10 @@ import {
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { uploadDocument } from "@documenso/features"; import { uploadDocument } from "@documenso/features";
import { DocumentStatus } from "@prisma/client"; import { DocumentStatus } from "@prisma/client";
import { Tooltip as ReactTooltip } from "react-tooltip";
import { Button, IconButton, SelectBox } from "@documenso/ui"; import { Button, IconButton, SelectBox } from "@documenso/ui";
import { NextPageContext } from "next"; import { NextPageContext } from "next";
import { deleteDocument, getDocuments } from "@documenso/lib/api"; import { deleteDocument, getDocuments } from "@documenso/lib/api";
import { Tooltip as ReactTooltip } from "react-tooltip";
const DocumentsPage: NextPageWithLayout = (props: any) => { const DocumentsPage: NextPageWithLayout = (props: any) => {
const router = useRouter(); const router = useRouter();
@@ -406,7 +406,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<h3 className="mt-2 text-sm font-medium text-gray-900">No documents</h3> <h3 className="mt-2 text-sm font-medium text-gray-900">No documents</h3>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500">
Get started by creating a new document. Get started by adding a document. Any PDF will do.
</p> </p>
<div className="mt-6"> <div className="mt-6">
<Button <Button
@@ -415,11 +415,12 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
document?.getElementById("fileUploadHelper")?.click(); document?.getElementById("fileUploadHelper")?.click();
}} }}
> >
Upload Document Add Document
</Button> </Button>
<input <input
id="fileUploadHelper" id="fileUploadHelper"
type="file" type="file"
accept="application/pdf"
onChange={(event: any) => { onChange={(event: any) => {
uploadDocument(event); uploadDocument(event);
}} }}
@@ -427,6 +428,11 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
/> />
</div> </div>
</div> </div>
<ReactTooltip
anchorId="empty"
place="bottom"
content="No preparation needed. Any PDF will do."
/>
</> </>
); );
}; };

View File

@@ -1,5 +1,5 @@
import Head from "next/head"; import Head from "next/head";
import { Fragment, ReactElement, useRef, useState } from "react"; import { ReactElement, useRef, useState } from "react";
import Layout from "../../../components/layout"; import Layout from "../../../components/layout";
import { NextPageWithLayout } from "../../_app"; import { NextPageWithLayout } from "../../_app";
import { classNames, NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib"; import { classNames, NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
@@ -7,37 +7,27 @@ import {
ArrowDownTrayIcon, ArrowDownTrayIcon,
CheckBadgeIcon, CheckBadgeIcon,
CheckIcon, CheckIcon,
EnvelopeIcon,
PaperAirplaneIcon, PaperAirplaneIcon,
PencilSquareIcon, PencilSquareIcon,
TrashIcon, TrashIcon,
UserPlusIcon, UserPlusIcon,
EnvelopeIcon,
XMarkIcon, XMarkIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { getUserFromToken } from "@documenso/lib/server"; import { getUserFromToken } from "@documenso/lib/server";
import { getDocument } from "@documenso/lib/query"; import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument } from "@prisma/client"; import { Document as PrismaDocument, DocumentStatus } from "@prisma/client";
import { Breadcrumb, Button, IconButton } from "@documenso/ui"; import { Breadcrumb, Button, Dialog, IconButton } from "@documenso/ui";
import { Dialog, Transition } from "@headlessui/react"; import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
import {
createOrUpdateRecipient,
deleteRecipient,
sendSigningRequests,
} from "@documenso/lib/api";
import {
FormProvider,
useFieldArray,
useForm,
useWatch,
} from "react-hook-form";
type FormValues = { import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
export type FormValues = {
signers: { id: number; email: string; name: string }[]; signers: { id: number; email: string; name: string }[];
}; };
const RecipientsPage: NextPageWithLayout = (props: any) => { const RecipientsPage: NextPageWithLayout = (props: any) => {
const title: string = const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
`"` + props?.document?.title + `"` + "Recipients | Documenso";
const breadcrumbItems = [ const breadcrumbItems = [
{ {
title: "Documents", title: "Documents",
@@ -49,11 +39,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
}, },
{ {
title: "Recipients", title: "Recipients",
href: href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients",
NEXT_PUBLIC_WEBAPP_URL +
"/documents/" +
props.document.id +
"/recipients",
}, },
]; ];
@@ -85,25 +71,17 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
<Head> <Head>
<title>{title}</title> <title>{title}</title>
</Head> </Head>
<div className="mt-10"> <div className="px-6 mt-10 sm:px-0">
<div> <div>
<Breadcrumb document={props.document} items={breadcrumbItems} /> <Breadcrumb document={props.document} items={breadcrumbItems} />
</div> </div>
<div className="mt-2 md:flex md:items-center md:justify-between"> <div className="mt-2 md:flex md:items-center md:justify-between">
<div className="min-w-0 flex-1"> <div className="flex-1 min-w-0">
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight"> <h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
{props.document.title} {props.document.title}
</h2> </h2>
</div> </div>
<div className="mt-4 flex flex-shrink-0 md:mt-0 md:ml-4"> <div className="flex flex-shrink-0 mt-4 md:mt-0 md:ml-4">
<Button
icon={PencilSquareIcon}
color="secondary"
className="mr-2"
href={breadcrumbItems[1].href}
>
Edit Document
</Button>
<Button <Button
icon={ArrowDownTrayIcon} icon={ArrowDownTrayIcon}
color="secondary" color="secondary"
@@ -112,6 +90,15 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
> >
Download Download
</Button> </Button>
<Button
icon={PencilSquareIcon}
disabled={props.document.status === DocumentStatus.COMPLETED}
color={props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"}
className="mr-2"
href={breadcrumbItems[1].href}
>
Edit Document
</Button>
<Button <Button
className="min-w-[125px]" className="min-w-[125px]"
color="primary" color="primary"
@@ -122,8 +109,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
disabled={ disabled={
(formValues.length || 0) === 0 || (formValues.length || 0) === 0 ||
!formValues.some( !formValues.some(
(r: any) => (r: any) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
) || ) ||
loading loading
} }
@@ -132,12 +118,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
</Button> </Button>
</div> </div>
</div> </div>
<div className="overflow-hidden rounded-md bg-white shadow mt-10 p-6"> <div className="p-4 mt-10 overflow-hidden bg-white rounded-md shadow sm:p-6">
<div className="border-b border-gray-200 pb-5"> <div className="pb-3 border-b border-gray-200 sm:pb-5">
<h3 className="text-lg font-medium leading-6 text-gray-900"> <h3 className="text-lg font-medium leading-6 text-gray-900 ">Signers</h3>
Signers <p className="max-w-4xl mt-2 text-sm text-gray-500">
</h3>
<p className="mt-2 max-w-4xl text-sm text-gray-500">
The people who will sign the document. The people who will sign the document.
</p> </p>
</div> </div>
@@ -151,189 +135,174 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
{fields.map((item: any, index: number) => ( {fields.map((item: any, index: number) => (
<li <li
key={index} key={index}
className="px-0 py-4 w-full hover:bg-green-50 border-0 group" className="w-full px-2 py-3 border-0 hover:bg-green-50 group sm:py-4"
> >
<div id="container" className="flex w-full"> <div id="container" className="block w-full lg:flex lg:justify-between">
<div <div className="block space-y-2 md:space-x-2 md:space-y-0 md:flex">
className={classNames( <div
"ml-3 w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon", className={classNames(
item.sendStatus === "SENT" ? "bg-gray-100" : "" "md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
)} item.sendStatus === "SENT" ? "bg-gray-100" : ""
> )}
<label
htmlFor="name"
className="block text-xs font-medium text-gray-900"
> >
Email <label htmlFor="name" className="block text-xs font-medium text-gray-900">
</label> Email
<input </label>
type="email" <input
{...register(`signers.${index}.email`, { type="email"
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, {...register(`signers.${index}.email`, {
})} pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
defaultValue={item.email} })}
disabled={item.sendStatus === "SENT" || loading} defaultValue={item.email}
onBlur={() => { disabled={item.sendStatus === "SENT" || loading}
if (!errors?.signers?.[index]) onBlur={() => {
createOrUpdateRecipient({
...formValues[index],
documentId: props.document.id,
});
}}
onKeyDown={(event: any) => {
if (event.key === "Enter")
if (!errors?.signers?.[index]) if (!errors?.signers?.[index])
createOrUpdateRecipient({ createOrUpdateRecipient({
...formValues[index], ...formValues[index],
documentId: props.document.id, documentId: props.document.id,
}); });
}} }}
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 sm:text-sm outline-none bg-inherit" onKeyDown={(event: any) => {
placeholder="john.dorian@loremipsum.com" if (event.key === "Enter")
/> if (!errors?.signers?.[index])
{errors?.signers?.[index] ? ( createOrUpdateRecipient({
<p ...formValues[index],
className="mt-2 text-sm text-red-600" documentId: props.document.id,
id="email-error" });
> }}
<XMarkIcon className="inline h-5" /> Invalid Email className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
</p> placeholder="john.dorian@loremipsum.com"
) : ( />
"" {errors?.signers?.[index] ? (
)} <p className="mt-2 text-sm text-red-600" id="email-error">
</div> <XMarkIcon className="inline h-5" /> Invalid Email
<div </p>
className={classNames(
"ml-3 w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
item.sendStatus === "SENT" ? "bg-gray-100" : ""
)}
>
<label
htmlFor="name"
className="block text-xs font-medium text-gray-900"
>
Name (optional)
</label>
<input
type="text"
{...register(`signers.${index}.name`)}
defaultValue={item.name}
disabled={item.sendStatus === "SENT" || loading}
onBlur={() => {
if (!errors?.signers?.[index])
createOrUpdateRecipient({
...formValues[index],
documentId: props.document.id,
});
}}
onKeyDown={(event: any) => {
if (
event.key === "Enter" &&
!errors?.signers?.[index]
)
createOrUpdateRecipient({
...formValues[index],
documentId: props.document.id,
});
}}
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 sm:text-sm outline-none bg-inherit"
placeholder="John Dorian"
/>
</div>
<div className="ml-auto flex">
<div key={item.id}>
{item.sendStatus === "NOT_SENT" ? (
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-gray-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
Not Sent
</span>
) : (
""
)}
{item.sendStatus === "SENT" &&
item.readStatus !== "OPENED" ? (
<span id="sent_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
<CheckIcon className="inline h-5 mr-1"></CheckIcon>{" "}
Sent
</span>
</span>
) : (
""
)}
{item.readStatus === "OPENED" &&
item.signingStatus === "NOT_SIGNED" ? (
<span id="read_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
Seen
</span>
</span>
) : (
""
)}
{item.signingStatus === "SIGNED" ? (
<span id="signed_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
>
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>
Signed
</span>
</span>
) : ( ) : (
"" ""
)} )}
</div> </div>
</div> <div
<div className="ml-auto flex mr-1"> className={classNames(
<IconButton "md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
icon={PaperAirplaneIcon} item.sendStatus === "SENT" ? "bg-gray-100" : ""
disabled={ )}
!item.id ||
item.sendStatus !== "SENT" ||
item.signingStatus === "SIGNED" ||
loading
}
color="secondary"
className="mr-4 h-9 my-auto"
onClick={() => {
if (confirm("Resend this signing request?")) {
setLoading(true);
sendSigningRequests(props.document, [
item.id,
]).finally(() => {
setLoading(false);
});
}
}}
> >
Resend <label htmlFor="name" className="block text-xs font-medium text-gray-900">
</IconButton> Name (optional)
<IconButton </label>
icon={TrashIcon} <input
disabled={ type="text"
!item.id || item.sendStatus === "SENT" || loading {...register(`signers.${index}.name`)}
} defaultValue={item.name}
onClick={() => { disabled={item.sendStatus === "SENT" || loading}
const removedItem = { ...fields }[index]; onBlur={() => {
remove(index); if (!errors?.signers?.[index])
deleteRecipient(item)?.catch((err) => { createOrUpdateRecipient({
append(removedItem); ...formValues[index],
}); documentId: props.document.id,
}} });
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400" }}
/> onKeyDown={(event: any) => {
if (event.key === "Enter" && !errors?.signers?.[index])
createOrUpdateRecipient({
...formValues[index],
documentId: props.document.id,
});
}}
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
placeholder="John Dorian"
/>
</div>
</div>
<div className="flex items-center space-x-2 lg:ml-2">
<div className="flex mb-2 mr-2 lg:mr-0">
<div key={item.id} className="space-x-2">
{item.sendStatus === "NOT_SENT" ? (
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
Not Sent
</span>
) : (
""
)}
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
<span id="sent_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800 "
>
<CheckIcon className="inline h-5 mr-1" /> Sent
</span>
</span>
) : (
""
)}
{item.readStatus === "OPENED" && item.signingStatus === "NOT_SIGNED" ? (
<span id="read_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
Seen
</span>
</span>
) : (
""
)}
{item.signingStatus === "SIGNED" ? (
<span id="signed_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
>
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>
Signed
</span>
</span>
) : (
""
)}
</div>
</div>
<div className="flex mr-1">
<IconButton
icon={PaperAirplaneIcon}
disabled={
!item.id ||
item.sendStatus !== "SENT" ||
item.signingStatus === "SIGNED" ||
loading
}
color="secondary"
className="my-auto mr-4 h-9"
onClick={() => {
if (confirm("Resend this signing request?")) {
setLoading(true);
sendSigningRequests(props.document, [item.id]).finally(() => {
setLoading(false);
});
}
}}
>
Resend
</IconButton>
<IconButton
icon={TrashIcon}
disabled={!item.id || item.sendStatus === "SENT" || loading}
onClick={() => {
const removedItem = { ...fields }[index];
remove(index);
deleteRecipient(item)?.catch((err) => {
append(removedItem);
});
}}
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</div>
</div> </div>
</div> </div>
</li> </li>
@@ -359,79 +328,16 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
</FormProvider> </FormProvider>
</div> </div>
</div> </div>
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={setOpen}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto"> <Dialog
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> title="Ready to send"
<Transition.Child document={props.document}
as={Fragment} formValues={formValues}
enter="ease-out duration-300" open={open}
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" setLoading={setLoading}
enterTo="opacity-100 translate-y-0 sm:scale-100" setOpen={setOpen}
leave="ease-in duration-200" icon={<EnvelopeIcon className="w-6 h-6 text-green-600" aria-hidden="true" />}
leaveFrom="opacity-100 translate-y-0 sm:scale-100" />
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div>
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
<EnvelopeIcon
className="h-6 w-6 text-green-600"
aria-hidden="true"
/>
</div>
<div className="mt-3 text-center sm:mt-5">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
Ready to send
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
{`"${props.document.title}" will be sent to ${
formValues.filter(
(s: any) => s.email && s.sendStatus != "SENT"
).length
} recipients.`}
</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
<Button color="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
onClick={() => {
setOpen(false);
setLoading(true);
sendSigningRequests(props.document).finally(() => {
setLoading(false);
});
}}
>
Send
</Button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
</> </>
); );
}; };
@@ -451,11 +357,7 @@ export async function getServerSideProps(context: any) {
}; };
const { id: documentId } = context.query; const { id: documentId } = context.query;
const document: PrismaDocument = await getDocument( const document: PrismaDocument = await getDocument(+documentId, context.req, context.res);
+documentId,
context.req,
context.res
);
return { return {
props: { props: {

View File

@@ -107,7 +107,6 @@ export async function getServerSideProps(context: any) {
where: { where: {
documentId: recipient.Document.id, documentId: recipient.Document.id,
recipientId: recipient.id, recipientId: recipient.id,
type: { in: [FieldType.SIGNATURE] },
Signature: { is: null }, Signature: { is: null },
}, },
include: { include: {

View File

@@ -6,7 +6,7 @@ import { Button, IconButton } from "@documenso/ui";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
const SignPage: NextPageWithLayout = (props: any) => { const Signed: NextPageWithLayout = (props: any) => {
const router = useRouter(); const router = useRouter();
const allRecipientsSigned = props.document.Recipient?.every( const allRecipientsSigned = props.document.Recipient?.every(
(r: any) => r.signingStatus === "SIGNED" (r: any) => r.signingStatus === "SIGNED"
@@ -47,7 +47,12 @@ const SignPage: NextPageWithLayout = (props: any) => {
onClick={(event: any) => { onClick={(event: any) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
router.push("/api/documents/" + props.document.id); router.push(
"/api/documents/" +
props.document.id +
"?token=" +
props.recipient.token
);
}} }}
> >
Download "{props.document.title}" Download "{props.document.title}"
@@ -103,8 +108,9 @@ export async function getServerSideProps(context: any) {
props: { props: {
document: JSON.parse(JSON.stringify(recipient.Document)), document: JSON.parse(JSON.stringify(recipient.Document)),
fields: JSON.parse(JSON.stringify(fields)), fields: JSON.parse(JSON.stringify(fields)),
recipient: JSON.parse(JSON.stringify(recipient)),
}, },
}; };
} }
export default SignPage; export default Signed;

View File

@@ -1,13 +1,23 @@
import Head from "next/head"; import Head from "next/head";
import Login from "../components/login"; import Login from "../components/login";
export default function LoginPage() { export default function LoginPage(props: any) {
return ( return (
<> <>
<Head> <Head>
<title>Login | Documenso</title> <title>Login | Documenso</title>
</Head> </Head>
<Login></Login> <Login allowSignup={props.ALLOW_SIGNUP}></Login>
</> </>
); );
} }
export async function getServerSideProps(context: any) {
const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true";
return {
props: {
ALLOW_SIGNUP: ALLOW_SIGNUP,
},
};
}

View File

@@ -14,6 +14,14 @@ export default function SignupPage(props: { source: string }) {
} }
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {
if (process.env.ALLOW_SIGNUP !== "true")
return {
redirect: {
destination: "/login",
permanent: false,
},
};
const signupSource: string = context.query["source"]; const signupSource: string = context.query["source"];
return { return {
props: { props: {

Binary file not shown.

1908
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
"dev": "cd apps && cd web && next dev", "dev": "cd apps && cd web && next dev",
"build": "npm i && cd apps && cd web && npm i && next build", "build": "npm i && cd apps && cd web && npm i && next build",
"start": "cd apps && cd web && next start", "start": "cd apps && cd web && next start",
"db-migrate:dev": "prisma migrate dev",
"db-seed": "prisma db seed", "db-seed": "prisma db seed",
"db-studio": "prisma studio" "db-studio": "prisma studio"
}, },
@@ -24,6 +25,7 @@
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/node": "18.11.9", "@types/node": "18.11.9",
"@types/react-dom": "18.0.9", "@types/react-dom": "18.0.9",
"@types/react-signature-canvas": "^1.0.2",
"avatar-from-initials": "^1.0.3", "avatar-from-initials": "^1.0.3",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
@@ -31,7 +33,7 @@
"eslint-config-next": "13.0.3", "eslint-config-next": "13.0.3",
"install": "^0.13.0", "install": "^0.13.0",
"next": "13.0.3", "next": "13.0.3",
"next-auth": "^4.18.3", "next-auth": ">=4.20.1",
"next-transpile-modules": "^10.0.0", "next-transpile-modules": "^10.0.0",
"npm": "^9.1.3", "npm": "^9.1.3",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
@@ -41,9 +43,5 @@
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-signature-canvas": "^1.0.6", "react-signature-canvas": "^1.0.6",
"typescript": "4.8.4" "typescript": "4.8.4"
},
"devDependencies": {
"@types/react-signature-canvas": "^1.0.2",
"file-loader": "^6.2.0"
} }
} }

View File

@@ -6,7 +6,12 @@ export const uploadDocument = async (event: any) => {
if (event.target.files && event.target.files[0]) { if (event.target.files && event.target.files[0]) {
const body = new FormData(); const body = new FormData();
const document = event.target.files[0]; const document = event.target.files[0];
const fileName = event.target.files[0].name; const fileName: string = event.target.files[0].name;
if (!fileName.endsWith(".pdf")) {
toast.error("Non-PDF documents are not supported yet.");
return;
}
body.append("document", document || ""); body.append("document", document || "");
const response: any = await toast const response: any = await toast
.promise( .promise(

View File

@@ -2,11 +2,12 @@ import toast from "react-hot-toast";
export const createOrUpdateField = async ( export const createOrUpdateField = async (
document: any, document: any,
field: any field: any,
recipientToken: string = ""
): Promise<any> => { ): Promise<any> => {
try { try {
const created = await toast.promise( const created = await toast.promise(
fetch("/api/documents/" + document.id + "/fields", { fetch("/api/documents/" + document.id + "/fields?token=" + recipientToken, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@@ -11,14 +11,29 @@ export const sendMail = async (
content: string | Buffer; content: string | Buffer;
}[] = [] }[] = []
) => { ) => {
if (!process.env.SENDGRID_API_KEY) let transport;
throw new Error("Sendgrid API Key not set."); if (process.env.SENDGRID_API_KEY)
transport = nodemailer.createTransport(
nodemailerSendgrid({
apiKey: process.env.SENDGRID_API_KEY || "",
})
);
if (process.env.SMTP_MAIL_HOST)
transport = nodemailer.createTransport({
host: process.env.SMTP_MAIL_HOST || "",
port: Number(process.env.SMTP_MAIL_PORT) || 587,
auth: {
user: process.env.SMTP_MAIL_USER || "",
pass: process.env.SMTP_MAIL_PASSWORD || "",
},
});
if (!transport)
throw new Error(
"No valid transport for NodeMailer found. Probably Sendgrid API Key nor SMTP Mail host was set."
);
const transport = await nodemailer.createTransport(
nodemailerSendgrid({
apiKey: process.env.SENDGRID_API_KEY || "",
})
);
await transport await transport
.sendMail({ .sendMail({
from: process.env.MAIL_FROM, from: process.env.MAIL_FROM,

View File

@@ -4,16 +4,16 @@ import { SendStatus, ReadStatus, DocumentStatus } from "@prisma/client";
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants"; import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { signingRequestTemplate } from "@documenso/lib/mail"; import { signingRequestTemplate } from "@documenso/lib/mail";
export const sendSigningRequest = async ( export const sendSigningRequest = async (recipient: any, document: any, user: any) => {
recipient: any, const signingRequestMessage = user.name
document: any, ? `${user.name} (${user.email}) has sent you a document to sign. `
user: any : `${user.email} has sent you a document to sign. `;
) => {
await sendMail( await sendMail(
recipient.email, recipient.email,
`Please sign ${document.title}`, `Please sign ${document.title}`,
signingRequestTemplate( signingRequestTemplate(
`${user.name} (${user.email}) has sent you a document to sign. `, signingRequestMessage,
document, document,
recipient, recipient,
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${document.id}/sign?token=${recipient.token}`, `${NEXT_PUBLIC_WEBAPP_URL}/documents/${document.id}/sign?token=${recipient.token}`,

View File

@@ -12,7 +12,7 @@ export async function getUserFromToken(
const tokenEmail = token?.email?.toString(); const tokenEmail = token?.email?.toString();
if (!token) { if (!token) {
res.status(401).send("No session token found for request."); if (res.status) res.status(401).send("No session token found for request.");
return null; return null;
} }

View File

@@ -0,0 +1,148 @@
-- CreateEnum
CREATE TYPE "IdentityProvider" AS ENUM ('DOCUMENSO', 'GOOGLE');
-- CreateEnum
CREATE TYPE "DocumentStatus" AS ENUM ('DRAFT', 'PENDING', 'COMPLETED');
-- CreateEnum
CREATE TYPE "ReadStatus" AS ENUM ('NOT_OPENED', 'OPENED');
-- CreateEnum
CREATE TYPE "SendStatus" AS ENUM ('NOT_SENT', 'SENT');
-- CreateEnum
CREATE TYPE "SigningStatus" AS ENUM ('NOT_SIGNED', 'SIGNED');
-- CreateEnum
CREATE TYPE "FieldType" AS ENUM ('SIGNATURE', 'FREE_SIGNATURE', 'DATE', 'TEXT');
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"name" TEXT,
"email" TEXT NOT NULL,
"emailVerified" TIMESTAMP(3),
"password" TEXT,
"source" TEXT,
"identityProvider" "IdentityProvider" NOT NULL DEFAULT 'DOCUMENSO',
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Document" (
"id" SERIAL NOT NULL,
"created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" INTEGER NOT NULL,
"title" TEXT NOT NULL,
"status" "DocumentStatus" NOT NULL DEFAULT 'DRAFT',
"document" TEXT NOT NULL,
CONSTRAINT "Document_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Recipient" (
"id" SERIAL NOT NULL,
"documentId" INTEGER NOT NULL,
"email" VARCHAR(255) NOT NULL,
"name" VARCHAR(255) NOT NULL DEFAULT '',
"token" TEXT NOT NULL,
"expired" TIMESTAMP(3),
"readStatus" "ReadStatus" NOT NULL DEFAULT 'NOT_OPENED',
"signingStatus" "SigningStatus" NOT NULL DEFAULT 'NOT_SIGNED',
"sendStatus" "SendStatus" NOT NULL DEFAULT 'NOT_SENT',
CONSTRAINT "Recipient_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Field" (
"id" SERIAL NOT NULL,
"documentId" INTEGER NOT NULL,
"recipientId" INTEGER,
"type" "FieldType" NOT NULL,
"page" INTEGER NOT NULL,
"positionX" INTEGER NOT NULL DEFAULT 0,
"positionY" INTEGER NOT NULL DEFAULT 0,
"customText" TEXT NOT NULL,
"inserted" BOOLEAN NOT NULL,
CONSTRAINT "Field_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Signature" (
"id" SERIAL NOT NULL,
"created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"recipientId" INTEGER NOT NULL,
"fieldId" INTEGER NOT NULL,
"signatureImageAsBase64" TEXT,
"typedSignature" TEXT,
CONSTRAINT "Signature_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "Signature_fieldId_key" ON "Signature"("fieldId");
-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Document" ADD CONSTRAINT "Document_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Recipient" ADD CONSTRAINT "Recipient_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Field" ADD CONSTRAINT "Field_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Field" ADD CONSTRAINT "Field_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Signature" ADD CONSTRAINT "Signature_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Signature" ADD CONSTRAINT "Signature_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "Field"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,105 @@
import React from "react";
import { Transition, Dialog as DialogComponent } from "@headlessui/react";
import { Fragment } from "react";
import { Button } from "@documenso/ui";
import { sendSigningRequests } from "@documenso/lib/api";
import { Document as PrismaDocument } from "@prisma/client";
type FormValue = {
id: number;
email: string;
name: string;
};
type DialogProps = {
title: string;
open: boolean;
setOpen: (open: boolean) => void;
document: PrismaDocument;
formValues: FormValue[];
setLoading: (loading: boolean) => void;
icon: React.ReactNode;
};
export function Dialog({
title,
open,
setOpen,
document,
formValues,
setLoading,
icon,
}: DialogProps) {
const unsentEmailsLength = formValues.filter(
(s: any) => s.email && s.sendStatus != "SENT"
).length;
return (
<Transition.Root show={open} as={Fragment}>
<DialogComponent as="div" className="relative z-10" onClose={setOpen}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" />
</Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex items-end justify-center min-h-full p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<DialogComponent.Panel className="relative px-4 pt-5 pb-4 overflow-hidden text-left transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div>
<div className="flex items-center justify-center w-12 h-12 mx-auto bg-green-100 rounded-full">
{icon}
</div>
<div className="mt-3 text-center sm:mt-5">
<DialogComponent.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
{title}
</DialogComponent.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
{`"${document.title}" will be sent to ${unsentEmailsLength} recipients.`}
</p>
</div>
</div>
</div>
<div className="flex justify-end gap-3 mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:flex-none ">
<Button color="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
onClick={() => {
setOpen(false);
setLoading(true);
sendSigningRequests(document).finally(() => {
setLoading(false);
});
}}
>
Send
</Button>
</div>
</DialogComponent.Panel>
</Transition.Child>
</div>
</div>
</DialogComponent>
</Transition.Root>
);
}

View File

@@ -0,0 +1 @@
export { Dialog } from "./Dialog";

View File

@@ -1,3 +1,4 @@
export { Button, IconButton } from "./components/button/index"; export { Button, IconButton } from "./components/button/index";
export { SelectBox } from "./components/selectBox/index"; export { SelectBox } from "./components/selectBox/index";
export { Breadcrumb } from "./components/breadcrumb/index"; export { Breadcrumb } from "./components/breadcrumb/index";
export { Dialog } from "./components/dialog/index";