Compare commits
208 Commits
0.9-develo
...
feat/suppo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a300c3fb3a | ||
|
|
5e07b8bd92 | ||
|
|
7b1d626f9a | ||
|
|
de46d0f4ab | ||
|
|
cc7ab171b1 | ||
|
|
466941dbc2 | ||
|
|
0564792604 | ||
|
|
32f904ad68 | ||
|
|
ecaec356a1 | ||
|
|
38f730c730 | ||
|
|
2b4a9fbe21 | ||
|
|
106ac40fb1 | ||
|
|
62ac181193 | ||
|
|
9580100d66 | ||
|
|
38a8279757 | ||
|
|
ed77000746 | ||
|
|
73b72c6cce | ||
|
|
b2aa4d6587 | ||
|
|
bde80bf2c9 | ||
|
|
1e505088ad | ||
|
|
ae0799168a | ||
|
|
b5ec3cc817 | ||
|
|
370f38457b | ||
|
|
f34813e450 | ||
|
|
8f6c6dccf4 | ||
|
|
826704c21f | ||
|
|
4f47bbb552 | ||
|
|
825231fe2a | ||
|
|
012d2a9a09 | ||
|
|
85c593d8e3 | ||
|
|
0f28692a39 | ||
|
|
22bc854cac | ||
|
|
d2c5657093 | ||
|
|
6934e573d5 | ||
|
|
7eaa00b836 | ||
|
|
e7e881be01 | ||
|
|
e80997f462 | ||
|
|
da0166b746 | ||
|
|
900b816ae0 | ||
|
|
ed3e4d22ef | ||
|
|
bf84ec8962 | ||
|
|
1abfa93551 | ||
|
|
039cc75882 | ||
|
|
8457823d8e | ||
|
|
d135df827a | ||
|
|
d2301a923b | ||
|
|
108614bf46 | ||
|
|
adf69edd54 | ||
|
|
82139f6b2d | ||
|
|
270c82759c | ||
|
|
01c7903efa | ||
|
|
64b755d5ba | ||
|
|
8788b64585 | ||
|
|
c9547057f6 | ||
|
|
17e688c222 | ||
|
|
f5a42e694d | ||
|
|
b2d09216c8 | ||
|
|
6d30a486ab | ||
|
|
dc6217b14e | ||
|
|
a6171ec4f3 | ||
|
|
d0f962598c | ||
|
|
81fd9ff749 | ||
|
|
4dcb0a684d | ||
|
|
ab96990d43 | ||
|
|
ad5b2bcf82 | ||
|
|
6f18be6b5b | ||
|
|
8039871ab1 | ||
|
|
4b9840d7e0 | ||
|
|
544a16caff | ||
|
|
989d036e54 | ||
|
|
894f8720b8 | ||
|
|
70ea3ceaf3 | ||
|
|
80d26adf9c | ||
|
|
b4e21f97e3 | ||
|
|
95c3be9a77 | ||
|
|
52f554a636 | ||
|
|
849885b5b3 | ||
|
|
bcc2530484 | ||
|
|
d863f89232 | ||
|
|
84e3d29589 | ||
|
|
ba3ffe68ea | ||
|
|
5c58b32d92 | ||
|
|
f10bafd998 | ||
|
|
2cf8896e46 | ||
|
|
e873af3ec9 | ||
|
|
06501bde60 | ||
|
|
0dcab27e65 | ||
|
|
ff2334ab55 | ||
|
|
63bd044723 | ||
|
|
b111874d7c | ||
|
|
21149f82ba | ||
|
|
cb77a40fd9 | ||
|
|
7aa7485388 | ||
|
|
984084dd3b | ||
|
|
421327432a | ||
|
|
134e366c27 | ||
|
|
c79592cd0a | ||
|
|
f7cc44f138 | ||
|
|
60ff4fc992 | ||
|
|
e4e44b7f22 | ||
|
|
6034e7a21e | ||
|
|
2a34cc26c6 | ||
|
|
6ea38efd9d | ||
|
|
0ce66a7957 | ||
|
|
49cb50ed6e | ||
|
|
065efabb39 | ||
|
|
e86d4cc719 | ||
|
|
5dd3713475 | ||
|
|
30c1c76dd7 | ||
|
|
22e191e98c | ||
|
|
5db54d3b8c | ||
|
|
593c317bf1 | ||
|
|
ee4ca018d8 | ||
|
|
e3db462587 | ||
|
|
739d29d753 | ||
|
|
964e749039 | ||
|
|
84b57d715c | ||
|
|
85f2b5e84a | ||
|
|
51c63715d6 | ||
|
|
a9ad586035 | ||
|
|
f3b68772a6 | ||
|
|
2d0fb2879d | ||
|
|
2291744580 | ||
|
|
0c3305b11d | ||
|
|
4031faec46 | ||
|
|
a9cd5e0d94 | ||
|
|
24e044097c | ||
|
|
2bdfb884ec | ||
|
|
ff85c294b2 | ||
|
|
67328b4504 | ||
|
|
900ec32697 | ||
|
|
0344ac324c | ||
|
|
b3e89b16bc | ||
|
|
a9befd342c | ||
|
|
16f6da01c0 | ||
|
|
f8f941a9cd | ||
|
|
e3059cfb34 | ||
|
|
e79a622ddd | ||
|
|
9945cfb2c7 | ||
|
|
f32c3d999a | ||
|
|
4bb5064477 | ||
|
|
c655cb52ad | ||
|
|
3d0d7d1245 | ||
|
|
f96bf757e2 | ||
|
|
3f897abffa | ||
|
|
df238e2be3 | ||
|
|
2b83e28e6d | ||
|
|
6f31dacd74 | ||
|
|
d4324538cc | ||
|
|
2f2b708bfe | ||
|
|
7656d4259e | ||
|
|
d509a6178f | ||
|
|
2fed1a7034 | ||
|
|
de3c500fea | ||
|
|
dc0c78f270 | ||
|
|
a3e17e9f3e | ||
|
|
7d79e10587 | ||
|
|
1a37998f39 | ||
|
|
2d69783ca1 | ||
|
|
b7cc4aed9b | ||
|
|
3dfa8fc597 | ||
|
|
d5d3b17623 | ||
|
|
4710176f78 | ||
|
|
f22d4ebeab | ||
|
|
d32b9871db | ||
|
|
0e9aa4ab62 | ||
|
|
9e536e95b6 | ||
|
|
a57e0b2b57 | ||
|
|
a1736afc62 | ||
|
|
91b206e3d7 | ||
|
|
d37dd000af | ||
|
|
7d6bd00a22 | ||
|
|
8505b9cd10 | ||
|
|
dd67e1a6f0 | ||
|
|
9009506bb6 | ||
|
|
025e6a4eb1 | ||
|
|
72914c49c4 | ||
|
|
7d0c91e565 | ||
|
|
36526119b2 | ||
|
|
156b7a69e7 | ||
|
|
4eb4759381 | ||
|
|
1bfac711ac | ||
|
|
cb2d77c609 | ||
|
|
253e5cfcfa | ||
|
|
ff16972646 | ||
|
|
3ba6afabfc | ||
|
|
3961402c70 | ||
|
|
7fc228a562 | ||
|
|
899dd205f2 | ||
|
|
4a915134e4 | ||
|
|
266ecf0f8d | ||
|
|
071398273a | ||
|
|
6419d22155 | ||
|
|
738c798dbd | ||
|
|
cd5f6fde32 | ||
|
|
93654ccae5 | ||
|
|
7c830d3607 | ||
|
|
559432cc15 | ||
|
|
2400c34c71 | ||
|
|
ff977c6bff | ||
|
|
526be3b906 | ||
|
|
9f700ad0b2 | ||
|
|
f1dc5687d7 | ||
|
|
793902ae54 | ||
|
|
f77f101e67 | ||
|
|
49f36a103b | ||
|
|
6c7ee3edf4 | ||
|
|
c819ed3cfb |
38
.dockerignore
Normal file
38
.dockerignore
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
**/node_modules
|
||||||
|
**/.pnp
|
||||||
|
**.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
**/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
**/.next/
|
||||||
|
**/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
**/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
.env
|
||||||
|
.env.example
|
||||||
44
.env.example
44
.env.example
@@ -1,7 +1,10 @@
|
|||||||
# 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
|
# Option 3: Use the provided dx setup (RECOMMENDED)
|
||||||
|
# => postgres://documenso:password@127.0.0.1:54320/documenso
|
||||||
|
#
|
||||||
|
# ⚠ 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 +16,38 @@ 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
|
# SIGNING
|
||||||
|
CERT_FILE_PATH=
|
||||||
|
CERT_PASSPHRASE=
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# If you're using the dx setup you can use the following values:
|
||||||
|
#
|
||||||
|
# SMTP_MAIL_HOST='127.0.0.1'
|
||||||
|
# SMTP_MAIL_PORT='2500'
|
||||||
|
# SMTP_MAIL_USER='documenso'
|
||||||
|
# SMTP_MAIL_PASSWORD='documenso'
|
||||||
|
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'
|
||||||
|
|
||||||
|
# STRIPE
|
||||||
|
STRIPE_API_KEY=
|
||||||
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
|
||||||
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
|
||||||
|
|
||||||
|
#FEATURE FLAGS
|
||||||
|
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
|
||||||
|
NEXT_PUBLIC_ALLOW_SIGNUP=true
|
||||||
|
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=true
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "apps/website/documenso/website"]
|
|
||||||
path = apps/website/documenso/website
|
|
||||||
url = http://github.com/eltimuro/website.git
|
|
||||||
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@@ -13,7 +13,13 @@
|
|||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.removeUnusedImports": false
|
"source.removeUnusedImports": false
|
||||||
},
|
},
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"spellright.language": ["de"],
|
"spellright.language": [
|
||||||
"spellright.documentTypes": ["markdown", "latex", "plaintext"]
|
"de"
|
||||||
|
],
|
||||||
|
"spellright.documentTypes": [
|
||||||
|
"markdown",
|
||||||
|
"latex",
|
||||||
|
"plaintext"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
|
||||||
100
README.md
100
README.md
@@ -1,6 +1,9 @@
|
|||||||
|
> <strong>We are launching TOMORROW on Product Hunt soon! Sign up to support the launch: </strong>
|
||||||
|
> <center><a href="https://dub.sh/documenso-launch"><img src="https://img.shields.io/badge/Documenso%20on%20Product%20Hunt-Notify%20Me-orange" alt="Product Hunt"></a></center>
|
||||||
|
|
||||||
<p align="center" style="margin-top: 12px">
|
<p align="center" style="margin-top: 12px">
|
||||||
<a href="https://github.com/documenso/documenso.com">
|
<a href="https://github.com/documenso/documenso.com">
|
||||||
<img width="250px" src="https://user-images.githubusercontent.com/1309312/224986248-5b8a5cdc-2dc1-46b9-a354-985bb6808ee0.png" alt="Documenso Logo">
|
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/cd7823ec-4baa-40b9-be78-4acb3b1c73cb" alt="Documenso Logo">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h3 align="center">Open Source Signing Infrastructure</h3>
|
<h3 align="center">Open Source Signing Infrastructure</h3>
|
||||||
@@ -25,7 +28,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 +60,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,19 +71,17 @@ 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
|
|
||||||
|
|
||||||
- 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:
|
||||||
|
|
||||||
- [Typescript](https://www.typescriptlang.org/)
|
- [Typescript](https://www.typescriptlang.org/)
|
||||||
- [Javascript (when neccessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
- [Javascript (when necessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
||||||
- [NextJS (JS Fullstack Framework)](https://nextjs.org/)
|
- [NextJS (JS Fullstack Framework)](https://nextjs.org/)
|
||||||
- [Postgres SQL (Database)](https://www.postgresql.org/)
|
- [Postgres SQL (Database)](https://www.postgresql.org/)
|
||||||
- [Prisma (ORM - Object-relational mapping)](https://www.prisma.io/)
|
- [Prisma (ORM - Object-relational mapping)](https://www.prisma.io/)
|
||||||
@@ -87,7 +89,7 @@ Documenso is built using awesome open source tech including:
|
|||||||
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
|
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
|
||||||
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
|
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
|
||||||
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
|
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
|
||||||
- Check out /packages.json and /apps/web/package.json for more
|
- 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.
|
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
|
||||||
|
|
||||||
# Getting Started
|
# Getting Started
|
||||||
@@ -97,45 +99,78 @@ Documenso is built using awesome open source tech including:
|
|||||||
To run Documenso locally you need
|
To run Documenso locally you need
|
||||||
|
|
||||||
- [Node.js (Version: >=18.x)](https://nodejs.org/en/download/)
|
- [Node.js (Version: >=18.x)](https://nodejs.org/en/download/)
|
||||||
- Node Package Manger NPM - included in Node.js
|
- Node Package Manager NPM - included in Node.js
|
||||||
- [PostgreSQL (local or remote)](https://www.postgresql.org/download/)
|
- [PostgreSQL (local or remote)](https://www.postgresql.org/download/)
|
||||||
|
|
||||||
|
## Developer Quickstart
|
||||||
|
|
||||||
|
> **Note**: This is a quickstart for developers. It assumes that you have both [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/) installed on your machine.
|
||||||
|
|
||||||
|
Want to get up and running quickly? Follow these steps:
|
||||||
|
|
||||||
|
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/documenso/documenso
|
||||||
|
```
|
||||||
|
|
||||||
|
- Set up your `.env` file using the recommendations in the `.env.example` file.
|
||||||
|
- Run `npm run dx` in the root directory
|
||||||
|
- This will spin up a postgres database and inbucket mail server in docker containers.
|
||||||
|
- Run `npm run dev` in the root directory
|
||||||
|
- Want it even faster? Just use
|
||||||
|
```sh
|
||||||
|
npm run d
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! You should now be able to access the app at http://localhost:3000
|
||||||
|
|
||||||
|
Incoming mail will be available at http://localhost:9000
|
||||||
|
|
||||||
|
Your database will also be available on port `54320`. You can connect to it using your favorite database client.
|
||||||
|
|
||||||
## Developer Setup
|
## Developer Setup
|
||||||
|
|
||||||
Follow these steps to setup documenso on you local machnine:
|
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.
|
- [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 (recommended)
|
||||||
- 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\_\* variables</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 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)**.
|
||||||
|
|
||||||
## Updating
|
## Updating
|
||||||
- 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:
|
|
||||||
```sh
|
|
||||||
npx prisma generate
|
|
||||||
```
|
|
||||||
- This is not neccessary on first clone
|
|
||||||
|
|
||||||
# Creating your own signging certificate
|
- If you pull the newest version from main, using <code>git pull</code>, 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
|
||||||
|
```
|
||||||
|
- This is not necessary on first clone.
|
||||||
|
|
||||||
For the digital signature of you documents you need a signign certificate in .p12 formate (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
|
# Creating your own signing certificate
|
||||||
|
|
||||||
|
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
|
||||||
|
|
||||||
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:\
|
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>
|
<code>openssl genrsa -out private.key 2048</code>
|
||||||
@@ -144,10 +179,19 @@ 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>
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
|
||||||
|
> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
|
||||||
|
|
||||||
|
Want to create a production ready docker image? Follow these steps:
|
||||||
|
|
||||||
|
- Run `./docker/build.sh` in the root directory.
|
||||||
|
- Publish the image to your docker registry of choice.
|
||||||
|
|
||||||
# Deploying - Coming Soon™
|
# Deploying - Coming Soon™
|
||||||
|
|
||||||
- Docker support
|
- Docker support
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["next/babel"],
|
|
||||||
"plugins": []
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": ["next/babel", "next/core-web-vitals"]
|
"extends": [
|
||||||
}
|
"next/core-web-vitals"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"react/no-unescaped-entities": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
70
apps/web/components/billing-plans.tsx
Normal file
70
apps/web/components/billing-plans.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { classNames } from "@documenso/lib";
|
||||||
|
import { STRIPE_PLANS, fetchCheckoutSession, useSubscription } from "@documenso/lib/stripe";
|
||||||
|
import { Button } from "@documenso/ui";
|
||||||
|
import { Switch } from "@headlessui/react";
|
||||||
|
|
||||||
|
export const BillingPlans = () => {
|
||||||
|
const { subscription, isLoading } = useSubscription();
|
||||||
|
const [isAnnual, setIsAnnual] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!subscription &&
|
||||||
|
STRIPE_PLANS.map((plan) => (
|
||||||
|
<div key={plan.name} className="rounded-lg border py-4 px-6">
|
||||||
|
<h3 className="text-center text-lg font-medium leading-6 text-gray-900">{plan.name}</h3>
|
||||||
|
|
||||||
|
<div className="my-4 flex justify-center">
|
||||||
|
<Switch.Group as="div" className="flex items-center">
|
||||||
|
<Switch
|
||||||
|
checked={isAnnual}
|
||||||
|
onChange={setIsAnnual}
|
||||||
|
className={classNames(
|
||||||
|
isAnnual ? "bg-neon-600" : "bg-gray-200",
|
||||||
|
"focus:ring-neon-600 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2"
|
||||||
|
)}>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
isAnnual ? "translate-x-5" : "translate-x-0",
|
||||||
|
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
<Switch.Label as="span" className="ml-3 text-sm">
|
||||||
|
<span className="font-medium text-gray-900">Annual billing</span>{" "}
|
||||||
|
<span className="text-gray-500">(Save $60)</span>
|
||||||
|
</Switch.Label>
|
||||||
|
</Switch.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-2 text-center text-gray-500">
|
||||||
|
${(isAnnual ? plan.prices.yearly.price : plan.prices.monthly.price).toFixed(2)}{" "}
|
||||||
|
<span className="text-sm text-gray-400">{isAnnual ? "/yr" : "/mo"}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-4 text-center text-sm text-gray-500">
|
||||||
|
All you need for easy signing. <br></br>Includes everthing we build this year.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
disabled={isLoading}
|
||||||
|
onClick={() =>
|
||||||
|
fetchCheckoutSession({
|
||||||
|
priceId: isAnnual ? plan.prices.yearly.priceId : plan.prices.monthly.priceId,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.success) {
|
||||||
|
window.location.href = res.url;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
Subscribe
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
51
apps/web/components/billing-warning.tsx
Normal file
51
apps/web/components/billing-warning.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useSubscription } from "@documenso/lib/stripe"
|
||||||
|
import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { SubscriptionStatus } from '@prisma/client'
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const BillingWarning = () => {
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{subscription?.status === SubscriptionStatus.PAST_DUE && (
|
||||||
|
<div className="bg-yellow-50 p-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto flex max-w-3xl items-start justify-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<PaperAirplaneIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm text-yellow-700">
|
||||||
|
Your subscription is past due.{" "}
|
||||||
|
<Link href="/account/billing" className="text-yellow-700 underline">
|
||||||
|
Please update your payment information to avoid any service interruptions.
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subscription?.status === SubscriptionStatus.INACTIVE && (
|
||||||
|
<div className="bg-red-50 p-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto flex max-w-3xl items-center justify-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<PaperAirplaneIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm text-red-700">
|
||||||
|
Your subscription is inactive. You can continue to view and edit your documents,
|
||||||
|
but you will not be able to send them or create new ones.{" "}
|
||||||
|
<Link href="/account/billing" className="text-red-700 underline">
|
||||||
|
You can update your payment information here
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Draggable from "react-draggable";
|
|
||||||
import Logo from "../logo";
|
|
||||||
import { IconButton } from "@documenso/ui";
|
import { IconButton } from "@documenso/ui";
|
||||||
|
import Logo from "../logo";
|
||||||
import { XCircleIcon } from "@heroicons/react/20/solid";
|
import { XCircleIcon } from "@heroicons/react/20/solid";
|
||||||
|
import Draggable from "react-draggable";
|
||||||
|
|
||||||
const stc = require("string-to-color");
|
const stc = require("string-to-color");
|
||||||
|
|
||||||
type FieldPropsType = {
|
type FieldPropsType = {
|
||||||
@@ -51,21 +52,19 @@ export default function EditableField(props: FieldPropsType) {
|
|||||||
onMouseDown={(e: any) => {
|
onMouseDown={(e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{/* width: 192 height 96 */}
|
{/* width: 192 height 96 */}
|
||||||
<div
|
<div
|
||||||
hidden={props.hidden}
|
hidden={props.hidden}
|
||||||
ref={nodeRef}
|
ref={nodeRef}
|
||||||
className="cursor-move opacity-80 p-2 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none"
|
className="absolute top-0 left-0 m-auto h-16 w-48 cursor-move select-none flex-row-reverse p-2 text-center text-lg font-bold opacity-80"
|
||||||
style={{
|
style={{
|
||||||
background: stc(props.field.Recipient.email),
|
background: stc(props.field.Recipient.email),
|
||||||
}}
|
}}>
|
||||||
>
|
<div className="m-auto flex-row-reverse overflow-hidden text-center text-lg font-bold">
|
||||||
<div className="m-auto overflow-hidden flex-row-reverse text-lg font-bold text-center">
|
|
||||||
{field.type}
|
{field.type}
|
||||||
{field.type === "SIGNATURE" ? (
|
{field.type === "SIGNATURE" ? (
|
||||||
<div className="text-xs text-center">
|
<div className="text-center text-xs">
|
||||||
{`${props.field.Recipient?.name} <${props.field.Recipient?.email}>`}
|
{`${props.field.Recipient?.name} <${props.field.Recipient?.email}>`}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -79,8 +78,7 @@ export default function EditableField(props: FieldPropsType) {
|
|||||||
icon={XCircleIcon}
|
icon={XCircleIcon}
|
||||||
onClick={(event: any) => {
|
onClick={(event: any) => {
|
||||||
props.onDelete(props.field.id);
|
props.onDelete(props.field.id);
|
||||||
}}
|
}}></IconButton>
|
||||||
></IconButton>
|
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
</Draggable>
|
</Draggable>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { RadioGroup } from "@headlessui/react";
|
|
||||||
import { classNames } from "@documenso/lib";
|
import { classNames } from "@documenso/lib";
|
||||||
|
import { RadioGroup } from "@headlessui/react";
|
||||||
import { FieldType } from "@prisma/client";
|
import { FieldType } from "@prisma/client";
|
||||||
|
|
||||||
const stc = require("string-to-color");
|
const stc = require("string-to-color");
|
||||||
|
|
||||||
export default function FieldTypeSelector(props: any) {
|
export default function FieldTypeSelector(props: any) {
|
||||||
@@ -24,11 +25,7 @@ export default function FieldTypeSelector(props: any) {
|
|||||||
value={selectedFieldType}
|
value={selectedFieldType}
|
||||||
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) => (
|
||||||
<RadioGroup.Option
|
<RadioGroup.Option
|
||||||
@@ -40,30 +37,23 @@ export default function FieldTypeSelector(props: any) {
|
|||||||
className={({ checked, active }) =>
|
className={({ checked, active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
checked ? "border-neon border-2" : "border-transparent",
|
checked ? "border-neon border-2" : "border-transparent",
|
||||||
"hover:bg-slate-100 select-none relative block cursor-pointer rounded-lg border bg-white px-3 py-2 focus:outline-none sm:flex sm:justify-between"
|
"relative block cursor-pointer select-none rounded-lg border bg-white px-3 py-2 hover:bg-slate-100 focus:outline-none sm:flex sm:justify-between"
|
||||||
)
|
)
|
||||||
}
|
}>
|
||||||
>
|
|
||||||
{({ active, checked }) => (
|
{({ active, checked }) => (
|
||||||
<>
|
<>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<span className="flex flex-col text-sm">
|
<span className="flex flex-col text-sm">
|
||||||
<RadioGroup.Label
|
<RadioGroup.Label as="span" className="font-medium text-gray-900">
|
||||||
as="span"
|
|
||||||
className="font-medium text-gray-900"
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
className="inline-block h-4 w-4 flex-shrink-0 rounded-full mr-3 align-middle"
|
className="mr-3 inline-block h-4 w-4 flex-shrink-0 rounded-full align-middle"
|
||||||
style={{
|
style={{
|
||||||
background: stc(props.selectedRecipient?.email),
|
background: stc(props.selectedRecipient?.email),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="align-middle">
|
<span className="align-middle">
|
||||||
{" "}
|
{" "}
|
||||||
{
|
{fieldTypes.filter((e) => e.id === fieldType.id)[0].name}
|
||||||
fieldTypes.filter((e) => e.id === fieldType.id)[0]
|
|
||||||
.name
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
</RadioGroup.Label>
|
</RadioGroup.Label>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { createOrUpdateField, deleteField } from "@documenso/lib/api";
|
import dynamic from "next/dynamic";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { createField } from "@documenso/features/editor";
|
import { createField } from "@documenso/features/editor";
|
||||||
import RecipientSelector from "./recipient-selector";
|
import { createOrUpdateField, deleteField } from "@documenso/lib/api";
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
||||||
import FieldTypeSelector from "./field-type-selector";
|
import FieldTypeSelector from "./field-type-selector";
|
||||||
|
import RecipientSelector from "./recipient-selector";
|
||||||
|
import { InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
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 +20,8 @@ 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,31 @@ 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"> →</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,27 +85,19 @@ 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);
|
||||||
}}
|
}}></PDFViewer>
|
||||||
></PDFViewer>
|
|
||||||
<div
|
<div
|
||||||
hidden={noRecipients}
|
hidden={noRecipients}
|
||||||
className="fixed left-0 top-1/3 max-w-xs border border-slate-300 bg-white py-4 pr-5 rounded-md"
|
className="fixed left-0 top-1/3 max-w-xs rounded-md border border-slate-300 bg-white py-4 pr-5">
|
||||||
>
|
|
||||||
<RecipientSelector
|
<RecipientSelector
|
||||||
recipients={props?.document?.Recipient}
|
recipients={props?.document?.Recipient}
|
||||||
onChange={setSelectedRecipient}
|
onChange={setSelectedRecipient}
|
||||||
/>
|
/>
|
||||||
<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,16 +109,12 @@ 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, page, selectedRecipient, selectedFieldType);
|
||||||
e,
|
|
||||||
page,
|
|
||||||
selectedRecipient,
|
|
||||||
selectedFieldType
|
|
||||||
);
|
|
||||||
|
|
||||||
createOrUpdateField(props?.document, signatureField).then((res) => {
|
createOrUpdateField(props?.document, signatureField).then((res) => {
|
||||||
setFields(fields.concat(res));
|
setFields((prevState) => [...prevState, res]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
import Logo from "../logo";
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import SignatureDialog from "./signature-dialog";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "@documenso/ui";
|
import dynamic from "next/dynamic";
|
||||||
import {
|
import { useRouter } from "next/router";
|
||||||
CheckBadgeIcon,
|
|
||||||
InformationCircleIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { FieldType } from "@prisma/client";
|
|
||||||
import {
|
|
||||||
createOrUpdateField,
|
|
||||||
deleteField,
|
|
||||||
signDocument,
|
|
||||||
} from "@documenso/lib/api";
|
|
||||||
import { createField } from "@documenso/features/editor";
|
import { createField } from "@documenso/features/editor";
|
||||||
|
import { createOrUpdateField, deleteField, signDocument } from "@documenso/lib/api";
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
||||||
|
import { Button } from "@documenso/ui";
|
||||||
|
import Logo from "../logo";
|
||||||
|
import SignatureDialog from "./signature-dialog";
|
||||||
|
import { CheckBadgeIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { FieldType } from "@prisma/client";
|
||||||
|
|
||||||
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
|
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -28,9 +20,7 @@ export default function PDFSigner(props: any) {
|
|||||||
const [signingDone, setSigningDone] = useState(false);
|
const [signingDone, setSigningDone] = useState(false);
|
||||||
const [localSignatures, setLocalSignatures] = useState<any[]>([]);
|
const [localSignatures, setLocalSignatures] = useState<any[]>([]);
|
||||||
const [fields, setFields] = useState<any[]>(props.fields);
|
const [fields, setFields] = useState<any[]>(props.fields);
|
||||||
const signatureFields = fields.filter(
|
const signatureFields = fields.filter((field) => field.type === FieldType.SIGNATURE);
|
||||||
(field) => field.type === FieldType.SIGNATURE
|
|
||||||
);
|
|
||||||
const [dialogField, setDialogField] = useState<any>();
|
const [dialogField, setDialogField] = useState<any>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -70,7 +60,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);
|
||||||
}
|
}
|
||||||
@@ -81,33 +71,29 @@ export default function PDFSigner(props: any) {
|
|||||||
<div className="bg-neon p-4">
|
<div className="bg-neon p-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Logo className="h-12 w-12 -mt-2.5"></Logo>
|
<Logo className="-mt-2.5 h-12 w-12"></Logo>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 flex-1 md:flex md:justify-between text-center justify-start items-center">
|
<div className="ml-3 flex-1 items-center justify-start text-center md:flex md:justify-between">
|
||||||
<p className="text-lg text-slate-700">
|
<p className="text-lg text-slate-700">
|
||||||
{props.document.User.name
|
{props.document.User.name
|
||||||
? `${props.document.User.name} (${props.document.User.email})`
|
? `${props.document.User.name} (${props.document.User.email})`
|
||||||
: props.document.User.email}{" "}
|
: props.document.User.email}{" "}
|
||||||
would like you to sign this document.
|
would like you to sign this document.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-3 text-sm md:mt-0 md:ml-6">
|
<p className="mt-3 text-sm md:mt-0 md:ml-6 text-right md:text-inherit">
|
||||||
<Button
|
<Button
|
||||||
disabled={!signingDone}
|
disabled={!signingDone}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
icon={CheckBadgeIcon}
|
icon={CheckBadgeIcon}
|
||||||
className="float-right"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
signDocument(
|
signDocument(props.document, localSignatures, `${router.query.token}`).then(
|
||||||
props.document,
|
() => {
|
||||||
localSignatures,
|
router.push(
|
||||||
`${router.query.token}`
|
`/documents/${props.document.id}/signed?token=${router.query.token}`
|
||||||
).then(() => {
|
);
|
||||||
router.push(
|
}
|
||||||
`/documents/${props.document.id}/signed?token=${router.query.token}`
|
);
|
||||||
);
|
}}>
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Done
|
Done
|
||||||
</Button>
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
@@ -118,15 +104,11 @@ export default function PDFSigner(props: any) {
|
|||||||
<div className="bg-yellow-50 p-4">
|
<div className="bg-yellow-50 p-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<InformationCircleIcon
|
<InformationCircleIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||||
className="h-5 w-5 text-yellow-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 flex-1 md:flex md:justify-between">
|
<div className="ml-3 flex-1 md:flex md:justify-between">
|
||||||
<p className="text-sm text-yellow-700">
|
<p className="text-sm text-yellow-700">
|
||||||
You can sign this document anywhere you like, but maybe look for
|
You can sign this document anywhere you like, but maybe look for a signature line.
|
||||||
a signature line.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,12 +127,10 @@ export default function PDFSigner(props: any) {
|
|||||||
pdfUrl={`${NEXT_PUBLIC_WEBAPP_URL}/api/documents/${router.query.id}?token=${router.query.token}`}
|
pdfUrl={`${NEXT_PUBLIC_WEBAPP_URL}/api/documents/${router.query.id}?token=${router.query.token}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseDown={function onMouseDown(e: any, page: number) {
|
onMouseDown={function onMouseDown(e: any, page: number) {
|
||||||
if (signatureFields.length === 0)
|
if (signatureFields.length === 0) addFreeSignature(e, page, props.recipient);
|
||||||
addFreeSignature(e, page, props.recipient);
|
|
||||||
}}
|
}}
|
||||||
onMouseUp={() => {}}
|
onMouseUp={() => {}}
|
||||||
onDelete={onDeleteHandler}
|
onDelete={onDeleteHandler}></PDFViewer>
|
||||||
></PDFViewer>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -167,15 +147,10 @@ export default function PDFSigner(props: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addFreeSignature(e: any, page: number, recipient: any): any {
|
function addFreeSignature(e: any, page: number, recipient: any): any {
|
||||||
const freeSignatureField = createField(
|
const freeSignatureField = createField(e, page, recipient, FieldType.FREE_SIGNATURE);
|
||||||
e,
|
|
||||||
page,
|
|
||||||
recipient,
|
|
||||||
FieldType.FREE_SIGNATURE
|
|
||||||
);
|
|
||||||
|
|
||||||
createOrUpdateField(props.document, freeSignatureField).then((res) => {
|
createOrUpdateField(props.document, freeSignatureField, recipient.token).then((res) => {
|
||||||
setFields(fields.concat(res));
|
setFields((prevState) => [...prevState, res]);
|
||||||
setDialogField(res);
|
setDialogField(res);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
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 { FieldType } from "@prisma/client";
|
||||||
|
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
|
||||||
import short from "short-uuid";
|
import short from "short-uuid";
|
||||||
|
|
||||||
export default function PDFViewer(props) {
|
export default function PDFViewer(props) {
|
||||||
@@ -32,16 +33,14 @@ export default function PDFViewer(props) {
|
|||||||
<div
|
<div
|
||||||
hidden={loading}
|
hidden={loading}
|
||||||
onMouseUp={props.onMouseUp}
|
onMouseUp={props.onMouseUp}
|
||||||
style={{ height: numPages * pageHeight + 1000 }}
|
style={{ height: numPages * pageHeight + 1000 }}>
|
||||||
>
|
<div className="mt-6 max-w-xs"></div>
|
||||||
<div className="max-w-xs mt-6"></div>
|
|
||||||
<Document
|
<Document
|
||||||
file={props.pdfUrl}
|
file={props.pdfUrl}
|
||||||
onLoadSuccess={onDocumentLoadSuccess}
|
onLoadSuccess={onDocumentLoadSuccess}
|
||||||
options={options}
|
options={options}
|
||||||
renderMode="canvas"
|
renderMode="canvas"
|
||||||
className="absolute w-auto mx-auto left-0 right-0"
|
className="absolute left-0 right-0 mx-auto w-auto">
|
||||||
>
|
|
||||||
{Array.from({ length: numPages }, (_, index) => (
|
{Array.from({ length: numPages }, (_, index) => (
|
||||||
<Fragment key={short.generate().toString()}>
|
<Fragment key={short.generate().toString()}>
|
||||||
<div
|
<div
|
||||||
@@ -56,8 +55,7 @@ export default function PDFViewer(props) {
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
...props.style,
|
...props.style,
|
||||||
}}
|
}}
|
||||||
className="mx-auto w-fit"
|
className="mx-auto w-fit">
|
||||||
>
|
|
||||||
<Page
|
<Page
|
||||||
className="mt-5"
|
className="mt-5"
|
||||||
key={`page_${index + 1}`}
|
key={`page_${index + 1}`}
|
||||||
@@ -68,28 +66,29 @@ export default function PDFViewer(props) {
|
|||||||
if (e.height) setPageHeight(e.height);
|
if (e.height) setPageHeight(e.height);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}}
|
}}
|
||||||
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}></EditableField>
|
||||||
></EditableField>
|
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { Fragment, useEffect, useState } from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
|
import { classNames } from "@documenso/lib";
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline";
|
||||||
import { classNames } from "@documenso/lib";
|
|
||||||
const stc = require("string-to-color");
|
const stc = require("string-to-color");
|
||||||
|
|
||||||
export default function RecipientSelector(props: any) {
|
export default function RecipientSelector(props: any) {
|
||||||
const [selectedRecipient, setSelectedRecipient]: any = useState(
|
const [selectedRecipient, setSelectedRecipient]: any = useState(props?.recipients[0]);
|
||||||
props?.recipients[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
props.onChange(selectedRecipient);
|
props.onChange(selectedRecipient);
|
||||||
@@ -18,11 +17,10 @@ export default function RecipientSelector(props: any) {
|
|||||||
value={selectedRecipient}
|
value={selectedRecipient}
|
||||||
onChange={(e: any) => {
|
onChange={(e: any) => {
|
||||||
setSelectedRecipient(e);
|
setSelectedRecipient(e);
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div className="relative mt-1 mb-2">
|
<div className="relative mt-1 mb-2">
|
||||||
<Listbox.Button className="select-none relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:border-neon focus:outline-none focus:ring-1 focus:ring-neon sm:text-sm">
|
<Listbox.Button className="focus:border-neon focus:ring-neon relative w-full cursor-default select-none rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-1 sm:text-sm">
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<span
|
<span
|
||||||
className="inline-block h-4 w-4 flex-shrink-0 rounded-full"
|
className="inline-block h-4 w-4 flex-shrink-0 rounded-full"
|
||||||
@@ -33,10 +31,7 @@ export default function RecipientSelector(props: any) {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<ChevronUpDownIcon
|
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
className="h-5 w-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
|
|
||||||
@@ -45,20 +40,19 @@ export default function RecipientSelector(props: any) {
|
|||||||
as={Fragment}
|
as={Fragment}
|
||||||
leave="transition ease-in duration-100"
|
leave="transition ease-in duration-100"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0">
|
||||||
>
|
|
||||||
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||||
{props?.recipients.map((recipient: any) => (
|
{props?.recipients.map((recipient: any) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={recipient?.id}
|
key={recipient?.id}
|
||||||
|
disabled={!recipient?.email}
|
||||||
className={({ active }) =>
|
className={({ active }) =>
|
||||||
classNames(
|
classNames(
|
||||||
active ? "text-white bg-neon-dark" : "text-gray-900",
|
active ? "bg-neon-dark text-white" : "text-gray-900",
|
||||||
"relative cursor-default select-none py-2 pl-3 pr-9"
|
"relative cursor-default select-none py-2 pl-3 pr-9 aria-disabled:opacity-50 aria-disabled:cursor-not-allowed"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
value={recipient}
|
value={recipient}>
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
{({ selected, active }) => (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -72,9 +66,8 @@ export default function RecipientSelector(props: any) {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
selected ? "font-semibold" : "font-normal",
|
selected ? "font-semibold" : "font-normal",
|
||||||
"ml-3 block truncate"
|
"ml-3 block truncate"
|
||||||
)}
|
)}>
|
||||||
>
|
{`${recipient?.name} <${recipient?.email || 'unknown'}>`}
|
||||||
{`${selectedRecipient?.name} <${selectedRecipient?.email}>`}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,9 +76,8 @@ export default function RecipientSelector(props: any) {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-neon-dark",
|
active ? "text-white" : "text-neon-dark",
|
||||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||||
)}
|
)}>
|
||||||
>
|
<CheckIcon className="h-5 w-5" strokeWidth={3} aria-hidden="true" />
|
||||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Draggable from "react-draggable";
|
import { classNames } from "@documenso/lib";
|
||||||
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 Draggable from "react-draggable";
|
||||||
|
|
||||||
const stc = require("string-to-color");
|
const stc = require("string-to-color");
|
||||||
|
|
||||||
type FieldPropsType = {
|
type FieldPropsType = {
|
||||||
@@ -34,27 +36,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(
|
||||||
|
"absolute top-0 left-0 m-auto h-16 w-48 select-none flex-row-reverse text-center text-lg font-bold opacity-80",
|
||||||
|
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="my-4 font-medium">
|
||||||
<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}
|
||||||
className="font-qwigley text-5xl m-auto w-auto flex-row-reverse font-medium text-center"
|
className="font-qwigley m-auto w-auto flex-row-reverse text-center text-5xl font-medium">
|
||||||
>
|
|
||||||
{field?.signature?.type === "type" ? (
|
{field?.signature?.type === "type" ? (
|
||||||
<div className="my-4">{field?.signature.typedSignature}</div>
|
<div className="my-4">{field?.signature.typedSignature}</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -62,7 +65,7 @@ export default function SignableField(props: FieldPropsType) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field?.signature?.type === "draw" ? (
|
{field?.signature?.type === "draw" ? (
|
||||||
<img className="w-48 h-16" src={field?.signature?.signatureImage} />
|
<img className="h-16 w-48" src={field?.signature?.signatureImage} />
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import { classNames } from "@documenso/lib";
|
import { classNames } from "@documenso/lib";
|
||||||
|
import { localStorage } from "@documenso/lib";
|
||||||
import { Button, IconButton } from "@documenso/ui";
|
import { Button, IconButton } from "@documenso/ui";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import {
|
import { LanguageIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||||
LanguageIcon,
|
|
||||||
PencilIcon,
|
|
||||||
TrashIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import { Fragment, useEffect, useState } from "react";
|
|
||||||
import SignatureCanvas from "react-signature-canvas";
|
import SignatureCanvas from "react-signature-canvas";
|
||||||
import { localStorage } from "@documenso/lib";
|
import { useDebouncedValue } from "../../hooks/use-debounced-value";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: "Type", icon: LanguageIcon, current: true },
|
{ name: "Type", icon: LanguageIcon, current: true },
|
||||||
@@ -19,6 +16,9 @@ export default function SignatureDialog(props: any) {
|
|||||||
const [currentTab, setCurrentTab] = useState(tabs[0]);
|
const [currentTab, setCurrentTab] = useState(tabs[0]);
|
||||||
const [typedSignature, setTypedSignature] = useState("");
|
const [typedSignature, setTypedSignature] = useState("");
|
||||||
const [signatureEmpty, setSignatureEmpty] = useState(true);
|
const [signatureEmpty, setSignatureEmpty] = useState(true);
|
||||||
|
// This is a workaround to prevent the canvas from being rendered when the dialog is closed
|
||||||
|
// we also need the debounce to avoid rendering while transitions are occuring.
|
||||||
|
const showCanvas = useDebouncedValue<boolean>(props.open, 1);
|
||||||
let signCanvasRef: any | undefined;
|
let signCanvasRef: any | undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,8 +34,7 @@ export default function SignatureDialog(props: any) {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
props.setOpen(false);
|
props.setOpen(false);
|
||||||
setCurrent(tabs[0]);
|
setCurrent(tabs[0]);
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
@@ -43,8 +42,7 @@ export default function SignatureDialog(props: any) {
|
|||||||
enterTo="opacity-100"
|
enterTo="opacity-100"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0">
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
@@ -57,11 +55,10 @@ export default function SignatureDialog(props: any) {
|
|||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||||
>
|
<Dialog.Panel className="relative min-h-[350px] 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-sm sm:p-6">
|
||||||
<Dialog.Panel className="min-h-[350px] 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-sm sm:p-6">
|
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="border-b border-gray-200 mb-3">
|
<div className="mb-3 border-b border-gray-200">
|
||||||
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<a
|
<a
|
||||||
@@ -72,11 +69,10 @@ export default function SignatureDialog(props: any) {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
tab.current
|
tab.current
|
||||||
? "border-neon text-neon"
|
? "border-neon text-neon"
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
|
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
||||||
"group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm cursor-pointer"
|
"group inline-flex cursor-pointer items-center border-b-2 py-4 px-1 text-sm font-medium"
|
||||||
)}
|
)}
|
||||||
aria-current={tab.current ? "page" : undefined}
|
aria-current={tab.current ? "page" : undefined}>
|
||||||
>
|
|
||||||
<tab.icon
|
<tab.icon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
tab.current
|
tab.current
|
||||||
@@ -93,7 +89,7 @@ export default function SignatureDialog(props: any) {
|
|||||||
</div>
|
</div>
|
||||||
{isCurrentTab("Type") ? (
|
{isCurrentTab("Type") ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="my-8 border-b border-gray-300 mb-3">
|
<div className="my-7 mb-3 border-b border-gray-300">
|
||||||
<input
|
<input
|
||||||
value={typedSignature}
|
value={typedSignature}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -101,36 +97,31 @@ export default function SignatureDialog(props: any) {
|
|||||||
}}
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
typedSignature ? "font-qwigley text-4xl" : "",
|
typedSignature ? "font-qwigley text-4xl" : "",
|
||||||
"leading-none h-10 align-bottom mt-14 text-center block w-full focus:border-neon focus:ring-neon text-2xl"
|
"focus:border-neon focus:ring-neon mt-14 block h-10 w-full text-center align-bottom text-2xl leading-none"
|
||||||
)}
|
)}
|
||||||
placeholder="Kindly type your name"
|
placeholder="Kindly type your name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="float-right">
|
<div className="flex flex-row-reverse items-center gap-x-3">
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onClose();
|
props.onClose();
|
||||||
props.setOpen(false);
|
props.setOpen(false);
|
||||||
setCurrent(tabs[0]);
|
setCurrent(tabs[0]);
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="ml-3"
|
className="ml-3"
|
||||||
disabled={!typedSignature}
|
disabled={!typedSignature}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
localStorage.setItem(
|
localStorage.setItem("typedSignature", typedSignature);
|
||||||
"typedSignature",
|
|
||||||
typedSignature
|
|
||||||
);
|
|
||||||
props.onClose({
|
props.onClose({
|
||||||
type: "type",
|
type: "type",
|
||||||
typedSignature: typedSignature,
|
typedSignature: typedSignature,
|
||||||
});
|
});
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
Sign
|
Sign
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,52 +130,55 @@ export default function SignatureDialog(props: any) {
|
|||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
{isCurrentTab("Draw") ? (
|
{isCurrentTab("Draw") ? (
|
||||||
<div className="">
|
<div className="" key={props.open ? "closed" : "open"}>
|
||||||
<SignatureCanvas
|
{showCanvas && (
|
||||||
ref={(ref) => {
|
<SignatureCanvas
|
||||||
signCanvasRef = ref;
|
ref={(ref) => {
|
||||||
}}
|
signCanvasRef = ref;
|
||||||
canvasProps={{
|
|
||||||
className:
|
|
||||||
"sigCanvas border-b b-2 border-slate w-full h-full mb-3",
|
|
||||||
}}
|
|
||||||
clearOnResize={true}
|
|
||||||
onEnd={() => {
|
|
||||||
setSignatureEmpty(signCanvasRef?.isEmpty());
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
className="block float-left"
|
|
||||||
icon={TrashIcon}
|
|
||||||
onClick={() => {
|
|
||||||
signCanvasRef?.clear();
|
|
||||||
setSignatureEmpty(signCanvasRef?.isEmpty());
|
|
||||||
}}
|
|
||||||
></IconButton>
|
|
||||||
<div className="mt-10 float-right">
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
props.onClose();
|
|
||||||
props.setOpen(false);
|
|
||||||
setCurrent(tabs[0]);
|
|
||||||
}}
|
}}
|
||||||
>
|
canvasProps={{
|
||||||
Cancel
|
className: "sigCanvas border-b b-2 border-slate w-full h-full mb-3",
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="ml-3"
|
|
||||||
onClick={() => {
|
|
||||||
props.onClose({
|
|
||||||
type: "draw",
|
|
||||||
signatureImage:
|
|
||||||
signCanvasRef.toDataURL("image/png"),
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
disabled={signatureEmpty}
|
clearOnResize={true}
|
||||||
>
|
onEnd={() => {
|
||||||
Sign
|
setSignatureEmpty(signCanvasRef?.isEmpty());
|
||||||
</Button>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<IconButton
|
||||||
|
className="block"
|
||||||
|
icon={TrashIcon}
|
||||||
|
onClick={() => {
|
||||||
|
signCanvasRef?.clear();
|
||||||
|
setSignatureEmpty(signCanvasRef?.isEmpty());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-row-reverse items-center gap-x-3">
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
props.onClose();
|
||||||
|
props.setOpen(false);
|
||||||
|
setCurrent(tabs[0]);
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="ml-3"
|
||||||
|
onClick={() => {
|
||||||
|
props.onClose({
|
||||||
|
type: "draw",
|
||||||
|
signatureImage: signCanvasRef.toDataURL("image/png"),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={signatureEmpty}>
|
||||||
|
Sign
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -200,11 +194,11 @@ export default function SignatureDialog(props: any) {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
function isCurrentTab(tabName: string): boolean {
|
function isCurrentTab(tabName: string): boolean {
|
||||||
return currentTab.name === tabName;
|
return currentTab.name === tabName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCurrent(t: any) {
|
function setCurrent(t: any) {
|
||||||
tabs.forEach((tab) => {
|
tabs.forEach((tab) => {
|
||||||
tab.current = tab.name === t.name;
|
tab.current = tab.name === t.name;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
||||||
|
import { useSubscription } from "@documenso/lib/stripe";
|
||||||
import Navigation from "./navigation";
|
import Navigation from "./navigation";
|
||||||
|
import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { SubscriptionStatus } from "@prisma/client";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { BillingWarning } from "./billing-warning";
|
||||||
|
|
||||||
function useRedirectToLoginIfUnauthenticated() {
|
function useRedirectToLoginIfUnauthenticated() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
@@ -31,11 +35,16 @@ function useRedirectToLoginIfUnauthenticated() {
|
|||||||
export default function Layout({ children }: any) {
|
export default function Layout({ children }: any) {
|
||||||
useRedirectToLoginIfUnauthenticated();
|
useRedirectToLoginIfUnauthenticated();
|
||||||
|
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="min-h-full">
|
<div className="min-h-full">
|
||||||
<Navigation></Navigation>
|
<Navigation />
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
<BillingWarning />
|
||||||
|
|
||||||
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">{children}</div>
|
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">{children}</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { LockClosedIcon } from "@heroicons/react/20/solid";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import Logo from "./logo";
|
|
||||||
import { signIn } from "next-auth/react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
||||||
import { Button } from "@documenso/ui";
|
import { Button } from "@documenso/ui";
|
||||||
|
import Logo from "./logo";
|
||||||
|
import { LockClosedIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { signIn } from "next-auth/react";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
interface LoginValues {
|
interface LoginValues {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -17,16 +16,12 @@ 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 = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "";
|
||||||
let callbackUrl =
|
|
||||||
typeof router.query?.callbackUrl === "string"
|
|
||||||
? router.query.callbackUrl
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// If not absolute URL, make it absolute
|
// If not absolute URL, make it absolute
|
||||||
if (!/^https?:\/\//.test(callbackUrl)) {
|
if (!/^https?:\/\//.test(callbackUrl)) {
|
||||||
@@ -80,10 +75,7 @@ export default function Login() {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form
|
<form className="mt-8 space-y-6" onSubmit={methods.handleSubmit(onSubmit)}>
|
||||||
className="mt-8 space-y-6"
|
|
||||||
onSubmit={methods.handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="remember" defaultValue="true" />
|
<input type="hidden" name="remember" defaultValue="true" />
|
||||||
<div className="-space-y-px rounded-md shadow-sm">
|
<div className="-space-y-px rounded-md shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
@@ -97,7 +89,7 @@ export default function Login() {
|
|||||||
type="email"
|
type="email"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
required
|
required
|
||||||
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
|
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,29 +104,26 @@ export default function Login() {
|
|||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
|
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
</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="text-gray-500 hover:text-neon-700 font-medium">
|
||||||
Forgot your password?
|
Forgot your password?
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={formState.isSubmitting}
|
disabled={formState.isSubmitting}
|
||||||
className="group relative flex w-full"
|
className="group relative flex w-full">
|
||||||
>
|
|
||||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
|
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
<LockClosedIcon
|
<LockClosedIcon
|
||||||
className="h-5 w-5 text-neon-dark group-hover:text-neon disabled:group-hover:bg-gray-600 disabled:disabled:bg-gray-600"
|
className="text-neon-700 group-hover:text-neon-dark-700 h-5 w-5 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600 duration-200"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -143,24 +132,29 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
className="absolute inset-0 flex items-center"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div className="w-full border-t border-gray-300" />
|
<div className="w-full border-t border-gray-300" />
|
||||||
</div>
|
</div>
|
||||||
<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 href="/signup" className="text-gray-500 hover:text-neon-700 duration-200 font-medium">
|
||||||
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="text-neon hover:text-neon font-medium">
|
||||||
|
Hosted Documenso will be available soon™
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
import { classNames } from "@documenso/lib";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { classNames } from "@documenso/lib";
|
||||||
|
|
||||||
export default function Logo(props: any) {
|
export default function Logo(props: any) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link href="/dashboard">
|
<Link href="/dashboard">
|
||||||
<svg
|
<svg className="w-12" viewBox="0 0 88.6758041381836 32.18000030517578" {...props}>
|
||||||
className="w-12"
|
<rect width="88.6758041381836" height="32.18000030517578" fill="transparent"></rect>
|
||||||
viewBox="0 0 88.6758041381836 32.18000030517578"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<rect
|
|
||||||
width="88.6758041381836"
|
|
||||||
height="32.18000030517578"
|
|
||||||
fill="transparent"
|
|
||||||
></rect>
|
|
||||||
<g transform="matrix(1,0,0,1,-25.98720359802246,-66.41200256347656)">
|
<g transform="matrix(1,0,0,1,-25.98720359802246,-66.41200256347656)">
|
||||||
<path
|
<path
|
||||||
d="M99.004,68.26l-.845-1.848-23.226,2.299c-1.437,.142-2.876,.089-4.3-.156-2.949-.51-6.32-1.717-9.073-2.099-1.477-.206-2.934,.448-3.789,1.67-1.174,1.678-2.308,3.888-3.501,5.622-1.518,2.207-3.032,4.418-4.531,6.638-1.693,2.499-3.365,5.013-5.061,7.51-.103,.153-.333,.221-.503,.329-.079-.279-.306-.636-.206-.824,1.006-1.928,1.845-4,3.165-5.694,1.908-2.449,2.914-5.445,5.007-7.745,.342-.777,.845-1.466,1.151-2.244,.915-2.341-1.295-5.305-2.935-5.305h-.613c-1.196,0-2.526,.357-4.092,1.716-.986,.856-2.391,2.432-2.97,3.326-3.424,5.286-7.177,10.382-10.15,15.932-.365,.682-1.719,3.722-2.013,3.606-.214-.077-.159-.458-.041-.823,1.312-4.065,4.163-9.851,5.843-13.777,.41-.962,.635-1.516-.305-2.361-1.016-.913-2.669,.084-3.084,1.052-1.784,4.174-2.631,8.08-4.038,12.396-.466,1.43-.916,2.865-1.386,4.294-.273,.831-.548,1.661-.836,2.488-.112,.321-.226,.642-.345,.962-.032,.082-.064,.165-.095,.248-.006,.011-.003,.002-.009,.017-.005,.008-.003,.015-.006,.023-.011,.026-.021,.05-.03,.076-.024,.067-.012,.044,.009-.002-.691,1.577,.417,3.002,2.091,3.002,.808,0,1.552-1.431,1.943-2.055,1.224-1.957,3.28-5.125,4.36-7.163,.894-1.69,2.078-3.14,3.209-4.615,1.857-2.42,3.065-5.242,5.025-7.575,.33-.394,.694-.772,1.093-1.093,.121-.098,.473-.05,.618,.062,.124,.098,.2,.432,.127,.577-.397,.788-.863,1.542-1.278,2.322-1.481,2.79-2.953,5.582-4.425,8.377-1.16,2.204-2.659,5.055-3.551,6.755-.438,.833-.176,1.857,.604,2.382,0,0-.383,2.028,1.908,2.028,1.896,0,2.711-1.145,3.053-1.624,.348-.486,.748-.951,1.202-1.334,.151-.13,.563-.018,.821,.079,.076,.029,.085,.406,.03,.582-.398,1.272-.323,2.283,1.879,2.283,.784,0,2.218-1.283,2.904-2.213,.794-1.075,.731-1.1,1.415-2.244,1.678-2.821,3.132-5.055,4.751-7.91,1.083-1.91,2.175-3.854,3.294-5.516,.685-1.015,1.446-1.252,2.188-1.252s.409,.686,.336,1.025c-.071,.341-.677,1.545-2.531,4.35-1.115,1.687-2.244,3.367-3.308,5.084-1.075,1.736-2.141,3.48-3.205,5.225-.88,1.445,.162,3.467,1.854,3.467h2.765c2.653,0,5.302-.397,7.916-.851l7.41-1.287c1.421-.245,2.868-.298,4.303-.158l23.161,2.296,.845-1.849c4.61-9.858,15.211-11.322,15.66-11.38v-5.717c-.448-.058-11.05-1.522-15.66-11.381Zm-40.143,6.165c-.164,.465-.491,.922-.86,1.255-.918,.828-1.451,1.842-1.911,2.977-.512,1.269-1.829,2.119-1.966,3.65-.027,.3-.627,.577-1.006,.797-.109,.062-.357-.114-.524-.174,.015-.229-.024-.401,.039-.515,.848-1.495,1.678-3.002,2.584-4.462,.775-1.254,1.642-2.453,2.469-3.677,.085-.126,.158-.273,.27-.365,.457-.368,.93-.719,1.399-1.075,.497,.721-.312,1.071-.494,1.589Zm30.047,12.011c1.736,0,3.162-1.137,3.686-2.693h10.547c-3.02,1.916-6.1,4.684-8.402,8.716l-21.902-2.17v-15.584l21.902-2.167c2.302,4.032,5.382,6.802,8.402,8.714h-10.547c-.524-1.554-1.951-2.691-3.686-2.691-2.172,0-3.938,1.763-3.938,3.938s1.766,3.938,3.938,3.938Z"
|
d="M99.004,68.26l-.845-1.848-23.226,2.299c-1.437,.142-2.876,.089-4.3-.156-2.949-.51-6.32-1.717-9.073-2.099-1.477-.206-2.934,.448-3.789,1.67-1.174,1.678-2.308,3.888-3.501,5.622-1.518,2.207-3.032,4.418-4.531,6.638-1.693,2.499-3.365,5.013-5.061,7.51-.103,.153-.333,.221-.503,.329-.079-.279-.306-.636-.206-.824,1.006-1.928,1.845-4,3.165-5.694,1.908-2.449,2.914-5.445,5.007-7.745,.342-.777,.845-1.466,1.151-2.244,.915-2.341-1.295-5.305-2.935-5.305h-.613c-1.196,0-2.526,.357-4.092,1.716-.986,.856-2.391,2.432-2.97,3.326-3.424,5.286-7.177,10.382-10.15,15.932-.365,.682-1.719,3.722-2.013,3.606-.214-.077-.159-.458-.041-.823,1.312-4.065,4.163-9.851,5.843-13.777,.41-.962,.635-1.516-.305-2.361-1.016-.913-2.669,.084-3.084,1.052-1.784,4.174-2.631,8.08-4.038,12.396-.466,1.43-.916,2.865-1.386,4.294-.273,.831-.548,1.661-.836,2.488-.112,.321-.226,.642-.345,.962-.032,.082-.064,.165-.095,.248-.006,.011-.003,.002-.009,.017-.005,.008-.003,.015-.006,.023-.011,.026-.021,.05-.03,.076-.024,.067-.012,.044,.009-.002-.691,1.577,.417,3.002,2.091,3.002,.808,0,1.552-1.431,1.943-2.055,1.224-1.957,3.28-5.125,4.36-7.163,.894-1.69,2.078-3.14,3.209-4.615,1.857-2.42,3.065-5.242,5.025-7.575,.33-.394,.694-.772,1.093-1.093,.121-.098,.473-.05,.618,.062,.124,.098,.2,.432,.127,.577-.397,.788-.863,1.542-1.278,2.322-1.481,2.79-2.953,5.582-4.425,8.377-1.16,2.204-2.659,5.055-3.551,6.755-.438,.833-.176,1.857,.604,2.382,0,0-.383,2.028,1.908,2.028,1.896,0,2.711-1.145,3.053-1.624,.348-.486,.748-.951,1.202-1.334,.151-.13,.563-.018,.821,.079,.076,.029,.085,.406,.03,.582-.398,1.272-.323,2.283,1.879,2.283,.784,0,2.218-1.283,2.904-2.213,.794-1.075,.731-1.1,1.415-2.244,1.678-2.821,3.132-5.055,4.751-7.91,1.083-1.91,2.175-3.854,3.294-5.516,.685-1.015,1.446-1.252,2.188-1.252s.409,.686,.336,1.025c-.071,.341-.677,1.545-2.531,4.35-1.115,1.687-2.244,3.367-3.308,5.084-1.075,1.736-2.141,3.48-3.205,5.225-.88,1.445,.162,3.467,1.854,3.467h2.765c2.653,0,5.302-.397,7.916-.851l7.41-1.287c1.421-.245,2.868-.298,4.303-.158l23.161,2.296,.845-1.849c4.61-9.858,15.211-11.322,15.66-11.38v-5.717c-.448-.058-11.05-1.522-15.66-11.381Zm-40.143,6.165c-.164,.465-.491,.922-.86,1.255-.918,.828-1.451,1.842-1.911,2.977-.512,1.269-1.829,2.119-1.966,3.65-.027,.3-.627,.577-1.006,.797-.109,.062-.357-.114-.524-.174,.015-.229-.024-.401,.039-.515,.848-1.495,1.678-3.002,2.584-4.462,.775-1.254,1.642-2.453,2.469-3.677,.085-.126,.158-.273,.27-.365,.457-.368,.93-.719,1.399-1.075,.497,.721-.312,1.071-.494,1.589Zm30.047,12.011c1.736,0,3.162-1.137,3.686-2.693h10.547c-3.02,1.916-6.1,4.684-8.402,8.716l-21.902-2.17v-15.584l21.902-2.167c2.302,4.032,5.382,6.802,8.402,8.714h-10.547c-.524-1.554-1.951-2.691-3.686-2.691-2.172,0-3.938,1.763-3.938,3.938s1.766,3.938,3.938,3.938Z"
|
||||||
className={classNames(props.dark ? "fill-white" : "fill-brown")}
|
className={classNames(props.dark ? "fill-white" : "fill-brown")}></path>
|
||||||
></path>
|
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
import { Fragment, useEffect, useState } from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { getUser } from "@documenso/lib/api";
|
||||||
import avatarFromInitials from "avatar-from-initials";
|
import Logo from "./logo";
|
||||||
import { toast } from "react-hot-toast";
|
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ArrowRightOnRectangleIcon,
|
||||||
Bars3Icon,
|
Bars3Icon,
|
||||||
BellIcon,
|
BellIcon,
|
||||||
XMarkIcon,
|
|
||||||
UserCircleIcon,
|
|
||||||
ArrowRightOnRectangleIcon,
|
|
||||||
DocumentTextIcon,
|
|
||||||
ChartBarIcon,
|
ChartBarIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
UserCircleIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
|
XMarkIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import Logo from "./logo";
|
import avatarFromInitials from "avatar-from-initials";
|
||||||
import { getUser } from "@documenso/lib/api";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{
|
{
|
||||||
@@ -34,13 +33,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 +99,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">
|
||||||
@@ -118,14 +124,12 @@ export default function TopNavigation() {
|
|||||||
item.current
|
item.current
|
||||||
? "border-neon text-brown"
|
? "border-neon text-brown"
|
||||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
||||||
"inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
"inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium"
|
||||||
)}
|
)}
|
||||||
aria-current={item.current ? "page" : undefined}
|
aria-current={item.current ? "page" : undefined}>
|
||||||
>
|
|
||||||
<item.icon
|
<item.icon
|
||||||
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
|
className="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
|
||||||
aria-hidden="true"
|
aria-hidden="true"></item.icon>
|
||||||
></item.icon>
|
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@@ -135,8 +139,7 @@ export default function TopNavigation() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
document?.getElementById("mb")?.click();
|
document?.getElementById("mb")?.click();
|
||||||
}}
|
}}
|
||||||
className="hidden sm:ml-6 sm:flex sm:items-center hover:bg-gray-200 px-3 cursor-pointer"
|
className="hidden cursor-pointer px-3 hover:bg-gray-200 sm:ml-6 sm:flex sm:items-center">
|
||||||
>
|
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
<p className="font-bold">{user?.name || ""}</p>
|
<p className="font-bold">{user?.name || ""}</p>
|
||||||
<p>{user?.email}</p>
|
<p>{user?.email}</p>
|
||||||
@@ -145,16 +148,12 @@ export default function TopNavigation() {
|
|||||||
<div>
|
<div>
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
id="mb"
|
id="mb"
|
||||||
className="flex max-w-xs items-center rounded-full bg-white text-sm"
|
className="flex max-w-xs items-center rounded-full bg-white text-sm">
|
||||||
>
|
|
||||||
<span className="sr-only">Open user menu</span>
|
<span className="sr-only">Open user menu</span>
|
||||||
<div
|
<div
|
||||||
key={user?.email}
|
key={user?.email}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: avatarFromInitials(
|
__html: avatarFromInitials(user?.name || "" || "", 40),
|
||||||
user?.name || "" || "",
|
|
||||||
40
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
@@ -166,8 +165,7 @@ export default function TopNavigation() {
|
|||||||
enterTo="transform opacity-100 scale-100"
|
enterTo="transform opacity-100 scale-100"
|
||||||
leave="transition ease-in duration-75"
|
leave="transition ease-in duration-75"
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95">
|
||||||
>
|
|
||||||
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
{userNavigation.map((item) => (
|
{userNavigation.map((item) => (
|
||||||
<Menu.Item key={item.name}>
|
<Menu.Item key={item.name}>
|
||||||
@@ -178,12 +176,10 @@ export default function TopNavigation() {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "bg-gray-100" : "",
|
active ? "bg-gray-100" : "",
|
||||||
"block px-4 py-2 text-sm text-gray-700"
|
"block px-4 py-2 text-sm text-gray-700"
|
||||||
)}
|
)}>
|
||||||
>
|
|
||||||
<item.icon
|
<item.icon
|
||||||
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
|
className="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
|
||||||
aria-hidden="true"
|
aria-hidden="true"></item.icon>
|
||||||
></item.icon>
|
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
@@ -215,12 +211,14 @@ export default function TopNavigation() {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
item.current
|
item.current
|
||||||
? "bg-teal-50 border-teal-500 text-teal-700"
|
? "border-teal-500 bg-teal-50 text-teal-700"
|
||||||
: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800",
|
: "border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800",
|
||||||
"block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
|
"block border-l-4 py-2 pl-3 pr-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,22 +234,23 @@ 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">
|
||||||
>
|
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import Link from "next/link";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useSession } from "next-auth/react";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { updateUser } from "@documenso/features";
|
import { updateUser } from "@documenso/features";
|
||||||
import { Button } from "@documenso/ui";
|
|
||||||
import { getUser } from "@documenso/lib/api";
|
import { getUser } from "@documenso/lib/api";
|
||||||
|
import { fetchPortalSession, isSubscriptionsEnabled, useSubscription } from "@documenso/lib/stripe";
|
||||||
|
import { Button } from "@documenso/ui";
|
||||||
|
import { BillingPlans } from "./billing-plans";
|
||||||
|
import { CreditCardIcon, KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { SubscriptionStatus } from "@prisma/client";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
const subNavigation = [
|
const subNavigation = [
|
||||||
{
|
{
|
||||||
@@ -20,20 +23,29 @@ const subNavigation = [
|
|||||||
href: "/settings/password",
|
href: "/settings/password",
|
||||||
icon: KeyIcon,
|
icon: KeyIcon,
|
||||||
current: false,
|
current: false,
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true") {
|
||||||
|
subNavigation.push({
|
||||||
|
name: "Billing",
|
||||||
|
href: "/settings/billing",
|
||||||
|
icon: CreditCardIcon,
|
||||||
|
current: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function classNames(...classes: any) {
|
function classNames(...classes: any) {
|
||||||
return classes.filter(Boolean).join(" ");
|
return classes.filter(Boolean).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Setttings() {
|
export default function Setttings() {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
const { subscription, hasSubscription } = useSubscription();
|
||||||
const [user, setUser] = useState({
|
const [user, setUser] = useState({
|
||||||
email: "",
|
email: "",
|
||||||
name: "",
|
name: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUser().then((res: any) => {
|
getUser().then((res: any) => {
|
||||||
res.json().then((j: any) => {
|
res.json().then((j: any) => {
|
||||||
@@ -74,15 +86,12 @@ export default function Setttings() {
|
|||||||
</Head>
|
</Head>
|
||||||
<header className="py-10">
|
<header className="py-10">
|
||||||
<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">
|
||||||
<h1 className="text-3xl font-bold leading-tight tracking-tight text-brown">
|
<h1 className="text-brown text-3xl font-bold leading-tight tracking-tight">Settings</h1>
|
||||||
Settings
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
className="mx-auto max-w-screen-xl px-4 pb-6 sm:px-6 lg:px-8 lg:pb-16"
|
className="mx-auto max-w-screen-xl px-4 pb-6 sm:px-6 lg:px-8 lg:pb-16"
|
||||||
hidden={!user.email}
|
hidden={!user.email}>
|
||||||
>
|
|
||||||
<div className="overflow-hidden rounded-lg bg-white shadow">
|
<div className="overflow-hidden rounded-lg bg-white shadow">
|
||||||
<div className="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
|
<div className="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
|
||||||
<aside className="py-6 lg:col-span-3">
|
<aside className="py-6 lg:col-span-3">
|
||||||
@@ -93,18 +102,17 @@ export default function Setttings() {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
item.current
|
item.current
|
||||||
? "bg-teal-50 border-neon-dark text-teal-700 hover:bg-teal-50 hover:text-teal-700"
|
? "border-neon-dark bg-teal-50 text-teal-700 hover:bg-teal-50 hover:text-teal-700"
|
||||||
: "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900",
|
: "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900",
|
||||||
"group border-l-4 px-3 py-2 flex items-center text-sm font-medium"
|
"group flex items-center border-l-4 px-3 py-2 text-sm font-medium"
|
||||||
)}
|
)}
|
||||||
aria-current={item.current ? "page" : undefined}
|
aria-current={item.current ? "page" : undefined}>
|
||||||
>
|
|
||||||
<item.icon
|
<item.icon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
item.current
|
item.current
|
||||||
? "text-teal-500 group-hover:text-teal-500"
|
? "text-teal-500 group-hover:text-teal-500"
|
||||||
: "text-gray-400 group-hover:text-gray-500",
|
: "text-gray-400 group-hover:text-gray-500",
|
||||||
"flex-shrink-0 -ml-1 mr-3 h-6 w-6"
|
"-ml-1 mr-3 h-6 w-6 flex-shrink-0"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
@@ -115,16 +123,14 @@ export default function Setttings() {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
className="divide-y divide-gray-200 lg:col-span-9"
|
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9"
|
||||||
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">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-medium leading-6 text-gray-900">
|
<h2 className="text-lg font-medium leading-6 text-gray-900">Profile</h2>
|
||||||
Profile
|
|
||||||
</h2>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
Let people know who they are dealing with builds trust.
|
Let people know who they are dealing with builds trust.
|
||||||
</p>
|
</p>
|
||||||
@@ -132,10 +138,7 @@ export default function Setttings() {
|
|||||||
|
|
||||||
<div className="my-6 grid grid-cols-12 gap-6">
|
<div className="my-6 grid grid-cols-12 gap-6">
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="col-span-12 sm:col-span-6">
|
||||||
<label
|
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
|
||||||
htmlFor="first-name"
|
|
||||||
className="block text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
Full Name
|
Full Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -146,14 +149,11 @@ export default function Setttings() {
|
|||||||
onChange={(e) => handleNameChange(e)}
|
onChange={(e) => handleNameChange(e)}
|
||||||
onKeyDown={handleKeyPress}
|
onKeyDown={handleKeyPress}
|
||||||
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="focus:border-neon focus:ring-neon mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:outline-none sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="col-span-12 sm:col-span-6">
|
||||||
<label
|
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
|
||||||
htmlFor="first-name"
|
|
||||||
className="block text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -163,16 +163,93 @@ 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="focus:border-neon focus:ring-neon mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:outline-none disabled:bg-neutral-100 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="min-h-[251px] divide-y divide-gray-200 lg:col-span-9">
|
||||||
|
{/* 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
|
||||||
|
hidden={!subNavigation.at(2) || subNavigation.find((e) => e.current)?.name !== subNavigation.at(2)?.name}
|
||||||
|
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9">
|
||||||
|
{/* Billing 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">Billing</h2>
|
||||||
|
|
||||||
|
{!isSubscriptionsEnabled() && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500">
|
||||||
|
Subscriptions are not enabled on this instance, you have nothing to do here.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSubscriptionsEnabled() && (
|
||||||
|
<>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Your subscription is currently{" "}
|
||||||
|
<strong>
|
||||||
|
{subscription?.status &&
|
||||||
|
subscription?.status !== SubscriptionStatus.INACTIVE
|
||||||
|
? "Active"
|
||||||
|
: "Inactive"}
|
||||||
|
</strong>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{subscription?.status === SubscriptionStatus.PAST_DUE && (
|
||||||
|
<p className="mt-1 text-sm text-red-500">
|
||||||
|
Your subscription is past due. Please update your payment details to
|
||||||
|
continue using the service without interruption.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2">
|
||||||
|
<BillingPlans />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{subscription && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (isSubscriptionsEnabled() && subscription?.customerId) {
|
||||||
|
fetchPortalSession({
|
||||||
|
id: subscription.customerId,
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.success) {
|
||||||
|
window.location.href = res.url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Manage my subscription
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-10 max-w-[1100px]" hidden={!!user.email}>
|
<div className="mt-10 max-w-[1100px]" hidden={!!user.email}>
|
||||||
<div className="ph-item">
|
<div className="ph-item">
|
||||||
<div className="ph-col-12">
|
<div className="ph-col-12">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import { signup } from "@documenso/lib/api";
|
import { signup } from "@documenso/lib/api";
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
||||||
import { Button } from "@documenso/ui";
|
import { Button } from "@documenso/ui";
|
||||||
import { XCircleIcon } from "@heroicons/react/24/outline";
|
import { XCircleIcon } from "@heroicons/react/24/outline";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import Link from "next/link";
|
|
||||||
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
|
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
@@ -107,8 +107,7 @@ export default function Signup(props: { source: string }) {
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
stroke="rgb(17 24 39 / var(--tw-text-opacity))"
|
stroke="rgb(17 24 39 / var(--tw-text-opacity))"
|
||||||
className="w-8 h-8 inline mb-1"
|
className="mb-1 inline h-8 w-8">
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@@ -130,8 +129,7 @@ export default function Signup(props: { source: string }) {
|
|||||||
form.clearErrors();
|
form.clearErrors();
|
||||||
trigger();
|
trigger();
|
||||||
}}
|
}}
|
||||||
className="mt-8 space-y-6"
|
className="mt-8 space-y-6">
|
||||||
>
|
|
||||||
<input type="hidden" name="remember" defaultValue="true" />
|
<input type="hidden" name="remember" defaultValue="true" />
|
||||||
<div className="-space-y-px rounded-md shadow-sm">
|
<div className="-space-y-px rounded-md shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
@@ -145,7 +143,7 @@ export default function Signup(props: { source: string }) {
|
|||||||
type="email"
|
type="email"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
required
|
required
|
||||||
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
|
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,8 +155,7 @@ export default function Signup(props: { source: string }) {
|
|||||||
{...register("password", {
|
{...register("password", {
|
||||||
minLength: {
|
minLength: {
|
||||||
value: 7,
|
value: 7,
|
||||||
message:
|
message: "Your password has to be at least 7 characters long.",
|
||||||
"Your password has to be at least 7 characters long.",
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
id="password"
|
id="password"
|
||||||
@@ -166,7 +163,7 @@ export default function Signup(props: { source: string }) {
|
|||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
|
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,16 +174,12 @@ export default function Signup(props: { source: string }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.clearErrors();
|
form.clearErrors();
|
||||||
}}
|
}}
|
||||||
className="sgroup relative flex w-full"
|
className="sgroup relative flex w-full">
|
||||||
>
|
|
||||||
Create Account
|
Create Account
|
||||||
</Button>
|
</Button>
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
className="absolute inset-0 flex items-center"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div className="w-full border-t border-gray-300" />
|
<div className="w-full border-t border-gray-300" />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center"></div>
|
<div className="relative flex justify-center"></div>
|
||||||
@@ -194,10 +187,7 @@ export default function Signup(props: { source: string }) {
|
|||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-center text-sm text-gray-600">
|
<p className="mt-2 text-center text-sm text-gray-600">
|
||||||
Already have an account?{" "}
|
Already have an account?{" "}
|
||||||
<Link
|
<Link href="/login" className="text-gray-500 hover:text-neon-700 font-medium">
|
||||||
href="/login"
|
|
||||||
className="font-medium text-neon hover:text-neon"
|
|
||||||
>
|
|
||||||
Sign In
|
Sign In
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
18
apps/web/hooks/use-debounced-value.ts
Normal file
18
apps/web/hooks/use-debounced-value.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useDebouncedValue<T>(value: T, delay: number) {
|
||||||
|
// State and setters for debounced value
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
require("dotenv").config({ path: "../../.env" });
|
require("dotenv").config({ path: "../../.env" });
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
swcMinify: false,
|
swcMinify: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const withTM = require("next-transpile-modules")([
|
const transpileModules = require("next-transpile-modules")([
|
||||||
"@documenso/prisma",
|
"@documenso/prisma",
|
||||||
"@documenso/lib",
|
"@documenso/lib",
|
||||||
"@documenso/ui",
|
"@documenso/ui",
|
||||||
@@ -15,10 +15,11 @@ const withTM = require("next-transpile-modules")([
|
|||||||
"@documenso/signing",
|
"@documenso/signing",
|
||||||
"react-signature-canvas",
|
"react-signature-canvas",
|
||||||
]);
|
]);
|
||||||
const plugins = [];
|
|
||||||
plugins.push(withTM);
|
|
||||||
|
|
||||||
const moduleExports = () =>
|
const plugins = [
|
||||||
plugins.reduce((acc, next) => next(acc), nextConfig);
|
transpileModules
|
||||||
|
];
|
||||||
|
|
||||||
|
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||||
|
|
||||||
module.exports = moduleExports;
|
module.exports = moduleExports;
|
||||||
|
|||||||
@@ -7,36 +7,27 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"db-studio": "prisma db studio"
|
"db-studio": "prisma db studio",
|
||||||
|
"stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@documenso/lib": "*",
|
||||||
"@documenso/pdf": "*",
|
"@documenso/pdf": "*",
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"@headlessui/react": "^1.7.4",
|
"@headlessui/react": "^1.7.4",
|
||||||
"@heroicons/react": "^2.0.13",
|
"@heroicons/react": "^2.0.13",
|
||||||
"@pdf-lib/fontkit": "^1.1.1",
|
"@pdf-lib/fontkit": "^1.1.1",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
|
||||||
"@types/bcryptjs": "^2.4.2",
|
|
||||||
"@types/filesystem": "^0.0.32",
|
|
||||||
"@types/react-dom": "18.0.9",
|
|
||||||
"avatar-from-initials": "^1.0.3",
|
"avatar-from-initials": "^1.0.3",
|
||||||
"base64-arraybuffer": "^1.0.2",
|
"base64-arraybuffer": "^1.0.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"dotenv": "^16.0.3",
|
|
||||||
"eslint": "8.27.0",
|
|
||||||
"eslint-config-next": "13.0.3",
|
|
||||||
"file-loader": "^6.2.0",
|
|
||||||
"formidable": "^3.2.5",
|
"formidable": "^3.2.5",
|
||||||
"install": "^0.13.0",
|
"next": "13.2.4",
|
||||||
"next": "13.0.3",
|
"next-auth": "^4.22.0",
|
||||||
"next-auth": "^4.18.3",
|
|
||||||
"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",
|
||||||
"nodemailer": "^6.9.0",
|
"nodemailer": "^6.9.0",
|
||||||
"nodemailer-sendgrid": "^1.0.3",
|
"nodemailer-sendgrid": "^1.0.3",
|
||||||
"npm": "^9.1.3",
|
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"placeholder-loading": "^0.6.0",
|
"placeholder-loading": "^0.6.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
@@ -46,20 +37,30 @@
|
|||||||
"react-pdf": "^6.2.2",
|
"react-pdf": "^6.2.2",
|
||||||
"react-resizable": "^3.0.4",
|
"react-resizable": "^3.0.4",
|
||||||
"react-tooltip": "^5.7.2",
|
"react-tooltip": "^5.7.2",
|
||||||
"sass": "^1.57.1",
|
|
||||||
"short-uuid": "^4.2.2",
|
"short-uuid": "^4.2.2",
|
||||||
"string-to-color": "^2.2.2",
|
"string-to-color": "^2.2.2"
|
||||||
"typescript": "4.8.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
|
"@types/bcryptjs": "^2.4.2",
|
||||||
|
"@types/filesystem": "^0.0.32",
|
||||||
"@types/formidable": "^2.0.5",
|
"@types/formidable": "^2.0.5",
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"@types/nodemailer": "^6.4.7",
|
"@types/nodemailer": "^6.4.7",
|
||||||
"@types/nodemailer-sendgrid": "^1.0.0",
|
"@types/nodemailer-sendgrid": "^1.0.0",
|
||||||
|
"@types/react-dom": "18.0.9",
|
||||||
"@types/react-pdf": "^6.2.0",
|
"@types/react-pdf": "^6.2.0",
|
||||||
"@types/react-resizable": "^3.0.3",
|
"@types/react-resizable": "^3.0.3",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"eslint": "8.27.0",
|
||||||
|
"eslint-config-next": "13.0.3",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"next-transpile-modules": "^10.0.0",
|
||||||
"postcss": "^8.4.19",
|
"postcss": "^8.4.19",
|
||||||
"tailwindcss": "^3.2.4"
|
"sass": "^1.57.1",
|
||||||
|
"stripe-cli": "^0.1.0",
|
||||||
|
"tailwindcss": "^3.2.4",
|
||||||
|
"typescript": "4.8.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,29 @@
|
|||||||
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
|
|
||||||
|
|
||||||
import { Button } from "@documenso/ui";
|
import { Button } from "@documenso/ui";
|
||||||
import Logo from "../components/logo";
|
import Logo from "../components/logo";
|
||||||
|
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
export default function Custom404() {
|
export default function Custom404() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main className="relative min-h-full bg-gray-100 isolate">
|
<main className="relative isolate min-h-full bg-gray-100">
|
||||||
<div className="absolute top-10 left-10">
|
<div className="absolute top-10 left-10">
|
||||||
<Logo className="w-10 md:w-20" />
|
<Logo className="w-10 md:w-20" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-6 py-48 mx-auto text-center max-w-7xl sm:py-40 lg:px-8">
|
<div className="mx-auto max-w-7xl px-6 py-48 text-center sm:py-40 lg:px-8">
|
||||||
<p className="text-base font-semibold leading-8 text-brown">404</p>
|
<p className="text-brown text-base font-semibold leading-8">404</p>
|
||||||
<h1 className="mt-4 text-3xl font-bold tracking-tight text-brown sm:text-5xl">
|
<h1 className="text-brown mt-4 text-3xl font-bold tracking-tight sm:text-5xl">
|
||||||
Page not found
|
Page not found
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4 text-base text-gray-700 sm:mt-6">
|
<p className="mt-4 text-base text-gray-700 sm:mt-6">
|
||||||
Sorry, we couldn’t find the page you’re looking for.
|
Sorry, we couldn’t find the page you’re looking for.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center mt-10">
|
<div className="mt-10 flex justify-center">
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
href="/"
|
href="/"
|
||||||
icon={ArrowSmallLeftIcon}
|
icon={ArrowSmallLeftIcon}
|
||||||
className="text-base font-semibold leading-7 text-brown"
|
className="text-brown text-base font-semibold leading-7">
|
||||||
>
|
|
||||||
Back to home
|
Back to home
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
import Logo from "../components/logo";
|
|
||||||
import { Button } from "@documenso/ui";
|
import { Button } from "@documenso/ui";
|
||||||
|
import Logo from "../components/logo";
|
||||||
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
|
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
|
||||||
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
|
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
|
||||||
|
|
||||||
export default function Custom500() {
|
export default function Custom500() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative flex flex-col items-center justify-center min-h-full text-white bg-black">
|
<div className="relative flex min-h-full flex-col items-center justify-center bg-black text-white">
|
||||||
<div className="absolute top-10 left-10">
|
<div className="absolute top-10 left-10">
|
||||||
<Logo dark className="w-10 md:w-20" />
|
<Logo dark className="w-10 md:w-20" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 py-10 mt-20 max-w-7xl">
|
<div className="mt-20 max-w-7xl px-4 py-10">
|
||||||
<p className="inline-flex items-center text-3xl font-bold sm:text-5xl">
|
<p className="inline-flex items-center text-3xl font-bold sm:text-5xl">
|
||||||
500
|
500
|
||||||
<span className="relative px-3 font-thin sm:text-6xl -top-1.5">
|
<span className="relative -top-1.5 px-3 font-thin sm:text-6xl">|</span>{" "}
|
||||||
|
|
<span className="align-middle text-base font-semibold sm:text-2xl">
|
||||||
</span>{" "}
|
|
||||||
<span className="text-base font-semibold align-middle sm:text-2xl">
|
|
||||||
Something went wrong.
|
Something went wrong.
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center mt-10">
|
<div className="mt-10 flex justify-center">
|
||||||
<Button color="secondary" href="/" icon={ArrowSmallLeftIcon}>
|
<Button color="secondary" href="/" icon={ArrowSmallLeftIcon}>
|
||||||
Back to home
|
Back to home
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import "../styles/tailwind.css";
|
import { ReactElement, ReactNode } from "react";
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import type { AppProps } from "next/app";
|
||||||
|
import { SubscriptionProvider } from "@documenso/lib/stripe/providers/subscription-provider";
|
||||||
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
|
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
|
||||||
import "../../../node_modules/react-resizable/css/styles.css";
|
import "../../../node_modules/react-resizable/css/styles.css";
|
||||||
import "react-tooltip/dist/react-tooltip.css";
|
import "../styles/tailwind.css";
|
||||||
import { ReactElement, ReactNode } from "react";
|
|
||||||
import type { AppProps } from "next/app";
|
|
||||||
import { NextPage } from "next";
|
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
export { coloredConsole } from "@documenso/lib";
|
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import "react-tooltip/dist/react-tooltip.css";
|
||||||
|
|
||||||
|
export { coloredConsole } from "@documenso/lib";
|
||||||
|
|
||||||
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||||
getLayout?: (page: ReactElement) => ReactNode;
|
getLayout?: (page: ReactElement) => ReactNode;
|
||||||
@@ -19,13 +21,15 @@ type AppPropsWithLayout = AppProps & {
|
|||||||
|
|
||||||
export default function App({
|
export default function App({
|
||||||
Component,
|
Component,
|
||||||
pageProps: { session, ...pageProps },
|
pageProps: { session, initialSubscription, ...pageProps },
|
||||||
}: AppPropsWithLayout) {
|
}: AppPropsWithLayout) {
|
||||||
const getLayout = Component.getLayout || ((page: any) => page);
|
const getLayout = Component.getLayout || ((page: any) => page);
|
||||||
return (
|
return (
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session}>
|
||||||
<Toaster position="top-center"></Toaster>
|
<SubscriptionProvider initialSubscription={initialSubscription}>
|
||||||
{getLayout(<Component {...pageProps} />)}
|
<Toaster position="top-center" />
|
||||||
|
{getLayout(<Component {...pageProps} />)}
|
||||||
|
</SubscriptionProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import { Head, Html, Main, NextScript } from "next/document";
|
import { Head, Html, Main, NextScript } from "next/document";
|
||||||
import Script from "next/script";
|
|
||||||
|
|
||||||
export default function Document(props) {
|
|
||||||
let pageProps = props.__NEXT_DATA__?.props?.pageProps;
|
|
||||||
|
|
||||||
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
<Html
|
<Html className="h-full scroll-smooth bg-gray-100 font-normal antialiased" lang="en">
|
||||||
className="h-full bg-gray-100 scroll-smooth font-normal antialiased"
|
|
||||||
lang="en"
|
|
||||||
>
|
|
||||||
<Head>
|
<Head>
|
||||||
<meta name="color-scheme"></meta>
|
<meta name="color-scheme"></meta>
|
||||||
</Head>
|
</Head>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import NextAuth, { Session } from "next-auth";
|
|
||||||
import GitHubProvider from "next-auth/providers/github";
|
|
||||||
import CredentialsProvider from "next-auth/providers/credentials";
|
|
||||||
import { ErrorCode } from "@documenso/lib/auth";
|
import { ErrorCode } from "@documenso/lib/auth";
|
||||||
import prisma from "@documenso/prisma";
|
|
||||||
import { verifyPassword } from "@documenso/lib/auth";
|
import { verifyPassword } from "@documenso/lib/auth";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import NextAuth, { Session } from "next-auth";
|
||||||
|
import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
|
import GitHubProvider from "next-auth/providers/github";
|
||||||
|
|
||||||
export default NextAuth({
|
export default NextAuth({
|
||||||
secret: process.env.AUTH_SECRET,
|
secret: process.env.AUTH_SECRET,
|
||||||
@@ -27,8 +27,7 @@ export default NextAuth({
|
|||||||
password: {
|
password: {
|
||||||
label: "Password",
|
label: "Password",
|
||||||
type: "password",
|
type: "password",
|
||||||
placeholder:
|
placeholder: "Select a password. Here is some inspiration: https://xkcd.com/936/",
|
||||||
"Select a password. Here is some inspiration: https://xkcd.com/936/",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async authorize(credentials: any) {
|
async authorize(credentials: any) {
|
||||||
@@ -57,10 +56,7 @@ export default NextAuth({
|
|||||||
throw new Error(ErrorCode.UserMissingPassword);
|
throw new Error(ErrorCode.UserMissingPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCorrectPassword = await verifyPassword(
|
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
||||||
credentials.password,
|
|
||||||
user.password
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isCorrectPassword) {
|
if (!isCorrectPassword) {
|
||||||
throw new Error(ErrorCode.IncorrectPassword);
|
throw new Error(ErrorCode.IncorrectPassword);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import prisma from "@documenso/prisma";
|
|
||||||
import { hashPassword } from "@documenso/lib/auth";
|
import { hashPassword } from "@documenso/lib/auth";
|
||||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import { IdentityProvider } from "@prisma/client";
|
||||||
|
|
||||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { email, password, source } = req.body;
|
const { email, password, source } = req.body;
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import {
|
|
||||||
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 { getDocument } from "@documenso/lib/query";
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
|
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
|
||||||
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
|
|
||||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { id: documentId } = req.query;
|
const { id: documentId } = req.query;
|
||||||
@@ -18,10 +14,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,33 +33,36 @@ 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.`);
|
|
||||||
|
|
||||||
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");
|
||||||
res.setHeader("Content-Type", "application/pdf");
|
res.setHeader("Content-Type", "application/pdf");
|
||||||
res.setHeader("Content-Length", buffer.length);
|
res.setHeader("Content-Length", buffer.length);
|
||||||
res.setHeader(
|
res.setHeader("Content-Disposition", `attachment; filename=${document?.title}`);
|
||||||
"Content-Disposition",
|
|
||||||
`attachment; filename=${document.title}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(buffer);
|
return res.status(200).send(buffer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import {
|
|
||||||
defaultHandler,
|
|
||||||
defaultResponder,
|
|
||||||
getUserFromToken,
|
|
||||||
} from "@documenso/lib/server";
|
|
||||||
import prisma from "@documenso/prisma";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import short from "short-uuid";
|
|
||||||
import { Document as PrismaDocument, FieldType } from "@prisma/client";
|
|
||||||
import { getDocument } from "@documenso/lib/query";
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import { FieldType, Document as PrismaDocument } from "@prisma/client";
|
||||||
|
import short from "short-uuid";
|
||||||
|
|
||||||
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const user = await getUserFromToken(req, res);
|
const user = await getUserFromToken(req, res);
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import {
|
|
||||||
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, FieldType } from "@prisma/client";
|
|
||||||
import { getDocument } from "@documenso/lib/query";
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import { FieldType, Document as PrismaDocument } from "@prisma/client";
|
||||||
|
|
||||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const user = await getUserFromToken(req, res);
|
const user = await getUserFromToken(req, res);
|
||||||
@@ -36,8 +32,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 +46,26 @@ 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.status(401).send("Recipient does not have access to this document.");
|
||||||
return res.status(401).send("User 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({
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import {
|
|
||||||
defaultHandler,
|
|
||||||
defaultResponder,
|
|
||||||
getUserFromToken,
|
|
||||||
} from "@documenso/lib/server";
|
|
||||||
import prisma from "@documenso/prisma";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import short from "short-uuid";
|
|
||||||
import { Document as PrismaDocument } from "@prisma/client";
|
|
||||||
import { getDocument } from "@documenso/lib/query";
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
|
import short from "short-uuid";
|
||||||
|
|
||||||
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const user = await getUserFromToken(req, res);
|
const user = await getUserFromToken(req, res);
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import {
|
|
||||||
defaultHandler,
|
|
||||||
defaultResponder,
|
|
||||||
getUserFromToken,
|
|
||||||
} from "@documenso/lib/server";
|
|
||||||
import prisma from "@documenso/prisma";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import short from "short-uuid";
|
|
||||||
import { Document as PrismaDocument } from "@prisma/client";
|
|
||||||
import { getDocument } from "@documenso/lib/query";
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
|
import short from "short-uuid";
|
||||||
|
|
||||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const user = await getUserFromToken(req, res);
|
const user = await getUserFromToken(req, res);
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import {
|
|
||||||
defaultHandler,
|
|
||||||
defaultResponder,
|
|
||||||
getUserFromToken,
|
|
||||||
} from "@documenso/lib/server";
|
|
||||||
import prisma from "@documenso/prisma";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { sendSigningRequest } from "@documenso/lib/mail";
|
import { sendSigningRequest } from "@documenso/lib/mail";
|
||||||
import { getDocument } from "@documenso/lib/query";
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
import { Document as PrismaDocument, SendStatus } from "@prisma/client";
|
import { Document as PrismaDocument, SendStatus } from "@prisma/client";
|
||||||
|
|
||||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@@ -23,8 +19,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
|
|
||||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
const document: PrismaDocument = 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.`);
|
|
||||||
|
|
||||||
let recipientCondition: any = {
|
let recipientCondition: any = {
|
||||||
documentId: +documentId,
|
documentId: +documentId,
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import {
|
|
||||||
defaultHandler,
|
|
||||||
defaultResponder,
|
|
||||||
getUserFromToken,
|
|
||||||
} from "@documenso/lib/server";
|
|
||||||
import prisma from "@documenso/prisma";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { SigningStatus, DocumentStatus } from "@prisma/client";
|
|
||||||
import { getDocument } from "@documenso/lib/query";
|
|
||||||
import { Document as PrismaDocument, FieldType } from "@prisma/client";
|
|
||||||
import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
|
|
||||||
import { sendSigningDoneMail } from "@documenso/lib/mail";
|
import { sendSigningDoneMail } from "@documenso/lib/mail";
|
||||||
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||||
|
import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import { DocumentStatus, SigningStatus } from "@prisma/client";
|
||||||
|
import { FieldType, Document as PrismaDocument } from "@prisma/client";
|
||||||
|
|
||||||
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.`);
|
||||||
|
|
||||||
@@ -60,6 +63,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
signingStatus: SigningStatus.SIGNED,
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
signedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,11 +74,24 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const signedRecipients = await prisma.recipient.findMany({
|
||||||
|
where: {
|
||||||
|
documentId: recipient.documentId,
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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,
|
||||||
type: { in: [FieldType.DATE, FieldType.TEXT] },
|
type: { in: [FieldType.DATE, FieldType.TEXT] },
|
||||||
|
recipientId: { in: signedRecipients.map((r) => r.id) },
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
Recipient: true,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Insert fields other than signatures
|
// Insert fields other than signatures
|
||||||
@@ -86,7 +103,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
}).format(new Date())
|
}).format(field.Recipient?.signedAt ?? new Date())
|
||||||
: field.customText || "",
|
: field.customText || "",
|
||||||
field.positionX,
|
field.positionX,
|
||||||
field.positionY,
|
field.positionY,
|
||||||
@@ -110,10 +127,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
document: documentWithInserts,
|
document: documentWithInserts,
|
||||||
status:
|
status: unsignedRecipients.length > 0 ? DocumentStatus.PENDING : DocumentStatus.COMPLETED,
|
||||||
unsignedRecipients.length > 0
|
|
||||||
? DocumentStatus.PENDING
|
|
||||||
: DocumentStatus.COMPLETED,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,8 +138,11 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.document = documentWithInserts;
|
document.document = documentWithInserts;
|
||||||
if (documentOwner)
|
if (documentOwner) await sendSigningDoneMail(document, documentOwner);
|
||||||
await sendSigningDoneMail(recipient, document, documentOwner);
|
|
||||||
|
for (const signer of signedRecipients) {
|
||||||
|
await sendSigningDoneMail(document, signer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
@@ -134,9 +151,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (signedField?.Signature?.signatureImageAsBase64) {
|
if (signedField?.Signature?.signatureImageAsBase64) {
|
||||||
documentWithInserts = await insertImageInPDF(
|
documentWithInserts = await insertImageInPDF(
|
||||||
documentWithInserts,
|
documentWithInserts,
|
||||||
signedField.Signature
|
signedField.Signature ? signedField.Signature?.signatureImageAsBase64 : "",
|
||||||
? signedField.Signature?.signatureImageAsBase64
|
|
||||||
: "",
|
|
||||||
signedField.positionX,
|
signedField.positionX,
|
||||||
signedField.positionY,
|
signedField.positionY,
|
||||||
signedField.page
|
signedField.page
|
||||||
@@ -164,12 +179,8 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
create: {
|
create: {
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
fieldId: signature.fieldId,
|
fieldId: signature.fieldId,
|
||||||
signatureImageAsBase64: signature.signatureImage
|
signatureImageAsBase64: signature.signatureImage ? signature.signatureImage : null,
|
||||||
? signature.signatureImage
|
typedSignature: signature.typedSignature ? signature.typedSignature : null,
|
||||||
: null,
|
|
||||||
typedSignature: signature.typedSignature
|
|
||||||
? signature.typedSignature
|
|
||||||
: null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
|
||||||
import prisma from "@documenso/prisma";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getUserFromToken } from "@documenso/lib/server";
|
|
||||||
import formidable from "formidable";
|
|
||||||
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
|
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
|
||||||
|
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||||
|
import { getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import formidable from "formidable";
|
||||||
|
import { isSubscribedServer } from "@documenso/lib/stripe";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
@@ -15,7 +16,17 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const form = formidable();
|
const form = formidable();
|
||||||
|
|
||||||
const user = await getUserFromToken(req, res);
|
const user = await getUserFromToken(req, res);
|
||||||
if (!user) return;
|
if (!user) {
|
||||||
|
return res.status(401).end();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSubscribed = await isSubscribedServer(req);
|
||||||
|
|
||||||
|
if (!isSubscribed) {
|
||||||
|
throw new Error("User is not subscribed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
form.parse(req, async (err, fields, files) => {
|
form.parse(req, async (err, fields, files) => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||||
import prisma from "@documenso/prisma";
|
import prisma from "@documenso/prisma";
|
||||||
|
|
||||||
|
|||||||
1
apps/web/pages/api/stripe/checkout-session.ts
Normal file
1
apps/web/pages/api/stripe/checkout-session.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { checkoutSessionHandler as default } from '@documenso/lib/stripe/handlers/checkout-session'
|
||||||
1
apps/web/pages/api/stripe/portal-session.ts
Normal file
1
apps/web/pages/api/stripe/portal-session.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { portalSessionHandler as default } from "@documenso/lib/stripe/handlers/portal-session";
|
||||||
1
apps/web/pages/api/stripe/subscription.ts
Normal file
1
apps/web/pages/api/stripe/subscription.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { getSubscriptionHandler as default } from '@documenso/lib/stripe/handlers/get-subscription'
|
||||||
5
apps/web/pages/api/stripe/webhook.ts
Normal file
5
apps/web/pages/api/stripe/webhook.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const config = {
|
||||||
|
api: { bodyParser: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
export { webhookHandler as default } from "@documenso/lib/stripe/handlers/webhook";
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
import {
|
|
||||||
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 { getDocument } from "@documenso/lib/query";
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
|
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
|
||||||
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
|
|
||||||
// todo remove before launch
|
// todo remove before launch
|
||||||
|
|
||||||
@@ -17,10 +13,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const signedDocument = await addDigitalSignature(document.document);
|
const signedDocument = await addDigitalSignature(document.document);
|
||||||
res.setHeader("Content-Type", "application/pdf");
|
res.setHeader("Content-Type", "application/pdf");
|
||||||
res.setHeader("Content-Length", signedDocument.length);
|
res.setHeader("Content-Length", signedDocument.length);
|
||||||
res.setHeader(
|
res.setHeader("Content-Disposition", `attachment; filename=${document.title}`);
|
||||||
"Content-Disposition",
|
|
||||||
`attachment; filename=${document.title}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(signedDocument);
|
return res.status(200).send(signedDocument);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import {
|
|
||||||
defaultHandler,
|
|
||||||
defaultResponder,
|
|
||||||
getUserFromToken,
|
|
||||||
} from "@documenso/lib/server";
|
|
||||||
import prisma from "@documenso/prisma";
|
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
|
||||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { method, body } = req;
|
const { method, body } = req;
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import {
|
|
||||||
defaultHandler,
|
|
||||||
defaultResponder,
|
|
||||||
getUserFromToken,
|
|
||||||
} from "@documenso/lib/server";
|
|
||||||
import prisma from "@documenso/prisma";
|
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
|
||||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const user = await getUserFromToken(req, res);
|
const user = await getUserFromToken(req, res);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { ChangeEvent, ReactElement } from "react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { ReactElement } from "react";
|
|
||||||
import Layout from "../components/layout";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { uploadDocument } from "@documenso/features";
|
||||||
|
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
|
||||||
|
import { getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import Layout from "../components/layout";
|
||||||
import type { NextPageWithLayout } from "./_app";
|
import type { NextPageWithLayout } from "./_app";
|
||||||
import {
|
import {
|
||||||
CheckBadgeIcon,
|
CheckBadgeIcon,
|
||||||
@@ -9,22 +12,23 @@ import {
|
|||||||
ExclamationTriangleIcon,
|
ExclamationTriangleIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { uploadDocument } from "@documenso/features";
|
|
||||||
import {
|
import {
|
||||||
DocumentStatus,
|
DocumentStatus,
|
||||||
|
Document as PrismaDocument,
|
||||||
SendStatus,
|
SendStatus,
|
||||||
SigningStatus,
|
SigningStatus,
|
||||||
Document as PrismaDocument,
|
|
||||||
} from "@prisma/client";
|
} from "@prisma/client";
|
||||||
import { getUserFromToken } from "@documenso/lib/server";
|
|
||||||
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
|
|
||||||
import { truncate } from "fs";
|
import { truncate } from "fs";
|
||||||
|
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||||
|
import { useSubscription } from "@documenso/lib/stripe";
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
document: File;
|
document: File;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DashboardPage: NextPageWithLayout = (props: any) => {
|
const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||||
|
const { hasSubscription } = useSubscription();
|
||||||
|
|
||||||
const stats = [
|
const stats = [
|
||||||
{
|
{
|
||||||
name: "Draft",
|
name: "Draft",
|
||||||
@@ -58,29 +62,30 @@ 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="mt-8 grid gap-5 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="overflow-hidden rounded-lg bg-white px-4 py-3 shadow hover:shadow-lg duration-300 sm:py-5 md:p-6">
|
||||||
<dt className="truncate text-sm font-medium text-gray-500 ">
|
<dt className="truncate text-sm font-medium text-gray-700 ">
|
||||||
<item.icon
|
<item.icon
|
||||||
className="flex-shrink-0 mr-3 h-6 w-6 inline text-neon"
|
className="text-neon-600 mr-3 inline h-5 w-5 flex-shrink-0 sm:h-6 sm:w-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-3 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
|
||||||
{getStat(item.name, props)}
|
{getStat(item.name, props)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<input
|
<input
|
||||||
id="fileUploadHelper"
|
id="fileUploadHelper"
|
||||||
type="file"
|
type="file"
|
||||||
onChange={(event: any) => {
|
accept="application/pdf"
|
||||||
|
onChange={(event: ChangeEvent) => {
|
||||||
uploadDocument(event);
|
uploadDocument(event);
|
||||||
}}
|
}}
|
||||||
hidden
|
hidden
|
||||||
@@ -88,17 +93,19 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
document?.getElementById("fileUploadHelper")?.click();
|
if (hasSubscription) {
|
||||||
|
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"
|
aria-disabled={!hasSubscription}
|
||||||
>
|
className="group hover:border-neon-600 duration-200 relative block w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 aria-disabled:opacity-50 aria-disabled:pointer-events-none">
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
className="mx-auto h-12 w-12 text-gray-400"
|
className="mx-auto h-12 w-12 text-gray-400 group-hover:text-gray-700 duration-200"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 00 20 25"
|
viewBox="0 00 20 25"
|
||||||
aria-hidden="true"
|
aria-hidden="true">
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@@ -106,10 +113,15 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span className="mt-2 block text-sm font-medium text-neon">
|
<span id="add_document" className="text-gray-500 group-hover:text-neon-700 mt-2 block text-sm font-medium duration-200">
|
||||||
Upload a new PDF document
|
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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -138,9 +150,7 @@ export async function getServerSideProps(context: any) {
|
|||||||
|
|
||||||
const documents: any[] = await getDocumentsForUserFromToken(context);
|
const documents: any[] = await getDocumentsForUserFromToken(context);
|
||||||
|
|
||||||
const drafts: PrismaDocument[] = documents.filter(
|
const drafts: PrismaDocument[] = documents.filter((d) => d.status === DocumentStatus.DRAFT);
|
||||||
(d) => d.status === DocumentStatus.DRAFT
|
|
||||||
);
|
|
||||||
|
|
||||||
const waiting: any[] = documents.filter(
|
const waiting: any[] = documents.filter(
|
||||||
(e) =>
|
(e) =>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { ReactElement, useEffect, useState } from "react";
|
import { ReactElement, useEffect, useState } from "react";
|
||||||
|
import { NextPageContext } from "next";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { uploadDocument } from "@documenso/features";
|
||||||
|
import { deleteDocument, getDocuments } from "@documenso/lib/api";
|
||||||
|
import { Button, IconButton, SelectBox } from "@documenso/ui";
|
||||||
import Layout from "../components/layout";
|
import Layout from "../components/layout";
|
||||||
import type { NextPageWithLayout } from "./_app";
|
import type { NextPageWithLayout } from "./_app";
|
||||||
import Head from "next/head";
|
|
||||||
import {
|
import {
|
||||||
ArrowDownTrayIcon,
|
ArrowDownTrayIcon,
|
||||||
CheckBadgeIcon,
|
CheckBadgeIcon,
|
||||||
@@ -13,21 +18,24 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { uploadDocument } from "@documenso/features";
|
|
||||||
import { DocumentStatus } from "@prisma/client";
|
import { DocumentStatus } from "@prisma/client";
|
||||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||||
import { Button, IconButton, SelectBox } from "@documenso/ui";
|
import { useSubscription } from "@documenso/lib/stripe";
|
||||||
import { NextPageContext } from "next";
|
|
||||||
import { deleteDocument, getDocuments } from "@documenso/lib/api";
|
|
||||||
|
|
||||||
const DocumentsPage: NextPageWithLayout = (props: any) => {
|
const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { hasSubscription } = useSubscription();
|
||||||
const [documents, setDocuments]: any[] = useState([]);
|
const [documents, setDocuments]: any[] = useState([]);
|
||||||
const [filteredDocuments, setFilteredDocuments] = useState([]);
|
const [filteredDocuments, setFilteredDocuments] = useState([]);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const statusFilters = [
|
|
||||||
|
type statusFilterType = {
|
||||||
|
label: string;
|
||||||
|
value: DocumentStatus | "ALL";
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusFilters: statusFilterType[] = [
|
||||||
{ label: "All", value: "ALL" },
|
{ label: "All", value: "ALL" },
|
||||||
{ label: "Draft", value: "DRAFT" },
|
{ label: "Draft", value: "DRAFT" },
|
||||||
{ label: "Waiting for others", value: "PENDING" },
|
{ label: "Waiting for others", value: "PENDING" },
|
||||||
@@ -42,12 +50,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
{ label: "Last 12 months", value: 366 },
|
{ label: "Last 12 months", value: 366 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const [selectedStatusFilter, setSelectedStatusFilter] = useState(
|
const [selectedStatusFilter, setSelectedStatusFilter] = useState(statusFilters[0]);
|
||||||
statusFilters[0]
|
const [selectedCreatedFilter, setSelectedCreatedFilter] = useState(createdFilter[0]);
|
||||||
);
|
|
||||||
const [selectedCreatedFilter, setSelectedCreatedFilter] = useState(
|
|
||||||
createdFilter[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadDocuments = async () => {
|
const loadDocuments = async () => {
|
||||||
if (!documents.length) setLoading(true);
|
if (!documents.length) setLoading(true);
|
||||||
@@ -62,9 +66,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDocuments().finally(() => {
|
loadDocuments().finally(() => {
|
||||||
setSelectedStatusFilter(
|
setSelectedStatusFilter(
|
||||||
statusFilters.filter(
|
statusFilters.filter((status) => status.value === props.filter.toUpperCase())[0]
|
||||||
(status) => status.value === props.filter.toUpperCase()
|
|
||||||
)[0]
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -79,9 +81,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
|
|
||||||
function filterDocumentes(documents: []): any {
|
function filterDocumentes(documents: []): any {
|
||||||
let filteredDocuments = documents.filter(
|
let filteredDocuments = documents.filter(
|
||||||
(d: any) =>
|
(d: any) => d.status === selectedStatusFilter.value || selectedStatusFilter.value === "ALL"
|
||||||
d.status === selectedStatusFilter.value ||
|
|
||||||
selectedStatusFilter.value === "ALL"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
filteredDocuments = filteredDocuments.filter((document: any) =>
|
filteredDocuments = filteredDocuments.filter((document: any) =>
|
||||||
@@ -91,6 +91,20 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
return filteredDocuments;
|
return filteredDocuments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleStatusFilterChange(status: statusFilterType) {
|
||||||
|
router.replace(
|
||||||
|
{
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { filter: status.value },
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
shallow: true, // Perform a shallow update, without reloading the page
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setSelectedStatusFilter(status);
|
||||||
|
}
|
||||||
|
|
||||||
function wasXDaysAgoOrLess(documentDate: Date, lastXDays: number): boolean {
|
function wasXDaysAgoOrLess(documentDate: Date, lastXDays: number): boolean {
|
||||||
if (lastXDays < 0) return true;
|
if (lastXDays < 0) return true;
|
||||||
|
|
||||||
@@ -98,9 +112,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
const today: Date = new Date(); // Today's date
|
const today: Date = new Date(); // Today's date
|
||||||
|
|
||||||
// Calculate the difference between the two dates in days
|
// Calculate the difference between the two dates in days
|
||||||
const diffInDays = Math.floor(
|
const diffInDays = Math.floor((today.getTime() - documentDate.getTime()) / millisecondsInDay);
|
||||||
(today.getTime() - documentDate.getTime()) / millisecondsInDay
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(diffInDays);
|
console.log(diffInDays);
|
||||||
|
|
||||||
@@ -114,7 +126,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<title>Documents | Documenso</title>
|
<title>Documents | Documenso</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="px-4 sm:px-6 lg:px-8">
|
<div className="px-4 sm:px-6 lg:px-8">
|
||||||
<div className="sm:flex sm:items-center mt-10">
|
<div className="mt-10 sm:flex sm:items-center">
|
||||||
<div className="sm:flex-auto">
|
<div className="sm:flex-auto">
|
||||||
<header>
|
<header>
|
||||||
<h1 className="text-3xl font-bold leading-tight tracking-tight text-gray-900">
|
<h1 className="text-3xl font-bold leading-tight tracking-tight text-gray-900">
|
||||||
@@ -125,36 +137,34 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||||
<Button
|
<Button
|
||||||
icon={DocumentPlusIcon}
|
icon={DocumentPlusIcon}
|
||||||
|
disabled={!hasSubscription}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
document?.getElementById("fileUploadHelper")?.click();
|
document?.getElementById("fileUploadHelper")?.click();
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
Add Document
|
Add Document
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 mb-12">
|
<div className="mt-3 mb-12 flex flex-row-reverse items-center gap-x-4">
|
||||||
<div className="w-fit block float-right ml-3 mt-7">
|
<div className="pt-5 block w-fit">
|
||||||
{filteredDocuments.length != 1
|
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
|
||||||
? filteredDocuments.length + " Documents"
|
|
||||||
: "1 Document"}
|
|
||||||
</div>
|
</div>
|
||||||
<SelectBox
|
<SelectBox
|
||||||
className="w-1/4 block float-right"
|
className="block w-1/4"
|
||||||
label="Created"
|
label="Created"
|
||||||
options={createdFilter}
|
options={createdFilter}
|
||||||
value={selectedCreatedFilter}
|
value={selectedCreatedFilter}
|
||||||
onChange={setSelectedCreatedFilter}
|
onChange={setSelectedCreatedFilter}
|
||||||
/>
|
/>
|
||||||
<SelectBox
|
<SelectBox
|
||||||
className="w-1/4 block float-right ml-3"
|
className="block w-1/4"
|
||||||
label="Status"
|
label="Status"
|
||||||
options={statusFilters}
|
options={statusFilters}
|
||||||
value={selectedStatusFilter}
|
value={selectedStatusFilter}
|
||||||
onChange={setSelectedStatusFilter}
|
onChange={handleStatusFilterChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-20 max-w-[1100px]" hidden={!loading}>
|
<div className="mt-8 max-w-[1100px]" hidden={!loading}>
|
||||||
<div className="ph-item">
|
<div className="ph-item">
|
||||||
<div className="ph-col-12">
|
<div className="ph-col-12">
|
||||||
<div className="ph-picture"></div>
|
<div className="ph-picture"></div>
|
||||||
@@ -171,14 +181,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="mt-8 flex flex-col" hidden={!documents.length || loading}>
|
||||||
className="mt-28 flex flex-col"
|
|
||||||
hidden={!documents.length || loading}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8"
|
className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8"
|
||||||
hidden={!documents.length || loading}
|
hidden={!documents.length || loading}>
|
||||||
>
|
|
||||||
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
|
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
|
||||||
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||||
<table className="min-w-full divide-y divide-gray-300">
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
@@ -186,32 +192,25 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||||
>
|
|
||||||
Title
|
Title
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||||
>
|
|
||||||
Recipients
|
Recipients
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||||
>
|
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
|
||||||
>
|
|
||||||
Created
|
Created
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||||
scope="col"
|
|
||||||
className="relative py-3.5 pl-3 pr-4 sm:pr-6"
|
|
||||||
>
|
|
||||||
<span className="sr-only">Delete</span>
|
<span className="sr-only">Delete</span>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -220,38 +219,30 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
{filteredDocuments.map((document: any, index: number) => (
|
{filteredDocuments.map((document: any, index: number) => (
|
||||||
<tr
|
<tr
|
||||||
key={document.id}
|
key={document.id}
|
||||||
className="hover:bg-gray-100 cursor-pointer"
|
className="cursor-pointer hover:bg-gray-100"
|
||||||
onClick={(event) => showDocument(document.id)}
|
onClick={(event) => showDocument(document.id)}>
|
||||||
>
|
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
{document.title || "#" + document.id}
|
{document.title || "#" + document.id}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
<td className="whitespace-nowrap inline-flex py-3 gap-x-2 gap-y-1 flex-wrap max-w-[250px] text-sm text-gray-500">
|
||||||
{document.Recipient.map((item: any) => (
|
{document.Recipient.map((item: any) => (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
{item.sendStatus === "NOT_SENT" ? (
|
{item.sendStatus === "NOT_SENT" ? (
|
||||||
<span
|
<span
|
||||||
id="sent_icon"
|
id="sent_icon"
|
||||||
className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
|
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||||
>
|
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||||
{item.name
|
|
||||||
? item.name + " <" + item.email + ">"
|
|
||||||
: item.email}
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
{item.sendStatus === "SENT" &&
|
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
|
||||||
item.readStatus !== "OPENED" ? (
|
|
||||||
<span id="sent_icon">
|
<span id="sent_icon">
|
||||||
<span
|
<span
|
||||||
id="sent_icon"
|
id="sent_icon"
|
||||||
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800"
|
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||||
>
|
<EnvelopeIcon className="mr-1 inline h-4"></EnvelopeIcon>
|
||||||
<EnvelopeIcon className="inline h-5 mr-1"></EnvelopeIcon>
|
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||||
{item.name
|
|
||||||
? item.name + " <" + item.email + ">"
|
|
||||||
: item.email}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -262,13 +253,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<span id="read_icon">
|
<span id="read_icon">
|
||||||
<span
|
<span
|
||||||
id="sent_icon"
|
id="sent_icon"
|
||||||
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800"
|
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||||
>
|
<CheckIcon className="-mr-2 inline h-4"></CheckIcon>
|
||||||
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
|
<CheckIcon className="mr-1 inline h-4"></CheckIcon>
|
||||||
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
|
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||||
{item.name
|
|
||||||
? item.name + " <" + item.email + ">"
|
|
||||||
: item.email}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -276,8 +264,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
)}
|
)}
|
||||||
{item.signingStatus === "SIGNED" ? (
|
{item.signingStatus === "SIGNED" ? (
|
||||||
<span id="signed_icon">
|
<span id="signed_icon">
|
||||||
<span className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
<span className="flex-shrink-0 h-6 inline-flex items-center 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>{" "}
|
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>{" "}
|
||||||
{item.email}
|
{item.email}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -307,9 +295,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
{formatDocumentStatus(document.status)}
|
{formatDocumentStatus(document.status)}
|
||||||
<p>
|
<p>
|
||||||
<small hidden={document.Recipient.length === 0}>
|
<small hidden={document.Recipient.length === 0}>
|
||||||
{document.Recipient.filter(
|
{document.Recipient.filter((r: any) => r.signingStatus === "SIGNED")
|
||||||
(r: any) => r.signingStatus === "SIGNED"
|
.length || 0}
|
||||||
).length || 0}
|
|
||||||
/{document.Recipient.length || 0}
|
/{document.Recipient.length || 0}
|
||||||
</small>
|
</small>
|
||||||
</p>
|
</p>
|
||||||
@@ -327,6 +314,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
router.push("/documents/" + document.id);
|
router.push("/documents/" + document.id);
|
||||||
}}
|
}}
|
||||||
|
disabled={document.status === "COMPLETED"}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={ArrowDownTrayIcon}
|
icon={ArrowDownTrayIcon}
|
||||||
@@ -342,30 +330,20 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
onClick={(event: any) => {
|
onClick={(event: any) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (
|
if (confirm("Are you sure you want to delete this document")) {
|
||||||
confirm(
|
|
||||||
"Are you sure you want to delete this document"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const documentsWithoutIndex = [...documents];
|
const documentsWithoutIndex = [...documents];
|
||||||
const removedItem: any =
|
const removedItem: any = documentsWithoutIndex.splice(index, 1);
|
||||||
documentsWithoutIndex.splice(index, 1);
|
|
||||||
setDocuments(documentsWithoutIndex);
|
setDocuments(documentsWithoutIndex);
|
||||||
deleteDocument(document.id)
|
deleteDocument(document.id)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
documentsWithoutIndex.splice(
|
documentsWithoutIndex.splice(index, 0, removedItem);
|
||||||
index,
|
|
||||||
0,
|
|
||||||
removedItem
|
|
||||||
);
|
|
||||||
setDocuments(documentsWithoutIndex);
|
setDocuments(documentsWithoutIndex);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
loadDocuments();
|
loadDocuments();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}></IconButton>
|
||||||
></IconButton>
|
|
||||||
<span className="sr-only">, {document.name}</span>
|
<span className="sr-only">, {document.name}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -374,29 +352,21 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div hidden={filteredDocuments.length > 0} className="mx-auto mt-12 w-fit p-3">
|
||||||
hidden={filteredDocuments.length > 0}
|
<FunnelIcon className="mr-1 inline w-5 align-middle" /> Nothing here. Maybe try a
|
||||||
className="mx-auto w-fit mt-12 p-3"
|
different filter.
|
||||||
>
|
|
||||||
<FunnelIcon className="w-5 inline mr-1 align-middle" /> Nothing
|
|
||||||
here. Maybe try a different filter.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="mt-24 text-center" id="empty" hidden={documents.length > 0 || loading}>
|
||||||
className="text-center mt-24"
|
|
||||||
id="empty"
|
|
||||||
hidden={documents.length > 0 || loading}
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
className="mx-auto h-12 w-12 text-gray-400"
|
className="mx-auto h-12 w-12 text-gray-400"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
aria-hidden="true"
|
aria-hidden="true">
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
@@ -406,20 +376,20 @@ 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
|
||||||
icon={PlusIcon}
|
icon={PlusIcon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
document?.getElementById("fileUploadHelper")?.click();
|
document?.getElementById("fileUploadHelper")?.click();
|
||||||
}}
|
}}>
|
||||||
>
|
Add Document
|
||||||
Upload 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 +397,11 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ReactTooltip
|
||||||
|
anchorId="empty"
|
||||||
|
place="bottom"
|
||||||
|
content="No preparation needed. Any PDF will do."
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import Layout from "../../../components/layout";
|
import Link from "next/link";
|
||||||
import { NextPageWithLayout } from "../../_app";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
|
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
|
||||||
import { getUserFromToken } from "@documenso/lib/server";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { DocumentStatus } from "@prisma/client";
|
|
||||||
import {
|
|
||||||
InformationCircleIcon,
|
|
||||||
PaperAirplaneIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import { getDocument } from "@documenso/lib/query";
|
import { getDocument } from "@documenso/lib/query";
|
||||||
import { Document as PrismaDocument } from "@prisma/client";
|
import { getUserFromToken } from "@documenso/lib/server";
|
||||||
import { Button, Breadcrumb } from "@documenso/ui";
|
import { useSubscription } from "@documenso/lib/stripe";
|
||||||
|
import { Breadcrumb, Button } from "@documenso/ui";
|
||||||
import PDFEditor from "../../../components/editor/pdf-editor";
|
import PDFEditor from "../../../components/editor/pdf-editor";
|
||||||
|
import Layout from "../../../components/layout";
|
||||||
|
import { NextPageWithLayout } from "../../_app";
|
||||||
|
import { InformationCircleIcon, PaperAirplaneIcon, UsersIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { DocumentStatus } from "@prisma/client";
|
||||||
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
|
|
||||||
const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
|
const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { hasSubscription } = useSubscription();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
@@ -32,8 +30,7 @@ const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: props.document.title,
|
title: props.document.title,
|
||||||
href:
|
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
|
||||||
NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -67,21 +64,13 @@ const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
|
|||||||
<Button
|
<Button
|
||||||
icon={PaperAirplaneIcon}
|
icon={PaperAirplaneIcon}
|
||||||
className="ml-3"
|
className="ml-3"
|
||||||
href={
|
href={NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients"}
|
||||||
NEXT_PUBLIC_WEBAPP_URL +
|
|
||||||
"/documents/" +
|
|
||||||
props.document.id +
|
|
||||||
"/recipients"
|
|
||||||
}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (
|
if (
|
||||||
confirm(
|
confirm(`Send document out to ${props?.document?.Recipient?.length} recipients?`)
|
||||||
`Send document out to ${props?.document?.Recipient?.length} recipients?`
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
Prepare to Send
|
Prepare to Send
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,11 +109,7 @@ export async function getServerSideProps(context: any) {
|
|||||||
const { id: documentId } = context.query;
|
const { id: documentId } = context.query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const document: PrismaDocument = await getDocument(
|
const document: PrismaDocument = await getDocument(+documentId, context.req, context.res);
|
||||||
+documentId,
|
|
||||||
context.req,
|
|
||||||
context.res
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import { ReactElement, useRef, useState } from "react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { Fragment, ReactElement, useRef, useState } from "react";
|
import { NEXT_PUBLIC_WEBAPP_URL, classNames } from "@documenso/lib";
|
||||||
|
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
|
||||||
|
import { getDocument } from "@documenso/lib/query";
|
||||||
|
import { getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import { Breadcrumb, Button, Dialog, IconButton, Tooltip } from "@documenso/ui";
|
||||||
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 {
|
import {
|
||||||
ArrowDownTrayIcon,
|
ArrowDownTrayIcon,
|
||||||
CheckBadgeIcon,
|
CheckBadgeIcon,
|
||||||
@@ -14,30 +18,20 @@ import {
|
|||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { getUserFromToken } from "@documenso/lib/server";
|
import { DocumentStatus, Document as PrismaDocument, Recipient } from "@prisma/client";
|
||||||
import { getDocument } from "@documenso/lib/query";
|
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||||
import { Document as PrismaDocument } from "@prisma/client";
|
import { toast } from "react-hot-toast";
|
||||||
import { Breadcrumb, Button, IconButton } from "@documenso/ui";
|
import { useSubscription } from "@documenso/lib/stripe";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
import {
|
|
||||||
createOrUpdateRecipient,
|
|
||||||
deleteRecipient,
|
|
||||||
sendSigningRequests,
|
|
||||||
} from "@documenso/lib/api";
|
|
||||||
import {
|
|
||||||
FormProvider,
|
|
||||||
useFieldArray,
|
|
||||||
useForm,
|
|
||||||
useWatch,
|
|
||||||
} from "react-hook-form";
|
|
||||||
|
|
||||||
type FormValues = {
|
export type FormValues = {
|
||||||
signers: { id: number; email: string; name: string }[];
|
signers: Array<Pick<Recipient, 'id' | 'email' | 'name' | 'sendStatus' | 'readStatus' | 'signingStatus'>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FormSigner = FormValues["signers"][number];
|
||||||
|
|
||||||
const RecipientsPage: NextPageWithLayout = (props: any) => {
|
const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||||
const title: string =
|
const { hasSubscription } = useSubscription();
|
||||||
`"` + props?.document?.title + `"` + "Recipients | Documenso";
|
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
|
||||||
const breadcrumbItems = [
|
const breadcrumbItems = [
|
||||||
{
|
{
|
||||||
title: "Documents",
|
title: "Documents",
|
||||||
@@ -45,15 +39,14 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: props.document.title,
|
title: props.document.title,
|
||||||
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
|
href:
|
||||||
|
props.document.status !== DocumentStatus.COMPLETED
|
||||||
|
? NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id
|
||||||
|
: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Recipients",
|
title: "Recipients",
|
||||||
href:
|
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients",
|
||||||
NEXT_PUBLIC_WEBAPP_URL +
|
|
||||||
"/documents/" +
|
|
||||||
props.document.id +
|
|
||||||
"/recipients",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -75,7 +68,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
});
|
});
|
||||||
const formValues = useWatch({ control, name: "signers" });
|
const formValues = useWatch({ control, name: "signers" });
|
||||||
const cancelButtonRef = useRef(null);
|
const cancelButtonRef = useRef(null);
|
||||||
const hasEmailError = (formValue: any): boolean => {
|
const hasEmailError = (formValue: FormSigner): boolean => {
|
||||||
const index = formValues.findIndex((e) => e.id === formValue.id);
|
const index = formValues.findIndex((e) => e.id === formValue.id);
|
||||||
return !!errors?.signers?.[index]?.email;
|
return !!errors?.signers?.[index]?.email;
|
||||||
};
|
};
|
||||||
@@ -85,7 +78,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<Head>
|
<Head>
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="mt-10">
|
<div className="mt-10 px-6 sm:px-0">
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumb document={props.document} items={breadcrumbItems} />
|
<Breadcrumb document={props.document} items={breadcrumbItems} />
|
||||||
</div>
|
</div>
|
||||||
@@ -96,342 +89,262 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex flex-shrink-0 md:mt-0 md:ml-4">
|
<div className="mt-4 flex flex-shrink-0 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"
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
href={"/api/documents/" + props.document.id}
|
href={"/api/documents/" + props.document.id}>
|
||||||
>
|
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{props.document.status !== DocumentStatus.COMPLETED && (
|
||||||
className="min-w-[125px]"
|
<>
|
||||||
color="primary"
|
<Button
|
||||||
icon={PaperAirplaneIcon}
|
icon={PencilSquareIcon}
|
||||||
onClick={() => {
|
disabled={props.document.status === DocumentStatus.COMPLETED}
|
||||||
setOpen(true);
|
color={
|
||||||
}}
|
props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"
|
||||||
disabled={
|
}
|
||||||
(formValues.length || 0) === 0 ||
|
className="mr-2"
|
||||||
!formValues.some(
|
href={breadcrumbItems[1].href}>
|
||||||
(r: any) =>
|
Edit Document
|
||||||
r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
</Button>
|
||||||
) ||
|
<Button
|
||||||
loading
|
className="min-w-[125px]"
|
||||||
}
|
color="primary"
|
||||||
>
|
icon={PaperAirplaneIcon}
|
||||||
Send
|
onClick={() => {
|
||||||
</Button>
|
formValues.some((r) => r.email && hasEmailError(r))
|
||||||
|
? toast.error("Please enter a valid email address.", { id: "invalid email" })
|
||||||
|
: setOpen(true);
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
!hasSubscription ||
|
||||||
|
(formValues.length || 0) === 0 ||
|
||||||
|
!formValues.some(
|
||||||
|
(r) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
||||||
|
) ||
|
||||||
|
loading
|
||||||
|
}>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-hidden rounded-md bg-white shadow mt-10 p-6">
|
<div className="mt-10 overflow-hidden rounded-md bg-white p-4 shadow sm:p-6">
|
||||||
<div className="border-b border-gray-200 pb-5">
|
<div className="border-b border-gray-200 pb-3 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
|
|
||||||
</h3>
|
|
||||||
<p className="mt-2 max-w-4xl text-sm text-gray-500">
|
<p className="mt-2 max-w-4xl text-sm text-gray-500">
|
||||||
The people who will sign the document.
|
{props.document.status !== DocumentStatus.COMPLETED
|
||||||
|
? "The people who will sign the document."
|
||||||
|
: "The people who signed the document."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<form
|
<form
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
trigger();
|
trigger();
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<ul role="list" className="divide-y divide-gray-200">
|
<ul role="list" className="divide-y divide-gray-200">
|
||||||
{fields.map((item: any, index: number) => (
|
{fields.map((item, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className="px-0 py-4 w-full hover:bg-green-50 border-0 group"
|
className="group w-full border-0 px-2 py-3 hover:bg-green-50 sm:py-4">
|
||||||
>
|
<div id="container" className="block w-full lg:flex lg:justify-between">
|
||||||
<div id="container" className="flex w-full">
|
<div className="block space-y-2 md:flex md:space-x-2 md:space-y-0">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
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",
|
"focus-within:border-neon focus-within:ring-neon rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:ring-1 md:w-[250px]",
|
||||||
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
item.sendStatus === "SENT" ? "bg-gray-100" : ""
|
||||||
)}
|
)}>
|
||||||
>
|
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
|
||||||
<label
|
Email
|
||||||
htmlFor="name"
|
</label>
|
||||||
className="block text-xs font-medium text-gray-900"
|
<input
|
||||||
>
|
type="email"
|
||||||
Email
|
{...register(`signers.${index}.email`, {
|
||||||
</label>
|
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||||
<input
|
})}
|
||||||
type="email"
|
defaultValue={item.email}
|
||||||
{...register(`signers.${index}.email`, {
|
disabled={item.sendStatus === "SENT" || loading}
|
||||||
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
onBlur={() => {
|
||||||
})}
|
|
||||||
defaultValue={item.email}
|
|
||||||
disabled={item.sendStatus === "SENT" || loading}
|
|
||||||
onBlur={() => {
|
|
||||||
if (!errors?.signers?.[index])
|
|
||||||
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 border-0 bg-inherit p-0 text-gray-900 placeholder-gray-500 outline-none disabled:bg-neutral-100 sm:text-sm"
|
||||||
</p>
|
/>
|
||||||
) : (
|
{errors?.signers?.[index] ? (
|
||||||
""
|
<p className="mt-2 text-sm text-red-600" id="email-error">
|
||||||
)}
|
<XMarkIcon className="inline h-5" /> Invalid Email
|
||||||
</div>
|
</p>
|
||||||
<div
|
|
||||||
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
|
||||||
|
className={classNames(
|
||||||
|
"focus-within:border-neon focus-within:ring-neon rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:ring-1 md:w-[250px]",
|
||||||
|
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 bg-inherit p-0 text-gray-900 placeholder-gray-500 outline-none disabled:bg-neutral-100 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex mr-1">
|
<div className="flex items-center space-x-2 lg:ml-2">
|
||||||
<IconButton
|
<div className="mb-2 mr-2 flex lg:mr-0">
|
||||||
icon={PaperAirplaneIcon}
|
<div key={item.id} className="space-x-2">
|
||||||
disabled={
|
{item.sendStatus === "NOT_SENT" ? (
|
||||||
!item.id ||
|
<span
|
||||||
item.sendStatus !== "SENT" ||
|
id="sent_icon"
|
||||||
item.signingStatus === "SIGNED" ||
|
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
|
||||||
loading
|
Not Sent
|
||||||
}
|
</span>
|
||||||
color="secondary"
|
) : null}
|
||||||
className="mr-4 h-9 my-auto"
|
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
|
||||||
onClick={() => {
|
<span id="sent_icon">
|
||||||
if (confirm("Resend this signing request?")) {
|
<span
|
||||||
setLoading(true);
|
id="sent_icon"
|
||||||
sendSigningRequests(props.document, [
|
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800 ">
|
||||||
item.id,
|
<CheckIcon className="mr-1 inline h-5" /> Sent
|
||||||
]).finally(() => {
|
</span>
|
||||||
setLoading(false);
|
</span>
|
||||||
});
|
) : null}
|
||||||
}
|
{item.readStatus === "OPENED" && item.signingStatus === "NOT_SIGNED" ? (
|
||||||
}}
|
<span id="read_icon">
|
||||||
>
|
<span
|
||||||
Resend
|
id="sent_icon"
|
||||||
</IconButton>
|
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
|
||||||
<IconButton
|
<CheckIcon className="-mr-2 inline h-5"></CheckIcon>
|
||||||
icon={TrashIcon}
|
<CheckIcon className="mr-1 inline h-5"></CheckIcon>
|
||||||
disabled={
|
Seen
|
||||||
!item.id || item.sendStatus === "SENT" || loading
|
</span>
|
||||||
}
|
</span>
|
||||||
onClick={() => {
|
) : null}
|
||||||
const removedItem = { ...fields }[index];
|
{item.signingStatus === "SIGNED" ? (
|
||||||
remove(index);
|
<span id="signed_icon">
|
||||||
deleteRecipient(item)?.catch((err) => {
|
<span
|
||||||
append(removedItem);
|
id="sent_icon"
|
||||||
});
|
className="mt-3 inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||||
}}
|
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>
|
||||||
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
Signed
|
||||||
/>
|
</span>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{props.document.status !== DocumentStatus.COMPLETED && (
|
||||||
|
<div className="mr-1 flex">
|
||||||
|
<Tooltip label="Resend">
|
||||||
|
<IconButton
|
||||||
|
icon={PaperAirplaneIcon}
|
||||||
|
disabled={
|
||||||
|
!item.id ||
|
||||||
|
item.sendStatus !== "SENT" ||
|
||||||
|
item.signingStatus === "SIGNED" ||
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
onClick={(event: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (confirm("Resend this signing request?")) {
|
||||||
|
setLoading(true);
|
||||||
|
sendSigningRequests(props.document, [item.id]).finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="mx-1 group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Delete">
|
||||||
|
<IconButton
|
||||||
|
icon={TrashIcon}
|
||||||
|
disabled={!item.id || item.sendStatus === "SENT" || loading}
|
||||||
|
onClick={(event: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (confirm("Delete this signing request?")) {
|
||||||
|
const removedItem = { ...fields }[index];
|
||||||
|
remove(index);
|
||||||
|
deleteRecipient(item)?.catch((err) => {
|
||||||
|
append(removedItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="mx-1 group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<Button
|
{props.document.status !== "COMPLETED" && (
|
||||||
icon={UserPlusIcon}
|
<Button
|
||||||
className="mt-3"
|
icon={UserPlusIcon}
|
||||||
onClick={() => {
|
className="mt-3"
|
||||||
createOrUpdateRecipient({
|
onClick={() => {
|
||||||
id: "",
|
createOrUpdateRecipient({
|
||||||
email: "",
|
id: "",
|
||||||
name: "",
|
email: "",
|
||||||
documentId: props.document.id,
|
name: "",
|
||||||
}).then((res) => {
|
documentId: props.document.id,
|
||||||
append(res);
|
}).then((res) => {
|
||||||
});
|
append(res);
|
||||||
}}
|
});
|
||||||
>
|
}}>
|
||||||
Add Signer
|
Add Signer
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</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="h-6 w-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 +364,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: {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import prisma from "@documenso/prisma";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { NextPageWithLayout } from "../../_app";
|
|
||||||
import { ReadStatus } from "@prisma/client";
|
|
||||||
import PDFSigner from "../../../components/editor/pdf-signer";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import PDFSigner from "../../../components/editor/pdf-signer";
|
||||||
|
import { NextPageWithLayout } from "../../_app";
|
||||||
import { ClockIcon } from "@heroicons/react/24/outline";
|
import { ClockIcon } from "@heroicons/react/24/outline";
|
||||||
import { FieldType, DocumentStatus } from "@prisma/client";
|
import { ReadStatus } from "@prisma/client";
|
||||||
|
import { DocumentStatus, FieldType } from "@prisma/client";
|
||||||
|
|
||||||
const SignPage: NextPageWithLayout = (props: any) => {
|
const SignPage: NextPageWithLayout = (props: any) => {
|
||||||
return (
|
return (
|
||||||
@@ -14,36 +14,22 @@ const SignPage: NextPageWithLayout = (props: any) => {
|
|||||||
<title>Sign | Documenso</title>
|
<title>Sign | Documenso</title>
|
||||||
</Head>
|
</Head>
|
||||||
{!props.expired ? (
|
{!props.expired ? (
|
||||||
<PDFSigner
|
<PDFSigner document={props.document} recipient={props.recipient} fields={props.fields} />
|
||||||
document={props.document}
|
|
||||||
recipient={props.recipient}
|
|
||||||
fields={props.fields}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="mx-auto w-fit px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
|
<div className="mx-auto w-fit px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
|
||||||
<ClockIcon className="text-neon w-10 inline mr-1"></ClockIcon>
|
<ClockIcon className="text-neon mr-1 inline w-10"></ClockIcon>
|
||||||
<h1 className="text-base font-medium text-neon inline align-middle">
|
<h1 className="text-neon inline align-middle text-base font-medium">Time flies.</h1>
|
||||||
Time flies.
|
<p className="mt-2 text-4xl font-bold tracking-tight">This signing link is expired.</p>
|
||||||
</h1>
|
|
||||||
<p className="mt-2 text-4xl font-bold tracking-tight">
|
|
||||||
This signing link is expired.
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 text-base text-gray-500">
|
<p className="mt-2 text-base text-gray-500">
|
||||||
Please ask{" "}
|
Please ask {props.document.User.name ? `${props.document.User.name}` : `the sender`}{" "}
|
||||||
{props.document.User.name
|
|
||||||
? `${props.document.User.name}`
|
|
||||||
: `the sender`}{" "}
|
|
||||||
to resend it.
|
to resend it.
|
||||||
</p>
|
</p>
|
||||||
<div className="mx-auto w-fit text-xl pt-20"></div>
|
<div className="mx-auto w-fit pt-20 text-xl"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="relative mx-96">
|
<div className="relative mx-96">
|
||||||
<div
|
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
className="absolute inset-0 flex items-center"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div className="w-full border-t border-gray-300" />
|
<div className="w-full border-t border-gray-300" />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center"></div>
|
<div className="relative flex justify-center"></div>
|
||||||
@@ -51,10 +37,7 @@ const SignPage: NextPageWithLayout = (props: any) => {
|
|||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-center text-sm text-gray-600">
|
<p className="mt-4 text-center text-sm text-gray-600">
|
||||||
Want to send of your own?{" "}
|
Want to send of your own?{" "}
|
||||||
<Link
|
<Link href="/signup?source=expired" className="text-neon hover:text-neon font-medium">
|
||||||
href="/signup?source=expired"
|
|
||||||
className="font-medium text-neon hover:text-neon"
|
|
||||||
>
|
|
||||||
Create your own Account
|
Create your own Account
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
@@ -76,7 +59,7 @@ export async function getServerSideProps(context: any) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const recipient = await prisma.recipient.findFirstOrThrow({
|
const recipient = await prisma.recipient.findFirst({
|
||||||
where: {
|
where: {
|
||||||
token: recipientToken,
|
token: recipientToken,
|
||||||
},
|
},
|
||||||
@@ -85,12 +68,21 @@ export async function getServerSideProps(context: any) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!recipient) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: false,
|
||||||
|
destination: "/404",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Document is already signed
|
// Document is already signed
|
||||||
if (recipient.Document.status === DocumentStatus.COMPLETED) {
|
if (recipient.Document.status === DocumentStatus.COMPLETED) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: `/documents/${recipient.Document.id}/signed`,
|
destination: `/documents/${recipient.Document.id}/signed?token=${recipientToken}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -107,7 +99,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: {
|
||||||
@@ -119,13 +110,9 @@ export async function getServerSideProps(context: any) {
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
recipient: JSON.parse(JSON.stringify(recipient)),
|
recipient: JSON.parse(JSON.stringify(recipient)),
|
||||||
document: JSON.parse(
|
document: JSON.parse(JSON.stringify({ ...recipient.Document, document: "" })),
|
||||||
JSON.stringify({ ...recipient.Document, document: "" })
|
|
||||||
),
|
|
||||||
fields: JSON.parse(JSON.stringify(unsignedFields)),
|
fields: JSON.parse(JSON.stringify(unsignedFields)),
|
||||||
expired: recipient.expired
|
expired: recipient.expired ? new Date(recipient.expired) < new Date() : false,
|
||||||
? new Date(recipient.expired) < new Date()
|
|
||||||
: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import prisma from "@documenso/prisma";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { NextPageWithLayout } from "../../_app";
|
|
||||||
import { ArrowDownTrayIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
|
|
||||||
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";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import { Button, IconButton } from "@documenso/ui";
|
||||||
|
import { NextPageWithLayout } from "../../_app";
|
||||||
|
import { ArrowDownTrayIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
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"
|
||||||
@@ -18,48 +18,35 @@ const SignPage: NextPageWithLayout = (props: any) => {
|
|||||||
<title>Sign | Documenso</title>
|
<title>Sign | Documenso</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="mx-auto w-fit px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
|
<div className="mx-auto w-fit px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
|
||||||
<CheckBadgeIcon className="text-neon w-10 inline mr-1"></CheckBadgeIcon>
|
<CheckBadgeIcon className="text-neon mr-1 inline w-10"></CheckBadgeIcon>
|
||||||
<h1 className="text-base font-medium text-neon inline align-middle">
|
<h1 className="text-neon inline align-middle text-base font-medium">It's done!</h1>
|
||||||
It's done!
|
|
||||||
</h1>
|
|
||||||
<p className="mt-2 text-4xl font-bold tracking-tight">
|
<p className="mt-2 text-4xl font-bold tracking-tight">
|
||||||
You signed "{props.document.title}"
|
You signed "{props.document.title}"
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={allRecipientsSigned}>
|
||||||
className="mt-2 text-base text-gray-500 max-w-sm"
|
|
||||||
hidden={allRecipientsSigned}
|
|
||||||
>
|
|
||||||
You will be notfied when all recipients have signed.
|
You will be notfied when all recipients have signed.
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={!allRecipientsSigned}>
|
||||||
className="mt-2 text-base text-gray-500 max-w-sm"
|
|
||||||
hidden={!allRecipientsSigned}
|
|
||||||
>
|
|
||||||
All recipients signed.
|
All recipients signed.
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div className="mx-auto w-fit pt-20 text-xl" hidden={!allRecipientsSigned}>
|
||||||
className="mx-auto w-fit text-xl pt-20"
|
|
||||||
hidden={!allRecipientsSigned}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
icon={ArrowDownTrayIcon}
|
icon={ArrowDownTrayIcon}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
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}"
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="relative mx-96">
|
<div className="relative mx-96">
|
||||||
<div
|
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
className="absolute inset-0 flex items-center"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div className="w-full border-t border-gray-300" />
|
<div className="w-full border-t border-gray-300" />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center"></div>
|
<div className="relative flex justify-center"></div>
|
||||||
@@ -67,10 +54,7 @@ const SignPage: NextPageWithLayout = (props: any) => {
|
|||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-center text-sm text-gray-600">
|
<p className="mt-4 text-center text-sm text-gray-600">
|
||||||
Want to send slick signing links like this one?{" "}
|
Want to send slick signing links like this one?{" "}
|
||||||
<Link
|
<Link href="https://documenso.com" className="text-neon hover:text-neon font-medium">
|
||||||
href="https://documenso.com"
|
|
||||||
className="font-medium text-neon hover:text-neon"
|
|
||||||
>
|
|
||||||
Hosted Documenso is coming soon™
|
Hosted Documenso is coming soon™
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
@@ -103,8 +87,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;
|
||||||
|
|||||||
@@ -1,13 +1,34 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import { getUserFromToken } from "@documenso/lib/server";
|
||||||
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 user = await getUserFromToken(context.req, context.res);
|
||||||
|
if (user)
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
source: "/login",
|
||||||
|
destination: "/dashboard",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALLOW_SIGNUP = process.env.NEXT_PUBLIC_ALLOW_SIGNUP === "true";
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
ALLOW_SIGNUP,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
import SettingsPage from ".";
|
import SettingsPage from ".";
|
||||||
|
|
||||||
export default SettingsPage;
|
export default SettingsPage;
|
||||||
|
|||||||
1
apps/web/pages/settings/billing.tsx
Normal file
1
apps/web/pages/settings/billing.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from ".";
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
import SettingsPage from ".";
|
import SettingsPage from ".";
|
||||||
|
|
||||||
export default SettingsPage;
|
export default SettingsPage;
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
import SettingsPage from ".";
|
import SettingsPage from ".";
|
||||||
|
|
||||||
export default SettingsPage;
|
export default SettingsPage;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextPageContext } from "next";
|
import { NextPageContext } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import { getUserFromToken } from "@documenso/lib/server";
|
||||||
import Signup from "../components/signup";
|
import Signup from "../components/signup";
|
||||||
|
|
||||||
export default function SignupPage(props: { source: string }) {
|
export default function SignupPage(props: { source: string }) {
|
||||||
@@ -14,6 +15,24 @@ export default function SignupPage(props: { source: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(context: any) {
|
export async function getServerSideProps(context: any) {
|
||||||
|
if (process.env.NEXT_PUBLIC_ALLOW_SIGNUP !== "true")
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/login",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = await getUserFromToken(context.req, context.res);
|
||||||
|
if (user)
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
source: "/signup",
|
||||||
|
destination: "/dashboard",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const signupSource: string = context.query["source"];
|
const signupSource: string = context.query["source"];
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ module.exports = {
|
|||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
24
apps/web/process-env.d.ts
vendored
Normal file
24
apps/web/process-env.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
declare namespace NodeJS {
|
||||||
|
export interface ProcessEnv {
|
||||||
|
DATABASE_URL: string;
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL: string;
|
||||||
|
NEXTAUTH_SECRET: string;
|
||||||
|
NEXTAUTH_URL: string;
|
||||||
|
|
||||||
|
SENDGRID_API_KEY?: string;
|
||||||
|
SMTP_MAIL_HOST?: string;
|
||||||
|
SMTP_MAIL_PORT?: string;
|
||||||
|
SMTP_MAIL_USER?: string;
|
||||||
|
SMTP_MAIL_PASSWORD?: string;
|
||||||
|
|
||||||
|
MAIL_FROM: string;
|
||||||
|
|
||||||
|
STRIPE_API_KEY?: string;
|
||||||
|
STRIPE_WEBHOOK_SECRET?: string;
|
||||||
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID?: string;
|
||||||
|
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID?: string;
|
||||||
|
|
||||||
|
NEXT_PUBLIC_ALLOW_SIGNUP?: string;
|
||||||
|
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/web/ressources/example.pdf
Normal file
BIN
apps/web/ressources/example.pdf
Normal file
Binary file not shown.
@@ -24,9 +24,8 @@ body,
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url("/fonts/montserrat.woff2") format("woff2");
|
src: url("/fonts/montserrat.woff2") format("woff2");
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* latin */
|
/* latin */
|
||||||
@@ -36,7 +35,6 @@ body,
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url("/fonts/montserrat.woff2") format("woff2");
|
src: url("/fonts/montserrat.woff2") format("woff2");
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
|
||||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
U+FEFF, U+FFFD;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,48 @@ module.exports = {
|
|||||||
qwigley: ["Qwigley", "serif"],
|
qwigley: ["Qwigley", "serif"],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
neon: "#37f095",
|
neon: {
|
||||||
"neon-dark": "#2CC077",
|
DEFAULT: "#37F095",
|
||||||
brown: "#353434",
|
50: "#E2FDF0",
|
||||||
|
100: "#CFFBE5",
|
||||||
|
200: "#A9F9D1",
|
||||||
|
300: "#83F6BD",
|
||||||
|
400: "#5DF3A9",
|
||||||
|
500: "#37F095",
|
||||||
|
600: "#11DE79",
|
||||||
|
700: "#0DAA5D",
|
||||||
|
800: "#097640",
|
||||||
|
900: "#054224",
|
||||||
|
950: "#032816",
|
||||||
|
},
|
||||||
|
"neon-dark": {
|
||||||
|
DEFAULT: "#2CC077",
|
||||||
|
50: "#B5EED2",
|
||||||
|
100: "#A5EAC8",
|
||||||
|
200: "#84E3B4",
|
||||||
|
300: "#62DBA0",
|
||||||
|
400: "#41D48B",
|
||||||
|
500: "#2CC077",
|
||||||
|
600: "#22925B",
|
||||||
|
700: "#17653E",
|
||||||
|
800: "#0D3722",
|
||||||
|
900: "#020906",
|
||||||
|
950: "#000000",
|
||||||
|
},
|
||||||
|
brown: {
|
||||||
|
DEFAULT: "#353434",
|
||||||
|
50: "#918F8F",
|
||||||
|
100: "#878585",
|
||||||
|
200: "#737171",
|
||||||
|
300: "#5E5C5C",
|
||||||
|
400: "#4A4848",
|
||||||
|
500: "#353434",
|
||||||
|
600: "#191818",
|
||||||
|
700: "#000000",
|
||||||
|
800: "#000000",
|
||||||
|
900: "#000000",
|
||||||
|
950: "#000000",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
"4xl": "2rem",
|
"4xl": "2rem",
|
||||||
|
|||||||
@@ -21,6 +21,6 @@
|
|||||||
"../../packages/types/next-auth.d.ts",
|
"../../packages/types/next-auth.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx"
|
"**/*.tsx"
|
||||||
],
|
, "../../packages/lib/process-env.d.ts" ],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule apps/website/documenso/website deleted from db81fb20e7
50
docker/Dockerfile
Normal file
50
docker/Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
FROM node:18-alpine AS base
|
||||||
|
|
||||||
|
# Install dependencies only when needed
|
||||||
|
FROM base AS production_deps
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
|
||||||
|
# Copy our current monorepo
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm ci --production
|
||||||
|
|
||||||
|
# Install dependencies only when needed
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
|
||||||
|
# Copy our current monorepo
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
RUN npm run build --workspaces
|
||||||
|
|
||||||
|
# Production image, copy all the files and run next
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=production_deps --chown=nextjs:nodejs /app/node_modules ./node_modules
|
||||||
|
COPY --from=production_deps --chown=nextjs:nodejs /app/package-lock.json ./package-lock.json
|
||||||
|
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/package.json ./package.json
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./public
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next ./.next
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT 3000
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
28
docker/build.sh
Executable file
28
docker/build.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 || {
|
||||||
|
echo "Docker is not running. Please start Docker and try again."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
command -v jq >/dev/null 2>&1 || {
|
||||||
|
echo "jq is not installed. Please install jq and try again."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
|
||||||
|
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
|
||||||
|
|
||||||
|
APP_VERSION="$(jq -r '.version' "$MONOREPO_ROOT/apps/web/package.json")"
|
||||||
|
GIT_SHA="$(git rev-parse HEAD)"
|
||||||
|
|
||||||
|
echo "Building docker image for monorepo at $MONOREPO_ROOT"
|
||||||
|
echo "App version: $APP_VERSION"
|
||||||
|
echo "Git SHA: $GIT_SHA"
|
||||||
|
|
||||||
|
docker build -f "$SCRIPT_DIR/Dockerfile" \
|
||||||
|
--progress=plain \
|
||||||
|
-t "documentso:latest" \
|
||||||
|
-t "documenso:$GIT_SHA" \
|
||||||
|
-t "documenso:$APP_VERSION" \
|
||||||
|
"$MONOREPO_ROOT"
|
||||||
12
docker/compose-entrypoint.sh
Executable file
12
docker/compose-entrypoint.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
|
||||||
|
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
|
||||||
|
|
||||||
|
cd "$MONOREPO_ROOT"
|
||||||
|
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
npm run db-migrate:dev
|
||||||
|
|
||||||
|
npm run dev
|
||||||
19
docker/compose-without-app.yml
Normal file
19
docker/compose-without-app.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: documenso
|
||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: database
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=documenso
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
- POSTGRES_DB=documenso
|
||||||
|
ports:
|
||||||
|
- 54320:5432
|
||||||
|
|
||||||
|
inbucket:
|
||||||
|
image: inbucket/inbucket
|
||||||
|
container_name: mailserver
|
||||||
|
ports:
|
||||||
|
- 9000:9000
|
||||||
|
- 2500:2500
|
||||||
|
- 1100:1100
|
||||||
40
docker/compose.yml
Normal file
40
docker/compose.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
services:
|
||||||
|
database:
|
||||||
|
image: postgres:15
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=documenso
|
||||||
|
- POSTGRES_PASSWORD=password
|
||||||
|
- POSTGRES_DB=documenso
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
inbucket:
|
||||||
|
image: inbucket/inbucket
|
||||||
|
ports:
|
||||||
|
- 9000:9000
|
||||||
|
- 2500:2500
|
||||||
|
- 1100:1100
|
||||||
|
|
||||||
|
documenso:
|
||||||
|
image: node:18
|
||||||
|
working_dir: /app
|
||||||
|
command: ./docker/compose-entrypoint.sh
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
- inbucket
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgres://documenso:password@database:5432/documenso
|
||||||
|
- NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||||
|
- NEXTAUTH_SECRET=my-super-secure-secret
|
||||||
|
- NEXTAUTH_URL=http://localhost:3000
|
||||||
|
- SENDGRID_API_KEY=
|
||||||
|
- SMTP_MAIL_HOST=inbucket
|
||||||
|
- SMTP_MAIL_PORT=2500
|
||||||
|
- SMTP_MAIL_USER=username
|
||||||
|
- SMTP_MAIL_PASSWORD=password
|
||||||
|
- MAIL_FROM=admin@example.com
|
||||||
|
- NEXT_PUBLIC_ALLOW_SIGNUP=true
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
volumes:
|
||||||
|
- ../:/app
|
||||||
9957
package-lock.json
generated
9957
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -2,11 +2,18 @@
|
|||||||
"name": "documenso-monorepo",
|
"name": "documenso-monorepo",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cd apps && cd web && next dev",
|
"dev": "npm run dev -w apps/web",
|
||||||
"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",
|
||||||
|
"docker:compose": "docker-compose -f ./docker/compose-without-app.yml",
|
||||||
|
"docker:compose-up": "npm run docker:compose -- up -d",
|
||||||
|
"docker:compose-down": "npm run docker:compose -- down",
|
||||||
|
"stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook",
|
||||||
|
"dx": "npm install && run-s docker:compose-up db-migrate:dev",
|
||||||
|
"d": "npm install && run-s docker:compose-up db-migrate:dev && npm run db-seed && npm run dev"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -20,30 +27,31 @@
|
|||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"@headlessui/react": "^1.7.4",
|
"@headlessui/react": "^1.7.4",
|
||||||
"@heroicons/react": "^2.0.13",
|
"@heroicons/react": "^2.0.13",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
|
||||||
"@types/bcryptjs": "^2.4.2",
|
|
||||||
"@types/node": "18.11.9",
|
|
||||||
"@types/react-dom": "18.0.9",
|
|
||||||
"avatar-from-initials": "^1.0.3",
|
"avatar-from-initials": "^1.0.3",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"dotenv": "^16.0.3",
|
"next": "13.2.4",
|
||||||
"eslint": "8.27.0",
|
"next-auth": ">=4.20.1",
|
||||||
"eslint-config-next": "13.0.3",
|
|
||||||
"install": "^0.13.0",
|
|
||||||
"next": "13.0.3",
|
|
||||||
"next-auth": "^4.18.3",
|
|
||||||
"next-transpile-modules": "^10.0.0",
|
|
||||||
"npm": "^9.1.3",
|
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.41.5",
|
"react-hook-form": "^7.41.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"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||||
|
"@types/bcryptjs": "^2.4.2",
|
||||||
|
"@types/node": "18.11.9",
|
||||||
|
"@types/react-dom": "18.0.9",
|
||||||
"@types/react-signature-canvas": "^1.0.2",
|
"@types/react-signature-canvas": "^1.0.2",
|
||||||
"file-loader": "^6.2.0"
|
"dotenv": "^16.0.3",
|
||||||
|
"eslint": "8.27.0",
|
||||||
|
"eslint-config-next": "13.0.3",
|
||||||
|
"next-transpile-modules": "^10.0.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.2.5",
|
||||||
|
"typescript": "4.8.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,10 +14,8 @@ export const createField = (
|
|||||||
if (newFieldX < 0) newFieldX = 0;
|
if (newFieldX < 0) newFieldX = 0;
|
||||||
if (newFieldY < 0) newFieldY = 0;
|
if (newFieldY < 0) newFieldY = 0;
|
||||||
|
|
||||||
if (newFieldX + fieldSize.width > rect.width)
|
if (newFieldX + fieldSize.width > rect.width) newFieldX = rect.width - fieldSize.width;
|
||||||
newFieldX = rect.width - fieldSize.width;
|
if (newFieldY + fieldSize.height > rect.height) newFieldY = rect.height - fieldSize.height;
|
||||||
if (newFieldY + fieldSize.height > rect.height)
|
|
||||||
newFieldY = rect.height - fieldSize.height;
|
|
||||||
|
|
||||||
const signatureField = {
|
const signatureField = {
|
||||||
id: -1,
|
id: -1,
|
||||||
|
|||||||
40
packages/features/ee/LICENSE
Normal file
40
packages/features/ee/LICENSE
Normal 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.
|
||||||
15
packages/features/ee/README.md
Normal file
15
packages/features/ee/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<div align="center"style="padding: 12px">
|
||||||
|
<a href="https://github.com/documenso/documenso.com">
|
||||||
|
<img width="250px" src="https://user-images.githubusercontent.com/1309312/224986248-5b8a5cdc-2dc1-46b9-a354-985bb6808ee0.png" alt="Documenso Logo">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://dub.sh/documenso-enterprise">Contact Us</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
# Enterprise Edition
|
||||||
|
|
||||||
|
Welcome to the Enterprise Edition ("/ee") of Documenso.com.
|
||||||
|
|
||||||
|
The [/ee](https://github.com/documenso/documenso/tree/main/packages/features/ee) subfolder is the home of all the **Enterprise Edition** features from our [hosted](https://documenso.com/pricing) plan. To use this code in production you need and valid Enterprise License.
|
||||||
|
|
||||||
|
> IMPORTANT: This subfolder is licensed differently than the rest of our [main repo](https://github.com/documenso/documenso). [Contact us](https://dub.sh/documenso-enterprise) to learn more.
|
||||||
@@ -1,31 +1,43 @@
|
|||||||
|
import { ChangeEvent } from "react";
|
||||||
import router from "next/router";
|
import router from "next/router";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
|
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export const uploadDocument = async (event: any) => {
|
export const uploadDocument = async (event: ChangeEvent) => {
|
||||||
if (event.target.files && event.target.files[0]) {
|
if (event.target instanceof HTMLInputElement && 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
|
|
||||||
.promise(
|
await toast.promise(
|
||||||
fetch("/api/documents", {
|
fetch("/api/documents", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body,
|
body,
|
||||||
}),
|
}).then((response: Response) => {
|
||||||
{
|
if (!response.ok) {
|
||||||
loading: "Uploading document...",
|
throw new Error("Could not upload document");
|
||||||
success: `${fileName} uploaded successfully.`,
|
|
||||||
error: "Could not upload document :/",
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
.then((response: Response) => {
|
|
||||||
response.json().then((createdDocumentIdFromBody) => {
|
response.json().then((createdDocumentIdFromBody) => {
|
||||||
router.push(
|
router.push(
|
||||||
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${createdDocumentIdFromBody}/recipients`
|
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${createdDocumentIdFromBody}/recipients`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
}),
|
||||||
|
{
|
||||||
|
loading: "Uploading document...",
|
||||||
|
success: `${fileName} uploaded successfully.`,
|
||||||
|
error: "Could not upload document :/",
|
||||||
|
}
|
||||||
|
).catch((_err) => {
|
||||||
|
// Do nothing
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -6,16 +6,13 @@ export const deleteRecipient = (recipient: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return toast.promise(
|
return toast.promise(
|
||||||
fetch(
|
fetch("/api/documents/" + recipient.documentId + "/recipients/" + recipient.id, {
|
||||||
"/api/documents/" + recipient.documentId + "/recipients/" + recipient.id,
|
method: "DELETE",
|
||||||
{
|
headers: {
|
||||||
method: "DELETE",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
},
|
||||||
"Content-Type": "application/json",
|
body: JSON.stringify(recipient),
|
||||||
},
|
}),
|
||||||
body: JSON.stringify(recipient),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
loading: "Deleting...",
|
loading: "Deleting...",
|
||||||
success: "Deleted.",
|
success: "Deleted.",
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ export { getDocuments } from "./getDocuments";
|
|||||||
export { deleteDocument } from "./deleteDocument";
|
export { deleteDocument } from "./deleteDocument";
|
||||||
export { deleteRecipient } from "./deleteRecipient";
|
export { deleteRecipient } from "./deleteRecipient";
|
||||||
export { createOrUpdateRecipient } from "./createOrUpdateRecipient";
|
export { createOrUpdateRecipient } from "./createOrUpdateRecipient";
|
||||||
export { sendSigningRequests } from "./sendSigningRequests";
|
export { sendSigningRequests } from "./sendSigningRequests";
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export const sendSigningRequests = async (
|
export const sendSigningRequests = async (document: any, resendTo: number[] = []) => {
|
||||||
document: any,
|
|
||||||
resendTo: number[] = []
|
|
||||||
) => {
|
|
||||||
if (!document || !document.id) return;
|
if (!document || !document.id) return;
|
||||||
try {
|
try {
|
||||||
const sent = await toast.promise(
|
const sent = await toast.promise(
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export const signDocument = (
|
export const signDocument = (document: any, signatures: any[], token: string): Promise<any> => {
|
||||||
document: any,
|
|
||||||
signatures: any[],
|
|
||||||
token: string
|
|
||||||
): Promise<any> => {
|
|
||||||
const body = { documentId: document.id, signatures };
|
const body = { documentId: document.id, signatures };
|
||||||
|
|
||||||
return toast.promise(
|
return toast.promise(
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { compare, hash } from "bcryptjs";
|
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
import type { Session } from "next-auth";
|
|
||||||
import {
|
|
||||||
getSession as getSessionInner,
|
|
||||||
GetSessionParams,
|
|
||||||
} from "next-auth/react";
|
|
||||||
|
|
||||||
import { HttpError } from "@documenso/lib/server";
|
import { HttpError } from "@documenso/lib/server";
|
||||||
|
import { compare, hash } from "bcryptjs";
|
||||||
|
import type { Session } from "next-auth";
|
||||||
|
import { GetSessionParams, getSession as getSessionInner } from "next-auth/react";
|
||||||
|
|
||||||
export async function hashPassword(password: string) {
|
export async function hashPassword(password: string) {
|
||||||
const hashedPassword = await hash(password, 12);
|
const hashedPassword = await hash(password, 12);
|
||||||
@@ -28,9 +24,7 @@ export function validPassword(password: string) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSession(
|
export async function getSession(options: GetSessionParams): Promise<Session | null> {
|
||||||
options: GetSessionParams
|
|
||||||
): Promise<Session | null> {
|
|
||||||
const session = await getSessionInner(options);
|
const session = await getSessionInner(options);
|
||||||
|
|
||||||
// that these are equal are ensured in `[...nextauth]`'s callback
|
// that these are equal are ensured in `[...nextauth]`'s callback
|
||||||
@@ -43,11 +37,7 @@ export function isPasswordValid(
|
|||||||
breakdown: boolean,
|
breakdown: boolean,
|
||||||
strict?: boolean
|
strict?: boolean
|
||||||
): { caplow: boolean; num: boolean; min: boolean; admin_min: boolean };
|
): { caplow: boolean; num: boolean; min: boolean; admin_min: boolean };
|
||||||
export function isPasswordValid(
|
export function isPasswordValid(password: string, breakdown?: boolean, strict?: boolean) {
|
||||||
password: string,
|
|
||||||
breakdown?: boolean,
|
|
||||||
strict?: boolean
|
|
||||||
) {
|
|
||||||
let cap = false, // Has uppercase characters
|
let cap = false, // Has uppercase characters
|
||||||
low = false, // Has lowercase characters
|
low = false, // Has lowercase characters
|
||||||
num = false, // At least one number
|
num = false, // At least one number
|
||||||
@@ -63,8 +53,7 @@ export function isPasswordValid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!breakdown)
|
if (!breakdown) return cap && low && num && min && (strict ? admin_min : true);
|
||||||
return cap && low && num && min && (strict ? admin_min : true);
|
|
||||||
|
|
||||||
let errors: Record<string, boolean> = { caplow: cap && low, num, min };
|
let errors: Record<string, boolean> = { caplow: cap && low, num, min };
|
||||||
// Only return the admin key if strict mode is enabled.
|
// Only return the admin key if strict mode is enabled.
|
||||||
@@ -79,8 +68,7 @@ type CtxOrReq =
|
|||||||
|
|
||||||
export const ensureSession = async (ctxOrReq: CtxOrReq) => {
|
export const ensureSession = async (ctxOrReq: CtxOrReq) => {
|
||||||
const session = await getSession(ctxOrReq);
|
const session = await getSession(ctxOrReq);
|
||||||
if (!session?.user)
|
if (!session?.user) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||||
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
|
||||||
return session;
|
return session;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
export const NEXT_PUBLIC_WEBAPP_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;
|
export const NEXT_PUBLIC_WEBAPP_URL =
|
||||||
|
process.env.IS_PULL_REQUEST === "true"
|
||||||
|
? process.env.RENDER_EXTERNAL_URL
|
||||||
|
: process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { sendMail } from "./sendMail";
|
|
||||||
import { signingCompleteTemplate } from "@documenso/lib/mail";
|
import { signingCompleteTemplate } from "@documenso/lib/mail";
|
||||||
import { Document as PrismaDocument } from "@prisma/client";
|
|
||||||
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
|
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
|
||||||
|
import { sendMail } from "./sendMail";
|
||||||
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
|
|
||||||
export const sendSigningDoneMail = async (
|
export const sendSigningDoneMail = async (document: PrismaDocument, user: any) => {
|
||||||
recipient: any,
|
|
||||||
document: PrismaDocument,
|
|
||||||
user: any
|
|
||||||
) => {
|
|
||||||
await sendMail(
|
await sendMail(
|
||||||
user.email,
|
user.email,
|
||||||
`Completed: "${document.title}"`,
|
`Completed: "${document.title}"`,
|
||||||
@@ -15,10 +11,7 @@ export const sendSigningDoneMail = async (
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
filename: document.title,
|
filename: document.title,
|
||||||
content: Buffer.from(
|
content: Buffer.from(await addDigitalSignature(document.document), "base64"),
|
||||||
await addDigitalSignature(document.document),
|
|
||||||
"base64"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import prisma from "@documenso/prisma";
|
|
||||||
import { sendMail } from "./sendMail";
|
|
||||||
import { SendStatus, ReadStatus, DocumentStatus } from "@prisma/client";
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
|
||||||
import { signingRequestTemplate } from "@documenso/lib/mail";
|
import { signingRequestTemplate } from "@documenso/lib/mail";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||||
|
import { sendMail } from "./sendMail";
|
||||||
|
import { DocumentStatus, ReadStatus, SendStatus } from "@prisma/client";
|
||||||
|
|
||||||
|
export const sendSigningRequest = async (recipient: any, document: any, user: any) => {
|
||||||
|
const signingRequestMessage = user.name
|
||||||
|
? `${user.name} (${user.email}) has sent you a document to sign. `
|
||||||
|
: `${user.email} has sent you a document to sign. `;
|
||||||
|
|
||||||
export const sendSigningRequest = async (
|
|
||||||
recipient: any,
|
|
||||||
document: any,
|
|
||||||
user: any
|
|
||||||
) => {
|
|
||||||
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}`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||||
import { Document as PrismaDocument } from "@prisma/client";
|
|
||||||
import { baseEmailTemplate } from "./baseTemplate";
|
import { baseEmailTemplate } from "./baseTemplate";
|
||||||
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
|
|
||||||
export const signingCompleteTemplate = (message: string) => {
|
export const signingCompleteTemplate = (message: string) => {
|
||||||
const customContent = `
|
const customContent = `
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||||
import { Document as PrismaDocument } from "@prisma/client";
|
|
||||||
import { baseEmailTemplate } from "./baseTemplate";
|
import { baseEmailTemplate } from "./baseTemplate";
|
||||||
|
import { Document as PrismaDocument } from "@prisma/client";
|
||||||
|
|
||||||
export const signingRequestTemplate = (
|
export const signingRequestTemplate = (
|
||||||
message: string,
|
message: string,
|
||||||
@@ -11,8 +11,8 @@ export const signingRequestTemplate = (
|
|||||||
user: any
|
user: any
|
||||||
) => {
|
) => {
|
||||||
const customContent = `
|
const customContent = `
|
||||||
<p style="margin: 30px;">
|
<p style="margin: 30px 0px; text-align: center">
|
||||||
<a href="${ctaLink}" style="background-color: #37f095; color: white; border-color: transparent; border-width: 1px; border-radius: 0.375rem; font-size: 18px; padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; text-decoration: none; margin-top: 4px; margin-bottom: 4px;">
|
<a href="${ctaLink}" style="background-color: #37f095; white-space: nowrap; color: white; border-color: transparent; border-width: 1px; border-radius: 0.375rem; font-size: 18px; padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; text-decoration: none; margin-top: 4px; margin-bottom: 4px;">
|
||||||
${ctaLabel}
|
${ctaLabel}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^2.4.3"
|
"@documenso/prisma": "*",
|
||||||
|
"@prisma/client": "^4.8.1",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"micro": "^10.0.1",
|
||||||
|
"stripe": "^12.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
24
packages/lib/process-env.d.ts
vendored
Normal file
24
packages/lib/process-env.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
declare namespace NodeJS {
|
||||||
|
export interface ProcessEnv {
|
||||||
|
DATABASE_URL: string;
|
||||||
|
NEXT_PUBLIC_WEBAPP_URL: string;
|
||||||
|
NEXTAUTH_SECRET: string;
|
||||||
|
NEXTAUTH_URL: string;
|
||||||
|
|
||||||
|
SENDGRID_API_KEY?: string;
|
||||||
|
SMTP_MAIL_HOST?: string;
|
||||||
|
SMTP_MAIL_PORT?: string;
|
||||||
|
SMTP_MAIL_USER?: string;
|
||||||
|
SMTP_MAIL_PASSWORD?: string;
|
||||||
|
|
||||||
|
MAIL_FROM: string;
|
||||||
|
|
||||||
|
STRIPE_API_KEY?: string;
|
||||||
|
STRIPE_WEBHOOK_SECRET?: string;
|
||||||
|
STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID?: string;
|
||||||
|
STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID?: string;
|
||||||
|
|
||||||
|
NEXT_PUBLIC_ALLOW_SIGNUP?: string;
|
||||||
|
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS?: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { getUserFromToken } from "@documenso/lib/server";
|
import { getUserFromToken } from "@documenso/lib/server";
|
||||||
import prisma from "@documenso/prisma";
|
import prisma from "@documenso/prisma";
|
||||||
|
|
||||||
export const getDocumentsForUserFromToken = async (
|
export const getDocumentsForUserFromToken = async (context: any): Promise<any> => {
|
||||||
context: any
|
|
||||||
): Promise<any> => {
|
|
||||||
const user = await getUserFromToken(context.req, context.res);
|
const user = await getUserFromToken(context.req, context.res);
|
||||||
if (!user) return Promise.reject("Invalid user or token.");
|
if (!user) return Promise.reject("Invalid user or token.");
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
type Handlers = {
|
type Handlers = {
|
||||||
[method in "GET" | "POST" | "PATCH" | "PUT" | "DELETE"]?: Promise<{ default: NextApiHandler }>;
|
[method in "GET" | "POST" | "PATCH" | "PUT" | "DELETE"]?: Promise<{
|
||||||
|
default: NextApiHandler;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Allows us to split big API handlers by method */
|
/** Allows us to split big API handlers by method */
|
||||||
export const defaultHandler = (handlers: Handlers) => async (req: NextApiRequest, res: NextApiResponse) => {
|
export const defaultHandler =
|
||||||
const handler = (await handlers[req.method as keyof typeof handlers])?.default;
|
(handlers: Handlers) => async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
// auto catch unsupported methods.
|
const handler = (await handlers[req.method as keyof typeof handlers])?.default;
|
||||||
if (!handler) {
|
// auto catch unsupported methods.
|
||||||
return res
|
if (!handler) {
|
||||||
.status(405)
|
return res.status(405).json({
|
||||||
.json({ message: `Method Not Allowed (Allow: ${Object.keys(handlers).join(",")})` });
|
message: `Method Not Allowed (Allow: ${Object.keys(handlers).join(",")})`,
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handler(req, res);
|
await handler(req, res);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return res.status(500).json({ message: "Something went wrong" });
|
return res.status(500).json({ message: "Something went wrong" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { getServerErrorFromUnknown } from "@documenso/lib/server";
|
import { getServerErrorFromUnknown } from "@documenso/lib/server";
|
||||||
|
|
||||||
type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;
|
type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
|
||||||
PrismaClientKnownRequestError,
|
|
||||||
NotFoundError,
|
|
||||||
} from "@prisma/client/runtime";
|
|
||||||
|
|
||||||
import { HttpError } from "@documenso/lib/server";
|
import { HttpError } from "@documenso/lib/server";
|
||||||
|
import { NotFoundError, PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
|
|
||||||
export function getServerErrorFromUnknown(cause: unknown): HttpError {
|
export function getServerErrorFromUnknown(cause: unknown): HttpError {
|
||||||
// Error was manually thrown and does not need to be parsed.
|
// Error was manually thrown and does not need to be parsed.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import prisma from "@documenso/prisma";
|
import prisma from "@documenso/prisma";
|
||||||
import { User as PrismaUser } from "@prisma/client";
|
import { User as PrismaUser } from "@prisma/client";
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export async function getUserFromToken(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
if (res) res.status(401).end();
|
if (res && res.status) res.status(401).end();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ export class HttpError<TCode extends number = number> extends Error {
|
|||||||
public readonly url: string | undefined;
|
public readonly url: string | undefined;
|
||||||
public readonly method: string | undefined;
|
public readonly method: string | undefined;
|
||||||
|
|
||||||
constructor(opts: { url?: string; method?: string; message?: string; statusCode: TCode; cause?: Error }) {
|
constructor(opts: {
|
||||||
|
url?: string;
|
||||||
|
method?: string;
|
||||||
|
message?: string;
|
||||||
|
statusCode: TCode;
|
||||||
|
cause?: Error;
|
||||||
|
}) {
|
||||||
super(opts.message ?? `HTTP Error ${opts.statusCode} `);
|
super(opts.message ?? `HTTP Error ${opts.statusCode} `);
|
||||||
|
|
||||||
Object.setPrototypeOf(this, HttpError.prototype);
|
Object.setPrototypeOf(this, HttpError.prototype);
|
||||||
|
|||||||
7
packages/lib/stripe/client.ts
Normal file
7
packages/lib/stripe/client.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
|
|
||||||
|
export const stripe = new Stripe(process.env.STRIPE_API_KEY!, {
|
||||||
|
apiVersion: "2022-11-15",
|
||||||
|
typescript: true,
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user