Compare commits
271 Commits
before-pre
...
docs/add-r
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b3696d879 | |||
| ca1522a31e | |||
| 201c0ac22a | |||
| c573e15ac2 | |||
| e5a80a701f | |||
| f8aebbc484 | |||
| 4e60d4ac09 | |||
| b5328eebde | |||
| e87c57c97c | |||
| cef5c8e33f | |||
| b03dd1553f | |||
| f6d1b8c8a1 | |||
| cb29ffef37 | |||
| f7d0bb9823 | |||
| fc2809c4cf | |||
| 14536bda1e | |||
| 5fcd54ae09 | |||
| 35fecfffa9 | |||
| da07bf135c | |||
| 1a06dd261c | |||
| 9c3fbbdb3a | |||
| c20123ec75 | |||
| 4c25e85f01 | |||
| ed3dbefbd7 | |||
| 7d8f83750b | |||
| fda3472e67 | |||
| 2dd89b1bc1 | |||
| cc87eeb8e0 | |||
| 6c78332258 | |||
| 5922725e1a | |||
| 36232c7817 | |||
| 723339f812 | |||
| f279b41b89 | |||
| 3d0836c39c | |||
| fcaa59699c | |||
| 5063b60a8a | |||
| 1322f7333f | |||
| 0278495896 | |||
| ef34c4fe5d | |||
| 6dad379943 | |||
| d3f82e1eb0 | |||
| 6838b953a8 | |||
| fdd5a6114e | |||
| b05ab9fbb4 | |||
| e34be16d3d | |||
| 9fd9bc2893 | |||
| 3f14d8007a | |||
| e3bc41934c | |||
| 13a840ff78 | |||
| fe6561f596 | |||
| 9cfbb1dec9 | |||
| 9dd8c2842c | |||
| 54a965e2b4 | |||
| 7cc1ae2de0 | |||
| 5ec97657c1 | |||
| f08836216e | |||
| 7184c47ac4 | |||
| 02f9c38e1e | |||
| aa651fb4e0 | |||
| 9ff8527336 | |||
| 79bd410687 | |||
| 3a0648c85d | |||
| 2b9a2ff250 | |||
| 4136811e32 | |||
| e9cee23c15 | |||
| 5d2349086d | |||
| c47e01b2b8 | |||
| 7c30ee0c3e | |||
| 6e2b05f835 | |||
| 8dc9c9d72d | |||
| 66b529a841 | |||
| 8293b50195 | |||
| 002b22b1a8 | |||
| 447bf0cb76 | |||
| 4e65ff3a47 | |||
| effe781ce7 | |||
| a1bb360b6f | |||
| 37c4e68aac | |||
| 11c1b6841f | |||
| c41007e026 | |||
| db99bf3674 | |||
| 3caa01ab53 | |||
| 20b618c70f | |||
| bbedd6d3de | |||
| 054480500f | |||
| 15b5f31a74 | |||
| a07febef46 | |||
| 316fb49339 | |||
| fc1b3be5ad | |||
| 79c037216d | |||
| aa584c1495 | |||
| 8b9738f6d5 | |||
| 20b51198b4 | |||
| f80edf3f94 | |||
| 08faabc813 | |||
| 0a7ed0701c | |||
| 488cf58f0e | |||
| dd4568b7fa | |||
| 19e960f593 | |||
| 893ab9bea5 | |||
| 2aaeab3217 | |||
| 0a5de18235 | |||
| b5e03359c1 | |||
| a266e4f9d4 | |||
| eccd9b5cd3 | |||
| fdbcf33210 | |||
| 6048555e4a | |||
| e33f31c483 | |||
| fe82e3c84f | |||
| 7684a49b7d | |||
| d8ad4b4b2b | |||
| e40ebd84d4 | |||
| a340b9c481 | |||
| 307b0cc9d9 | |||
| 3e94491474 | |||
| 007fe44db8 | |||
| 1e6f65f92d | |||
| 82fbedf8e3 | |||
| 37ded07a92 | |||
| df2294b43b | |||
| 2f3be1cfe5 | |||
| 8ecd5cf215 | |||
| f5091dd4d7 | |||
| 4c06b5e640 | |||
| b477799d70 | |||
| b928993510 | |||
| ad4d844b4d | |||
| 3444d7fd93 | |||
| 3e220135be | |||
| 095c391d45 | |||
| b0e4fa9e1d | |||
| f6bff1649b | |||
| b2b499f397 | |||
| eb18a7e11c | |||
| 89d9e02464 | |||
| a83b09f4db | |||
| e445830ffb | |||
| bfff81dd3c | |||
| 02129aab73 | |||
| e7386928fa | |||
| 7890b4adf1 | |||
| 6aa40b2547 | |||
| c142c1bd54 | |||
| 980bd0d485 | |||
| 17d51354d7 | |||
| 0881abdee4 | |||
| a300c3fb3a | |||
| 5e07b8bd92 | |||
| 7b1d626f9a | |||
| de46d0f4ab | |||
| cc7ab171b1 | |||
| 466941dbc2 | |||
| 0564792604 | |||
| 32f904ad68 | |||
| 748f842115 | |||
| ecaec356a1 | |||
| 38f730c730 | |||
| 2b4a9fbe21 | |||
| 106ac40fb1 | |||
| 62ac181193 | |||
| 9580100d66 | |||
| 38a8279757 | |||
| ed77000746 | |||
| 73b72c6cce | |||
| b2aa4d6587 | |||
| bde80bf2c9 | |||
| 1e505088ad | |||
| ae0799168a | |||
| b5ec3cc817 | |||
| 4a2162478e | |||
| 370f38457b | |||
| 1a5948c50e | |||
| 8bf6594cf4 | |||
| b6ff01ef86 | |||
| 9b993c08f1 | |||
| f34813e450 | |||
| 8f6c6dccf4 | |||
| 826704c21f | |||
| 4f47bbb552 | |||
| 825231fe2a | |||
| 051e681701 | |||
| 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 |
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
|
||||
27
.env.example
@ -1,8 +1,11 @@
|
||||
# Database
|
||||
# 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
|
||||
# Option 2: Set up a local Postgres SQL instance (RECOMMENDED)
|
||||
# ⚠ 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.
|
||||
# Option 3: Use the provided dx setup (RECOMMENDED)
|
||||
# => postgres://documenso:password@127.0.0.1:54320/documenso
|
||||
#
|
||||
# ⚠ WARNING: The test database can be reset or taken offline at any point.
|
||||
# ⚠ WARNING: Please be aware that nothing written to the test database is private.
|
||||
DATABASE_URL=''
|
||||
|
||||
# URL
|
||||
@ -13,6 +16,11 @@ NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
||||
NEXTAUTH_SECRET='lorem ipsum sit dolor random string for encryption this could literally be anything'
|
||||
NEXTAUTH_URL='http://localhost:3000'
|
||||
|
||||
# SIGNING
|
||||
CERT_FILE_PATH=
|
||||
CERT_PASSPHRASE=
|
||||
CERT_FILE_ENCODING=
|
||||
|
||||
# MAIL (NODEMAILER)
|
||||
# SENDGRID
|
||||
# Get a Sendgrid Api key here: https://signup.sendgrid.com
|
||||
@ -20,6 +28,12 @@ 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=''
|
||||
@ -28,6 +42,13 @@ SMTP_MAIL_PASSWORD=''
|
||||
# Sender for signing requests and completion mails.
|
||||
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.
|
||||
ALLOW_SIGNUP=true
|
||||
NEXT_PUBLIC_ALLOW_SIGNUP=true
|
||||
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=false
|
||||
|
||||
3
.gitignore
vendored
@ -36,3 +36,6 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
.env
|
||||
.env.example
|
||||
|
||||
# turborepo
|
||||
.turbo
|
||||
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "apps/website/documenso/website"]
|
||||
path = apps/website/documenso/website
|
||||
url = http://github.com/documenso/website.git
|
||||
12
.vscode/settings.json
vendored
@ -13,7 +13,13 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.removeUnusedImports": false
|
||||
},
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||
"spellright.language": ["de"],
|
||||
"spellright.documentTypes": ["markdown", "latex", "plaintext"]
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"spellright.language": [
|
||||
"de"
|
||||
],
|
||||
"spellright.documentTypes": [
|
||||
"markdown",
|
||||
"latex",
|
||||
"plaintext"
|
||||
]
|
||||
}
|
||||
|
||||
@ -10,17 +10,20 @@ If you plan to contribute to Documenso, please take a moment to feel awesome ✨
|
||||
|
||||
## Developing
|
||||
|
||||
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).
|
||||
- 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://documen.so/slack).
|
||||
- Use [Conventional Commits](https://www.conventionalcommits.org/) to keep everything nice and clean.
|
||||
- Choose your branch name using the issue you are working on and a coventional commit type.
|
||||
|
||||
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
|
||||
own GitHub account and then
|
||||
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
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 something readable):
|
||||
|
||||
```sh
|
||||
git checkout -b doc-999-my-feature-or-fix
|
||||
git checkout -b feat/doc-999-somefeature-that-rocks
|
||||
```
|
||||
|
||||
3. See the [Developer Setup](https://github.com/documenso/documenso/blob/main/README.md#developer-setup) for more setup details.
|
||||
|
||||
6
MANIFEST.md
Normal file
@ -0,0 +1,6 @@
|
||||
# The Documenso Manifest
|
||||
Signing documents is a fundamental building block of private, economic, and government interactions. Access to easy and secure signing to participate in society should therefore be a fundamental right for everyone. The technology to enable this should be accessible and widespread.
|
||||
|
||||
We know that open source is the key to solving this need once and for all to benefit all humankind. Using open source kickstarts innovation by putting the open sharing of ideas and solutions first. With Documenso, we will create an open and globally accessible signing platform to empower users, customers, and developers to fulfill their needs. Documenso is built by and for the global community, listening and implementing what is needed. Being transparent with the code and the processes that use it brings trust and security to the platform.
|
||||
|
||||
We build Documenso for longevity and scale by embracing the capital efficiency and inclusiveness of the Commercial Open Source (COSS) movement. We are building a global commodity for the world.
|
||||
176
README.md
@ -1,17 +1,15 @@
|
||||
<p align="center" style="margin-top: 12px">
|
||||
<p align="center" style="margin-top: 120px">
|
||||
<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>
|
||||
|
||||
<h3 align="center">Open Source Signing Infrastructure</h3>
|
||||
|
||||
<p align="center">
|
||||
The DocuSign Open Source Alternative.
|
||||
<br />
|
||||
The Open Source DocuSign Alternative.
|
||||
<br>
|
||||
<a href="https://documenso.com"><strong>Learn more »</strong></a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w">Slack</a>
|
||||
<a href="https://documen.so/discord">Discord</a>
|
||||
·
|
||||
<a href="https://documenso.com">Website</a>
|
||||
·
|
||||
@ -22,7 +20,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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://documen.so/discord"><img src="https://img.shields.io/badge/Discord-documen.so/discord-%235865F2" alt="Join Documenso on Discord"></a>
|
||||
<a href="https://github.com/documenso/documenso/stargazers"><img src="https://img.shields.io/github/stars/documenso/documenso" alt="Github Stars"></a>
|
||||
<a href="https://github.com/documenso/documenso/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
||||
<a href="https://github.com/documenso/documenso/pulse"><img src="https://img.shields.io/github/commit-activity/m/documenso/documenso" alt="Commits-per-month"></a>
|
||||
@ -56,13 +54,18 @@
|
||||
|
||||
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.
|
||||
|
||||
## Recognition
|
||||
|
||||
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily" alt="Documenso - The open source DocuSign alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=395047&theme=light" alt="Documenso - The Open Source DocuSign Alternative. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
## Community and Next Steps 🎯
|
||||
|
||||
The current project goal is to <b>[release a production ready version](https://github.com/documenso/documenso/milestone/1)</b> for self-hosting as soon as possible. If you want to help making that happen you can:
|
||||
We're currently working on a redesign of the application including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon.
|
||||
|
||||
- Check out the first source code release in this repository and test it
|
||||
- Tell us what you think in the current [Discussions](https://github.com/documenso/documenso/discussions)
|
||||
- Join the [Slack Channel](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w) for any questions and getting to know to other community members
|
||||
- Join the [Slack Channel](https://documen.so/slack) for any questions and getting to know to other community members
|
||||
- ⭐ the repository to help us raise awareness
|
||||
- Spread the word on Twitter, that Documenso is working towards a more open signing tool
|
||||
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
|
||||
@ -71,14 +74,18 @@ The current project goal is to <b>[release a production ready version](https://g
|
||||
|
||||
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Tools
|
||||
## Contact us
|
||||
|
||||
Contact us if you are interested in our Enterprise plan for large organizations that need extra flexibility and control.
|
||||
|
||||
<a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
||||
|
||||
# Tech
|
||||
|
||||
Documenso is built using awesome open source tech including:
|
||||
|
||||
- [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/)
|
||||
- [Postgres SQL (Database)](https://www.postgresql.org/)
|
||||
- [Prisma (ORM - Object-relational mapping)](https://www.prisma.io/)
|
||||
@ -86,7 +93,7 @@ Documenso is built using awesome open source tech including:
|
||||
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
|
||||
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
|
||||
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
|
||||
- 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.
|
||||
|
||||
# Getting Started
|
||||
@ -96,63 +103,150 @@ Documenso is built using awesome open source tech including:
|
||||
To run Documenso locally you need
|
||||
|
||||
- [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/)
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
```sh
|
||||
git clone https://github.com/documenso/documenso
|
||||
```
|
||||
- Run <code>npm i</code> in root directory
|
||||
- Rename <code>.env.example</code> to <code>.env</code>
|
||||
- Run `npm i` in root directory
|
||||
- Rename `.env.example` to `.env`
|
||||
- Set DATABASE_URL value in .env file
|
||||
- You can use the provided test database url (may be wiped at any point)
|
||||
- Or setup a local postgres sql instance (recommened)
|
||||
- Create the database scheme by running <code>db-migrate:dev</code>
|
||||
- Or setup a local postgres sql instance (recommended)
|
||||
- Create the database scheme by running `db-migrate:dev`
|
||||
- Setup your mail provider
|
||||
- Set <code>SENDGRID_API_KEY</code> value in .env file
|
||||
- Set `SENDGRID_API_KEY` value in .env file
|
||||
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
|
||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* varibles</code> in your .env
|
||||
- Run <code>npm run dev</code> root directory to start
|
||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the `SMTP
|
||||
\_
|
||||
* variables` in your .env
|
||||
- Run `npm run dev` root directory to start
|
||||
- Register a new user at http://localhost:3000/signup
|
||||
|
||||
---
|
||||
- Optional: Seed the database using <code>npm run db-seed</code> to create a test user and document
|
||||
- Optional: Upload and sign <code>apps\web\ressources\example.pdf</code> manually to test your setup
|
||||
|
||||
- Optional: Seed the database using `npm run db-seed` to create a test user and document
|
||||
- Optional: Upload and sign `apps/web/resources/example.pdf` manually to test your setup
|
||||
|
||||
- Optional: Create your own signing certificate
|
||||
- A demo certificate is provided in /app/web/ressources/certificate.p12
|
||||
- To generate you own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signging certificate**.
|
||||
- A demo certificate is provided in `/app/web/resources/certificate.p12`
|
||||
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](#creating-your-own-signing-certificate)**.
|
||||
|
||||
## 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:
|
||||
- If you pull the newest version from main, using `git pull`, it may be necessary to regenerate your database client
|
||||
- You can do this by running the generate command in `/packages/prisma`:
|
||||
```sh
|
||||
npx prisma generate
|
||||
```
|
||||
- This is not neccessary on first clone
|
||||
- This is not necessary on first clone.
|
||||
|
||||
# Creating your own signging certificate
|
||||
# Creating your own signing certificate
|
||||
|
||||
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:
|
||||
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
|
||||
|
||||
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:\
|
||||
<code>openssl genrsa -out private.key 2048</code>
|
||||
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:
|
||||
|
||||
`openssl genrsa -out private.key 2048`
|
||||
|
||||
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
|
||||
|
||||
`openssl req -new -x509 -key private.key -out certificate.crt -days 365`
|
||||
|
||||
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:\
|
||||
<code>openssl req -new -x509 -key private.key -out certificate.crt -days 365</code> \
|
||||
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
|
||||
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this: \
|
||||
<code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code>
|
||||
|
||||
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this:
|
||||
|
||||
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
|
||||
|
||||
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
||||
5. Place the certificate <code>/apps/web/ressource/certificate.p12</code>
|
||||
5. Place the certificate `/apps/web/resources/certificate.p12`
|
||||
|
||||
# Deploying - Coming Soon™
|
||||
# Docker
|
||||
|
||||
- Docker support
|
||||
- One-Click-Deploy on Render.com Deploy
|
||||
> 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.
|
||||
|
||||
# Deployment
|
||||
|
||||
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
||||
|
||||
## Railway
|
||||
|
||||
[](https://railway.app/template/DjrRRX)
|
||||
|
||||
## Render
|
||||
|
||||
[](https://render.com/deploy?repo=https://github.com/documenso/documenso)
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## I'm not receiving any emails when using the developer quickstart
|
||||
|
||||
When using the developer quickstart an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing email locally for you to view.
|
||||
|
||||
The Web UI can be found at http://localhost:9000 while the SMTP port will be on localhost:2500.
|
||||
|
||||
## Support IPv6
|
||||
|
||||
In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the NextJS start command
|
||||
|
||||
For local docker run
|
||||
|
||||
```bash
|
||||
docker run -it documenso:latest npm run start -- -H ::
|
||||
```
|
||||
|
||||
For k8s or docker-compose
|
||||
|
||||
```yaml
|
||||
containers:
|
||||
- name: documenso
|
||||
image: documenso:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- npm
|
||||
args:
|
||||
- run
|
||||
- start
|
||||
- --
|
||||
- -H
|
||||
- "::"
|
||||
```
|
||||
|
||||
@ -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
@ -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 everything 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
@ -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 Draggable from "react-draggable";
|
||||
import Logo from "../logo";
|
||||
import { IconButton } from "@documenso/ui";
|
||||
import Logo from "../logo";
|
||||
import { XCircleIcon } from "@heroicons/react/20/solid";
|
||||
import Draggable from "react-draggable";
|
||||
|
||||
const stc = require("string-to-color");
|
||||
|
||||
type FieldPropsType = {
|
||||
@ -51,21 +52,19 @@ export default function EditableField(props: FieldPropsType) {
|
||||
onMouseDown={(e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{/* width: 192 height 96 */}
|
||||
<div
|
||||
hidden={props.hidden}
|
||||
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={{
|
||||
background: stc(props.field.Recipient.email),
|
||||
}}
|
||||
>
|
||||
<div className="m-auto overflow-hidden flex-row-reverse text-lg font-bold text-center">
|
||||
}}>
|
||||
<div className="m-auto flex-row-reverse overflow-hidden text-center text-lg font-bold">
|
||||
{field.type}
|
||||
{field.type === "SIGNATURE" ? (
|
||||
<div className="text-xs text-center">
|
||||
<div className="text-center text-xs">
|
||||
{`${props.field.Recipient?.name} <${props.field.Recipient?.email}>`}
|
||||
</div>
|
||||
) : (
|
||||
@ -79,8 +78,7 @@ export default function EditableField(props: FieldPropsType) {
|
||||
icon={XCircleIcon}
|
||||
onClick={(event: any) => {
|
||||
props.onDelete(props.field.id);
|
||||
}}
|
||||
></IconButton>
|
||||
}}></IconButton>
|
||||
</strong>
|
||||
</div>
|
||||
</Draggable>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { RadioGroup } from "@headlessui/react";
|
||||
import { classNames } from "@documenso/lib";
|
||||
import { RadioGroup } from "@headlessui/react";
|
||||
import { FieldType } from "@prisma/client";
|
||||
|
||||
const stc = require("string-to-color");
|
||||
|
||||
export default function FieldTypeSelector(props: any) {
|
||||
@ -24,8 +25,7 @@ export default function FieldTypeSelector(props: any) {
|
||||
value={selectedFieldType}
|
||||
onChange={(e: any) => {
|
||||
setSelectedFieldType(e);
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<div className="space-y-4">
|
||||
{fieldTypes.map((fieldType) => (
|
||||
<RadioGroup.Option
|
||||
@ -37,30 +37,23 @@ export default function FieldTypeSelector(props: any) {
|
||||
className={({ checked, active }) =>
|
||||
classNames(
|
||||
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 }) => (
|
||||
<>
|
||||
<span className="flex items-center">
|
||||
<span className="flex flex-col text-sm">
|
||||
<RadioGroup.Label
|
||||
as="span"
|
||||
className="font-medium text-gray-900"
|
||||
>
|
||||
<RadioGroup.Label as="span" className="font-medium text-gray-900">
|
||||
<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={{
|
||||
background: stc(props.selectedRecipient?.email),
|
||||
}}
|
||||
/>
|
||||
<span className="align-middle">
|
||||
{" "}
|
||||
{
|
||||
fieldTypes.filter((e) => e.id === fieldType.id)[0]
|
||||
.name
|
||||
}
|
||||
{fieldTypes.filter((e) => e.id === fieldType.id)[0].name}
|
||||
</span>
|
||||
</RadioGroup.Label>
|
||||
</span>
|
||||
|
||||
@ -1,13 +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 { createOrUpdateField, deleteField } from "@documenso/lib/api";
|
||||
import { createField } from "@documenso/features/editor";
|
||||
import RecipientSelector from "./recipient-selector";
|
||||
import FieldTypeSelector from "./field-type-selector";
|
||||
import { InformationCircleIcon } from "@heroicons/react/24/outline";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { createField } from "@documenso/features/editor";
|
||||
import { createOrUpdateField, deleteField } from "@documenso/lib/api";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
||||
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 PDFViewer = dynamic(() => import("./pdf-viewer"), {
|
||||
@ -20,8 +21,7 @@ export default function PDFEditor(props: any) {
|
||||
const [selectedRecipient, setSelectedRecipient]: any = useState();
|
||||
const [selectedFieldType, setSelectedFieldType] = useState();
|
||||
const noRecipients =
|
||||
props?.document.Recipient.length === 0 ||
|
||||
props?.document.Recipient.every((e: any) => !e.email);
|
||||
props?.document.Recipient.length === 0 || props?.document.Recipient.every((e: any) => !e.email);
|
||||
|
||||
function onPositionChangedHandler(position: any, id: any) {
|
||||
if (!position) return;
|
||||
@ -30,7 +30,7 @@ export default function PDFEditor(props: any) {
|
||||
movedField.positionY = position.y.toFixed(0);
|
||||
createOrUpdateField(props.document, movedField);
|
||||
|
||||
// no instant redraw neccessary, postion information for saving or later rerender is enough
|
||||
// no instant redraw necessary, position information for saving or later rerender is enough
|
||||
// setFields(newFields);
|
||||
}
|
||||
|
||||
@ -53,26 +53,16 @@ export default function PDFEditor(props: any) {
|
||||
<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"
|
||||
/>
|
||||
<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.
|
||||
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"
|
||||
>
|
||||
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>
|
||||
@ -98,12 +88,10 @@ export default function PDFEditor(props: any) {
|
||||
}}
|
||||
onMouseDown={(e: any, page: number) => {
|
||||
if (e.button === 0) addField(e, page);
|
||||
}}
|
||||
></PDFViewer>
|
||||
}}></PDFViewer>
|
||||
<div
|
||||
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
|
||||
recipients={props?.document?.Recipient}
|
||||
onChange={setSelectedRecipient}
|
||||
@ -123,12 +111,7 @@ export default function PDFEditor(props: any) {
|
||||
if (!selectedFieldType) return;
|
||||
if (noRecipients) return;
|
||||
|
||||
const signatureField = createField(
|
||||
e,
|
||||
page,
|
||||
selectedRecipient,
|
||||
selectedFieldType
|
||||
);
|
||||
const signatureField = createField(e, page, selectedRecipient, selectedFieldType);
|
||||
|
||||
createOrUpdateField(props?.document, signatureField).then((res) => {
|
||||
setFields((prevState) => [...prevState, res]);
|
||||
|
||||
@ -1,21 +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 { Button } from "@documenso/ui";
|
||||
import {
|
||||
CheckBadgeIcon,
|
||||
InformationCircleIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { FieldType } from "@prisma/client";
|
||||
import {
|
||||
createOrUpdateField,
|
||||
deleteField,
|
||||
signDocument,
|
||||
} from "@documenso/lib/api";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useRouter } from "next/router";
|
||||
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"), {
|
||||
ssr: false,
|
||||
@ -27,9 +20,7 @@ export default function PDFSigner(props: any) {
|
||||
const [signingDone, setSigningDone] = useState(false);
|
||||
const [localSignatures, setLocalSignatures] = useState<any[]>([]);
|
||||
const [fields, setFields] = useState<any[]>(props.fields);
|
||||
const signatureFields = fields.filter(
|
||||
(field) => field.type === FieldType.SIGNATURE
|
||||
);
|
||||
const signatureFields = fields.filter((field) => field.type === FieldType.SIGNATURE);
|
||||
const [dialogField, setDialogField] = useState<any>();
|
||||
|
||||
useEffect(() => {
|
||||
@ -79,34 +70,32 @@ export default function PDFSigner(props: any) {
|
||||
<SignatureDialog open={open} setOpen={setOpen} onClose={onDialogClose} />
|
||||
<div className="bg-neon p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<Logo className="h-12 w-12 -mt-2.5"></Logo>
|
||||
<div className="flex-shrink-0 flex gap-x-2 items-center">
|
||||
<Logo className="h-8 w-8 text-black" />
|
||||
<h2 className="text-2xl font-semibold">Documenso</h2>
|
||||
</div>
|
||||
<div className="ml-3 flex-1 md:flex md:justify-between text-center justify-start items-center">
|
||||
|
||||
<div className="mx-3 flex-1 items-center justify-start text-center md:flex md:justify-between">
|
||||
<p className="text-lg text-slate-700">
|
||||
{props.document.User.name
|
||||
? `${props.document.User.name} (${props.document.User.email})`
|
||||
: props.document.User.email}{" "}
|
||||
would like you to sign this document.
|
||||
</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
|
||||
disabled={!signingDone}
|
||||
color="secondary"
|
||||
icon={CheckBadgeIcon}
|
||||
className="float-right"
|
||||
onClick={() => {
|
||||
signDocument(
|
||||
props.document,
|
||||
localSignatures,
|
||||
`${router.query.token}`
|
||||
).then(() => {
|
||||
router.push(
|
||||
`/documents/${props.document.id}/signed?token=${router.query.token}`
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
signDocument(props.document, localSignatures, `${router.query.token}`).then(
|
||||
() => {
|
||||
router.push(
|
||||
`/documents/${props.document.id}/signed?token=${router.query.token}`
|
||||
);
|
||||
}
|
||||
);
|
||||
}}>
|
||||
Done
|
||||
</Button>
|
||||
</p>
|
||||
@ -117,15 +106,11 @@ export default function PDFSigner(props: any) {
|
||||
<div className="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"
|
||||
/>
|
||||
<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">
|
||||
You can sign this document anywhere you like, but maybe look for
|
||||
a signature line.
|
||||
You can sign this document anywhere you like, but maybe look for a signature line.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -144,12 +129,10 @@ export default function PDFSigner(props: any) {
|
||||
pdfUrl={`${NEXT_PUBLIC_WEBAPP_URL}/api/documents/${router.query.id}?token=${router.query.token}`}
|
||||
onClick={onClick}
|
||||
onMouseDown={function onMouseDown(e: any, page: number) {
|
||||
if (signatureFields.length === 0)
|
||||
addFreeSignature(e, page, props.recipient);
|
||||
if (signatureFields.length === 0) addFreeSignature(e, page, props.recipient);
|
||||
}}
|
||||
onMouseUp={() => {}}
|
||||
onDelete={onDeleteHandler}
|
||||
></PDFViewer>
|
||||
onDelete={onDeleteHandler}></PDFViewer>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -166,18 +149,9 @@ export default function PDFSigner(props: any) {
|
||||
}
|
||||
|
||||
function addFreeSignature(e: any, page: number, recipient: any): any {
|
||||
const freeSignatureField = createField(
|
||||
e,
|
||||
page,
|
||||
recipient,
|
||||
FieldType.FREE_SIGNATURE
|
||||
);
|
||||
const freeSignatureField = createField(e, page, recipient, FieldType.FREE_SIGNATURE);
|
||||
|
||||
createOrUpdateField(
|
||||
props.document,
|
||||
freeSignatureField,
|
||||
recipient.token
|
||||
).then((res) => {
|
||||
createOrUpdateField(props.document, freeSignatureField, recipient.token).then((res) => {
|
||||
setFields((prevState) => [...prevState, res]);
|
||||
setDialogField(res);
|
||||
setOpen(true);
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
|
||||
import EditableField from "./editable-field";
|
||||
import SignableField from "./signable-field";
|
||||
import short from "short-uuid";
|
||||
import { FieldType } from "@prisma/client";
|
||||
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
|
||||
import short from "short-uuid";
|
||||
|
||||
export default function PDFViewer(props) {
|
||||
const [numPages, setNumPages] = useState(null);
|
||||
@ -33,16 +33,14 @@ export default function PDFViewer(props) {
|
||||
<div
|
||||
hidden={loading}
|
||||
onMouseUp={props.onMouseUp}
|
||||
style={{ height: numPages * pageHeight + 1000 }}
|
||||
>
|
||||
<div className="max-w-xs mt-6"></div>
|
||||
style={{ height: numPages * pageHeight + 1000 }}>
|
||||
<div className="mt-6 max-w-xs"></div>
|
||||
<Document
|
||||
file={props.pdfUrl}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
options={options}
|
||||
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) => (
|
||||
<Fragment key={short.generate().toString()}>
|
||||
<div
|
||||
@ -57,8 +55,7 @@ export default function PDFViewer(props) {
|
||||
position: "relative",
|
||||
...props.style,
|
||||
}}
|
||||
className="mx-auto w-fit"
|
||||
>
|
||||
className="mx-auto w-fit">
|
||||
<Page
|
||||
className="mt-5"
|
||||
key={`page_${index + 1}`}
|
||||
@ -69,8 +66,7 @@ export default function PDFViewer(props) {
|
||||
if (e.height) setPageHeight(e.height);
|
||||
setLoading(false);
|
||||
}}
|
||||
onRenderError={() => setLoading(false)}
|
||||
></Page>
|
||||
onRenderError={() => setLoading(false)}></Page>
|
||||
{props?.fields
|
||||
.filter((field) => field.page === index)
|
||||
.map((field) =>
|
||||
@ -80,8 +76,7 @@ export default function PDFViewer(props) {
|
||||
key={field.id}
|
||||
field={field}
|
||||
className="absolute"
|
||||
onDelete={onDeleteHandler}
|
||||
></SignableField>
|
||||
onDelete={onDeleteHandler}></SignableField>
|
||||
) : (
|
||||
<EditableField
|
||||
hidden={
|
||||
@ -93,8 +88,7 @@ export default function PDFViewer(props) {
|
||||
field={field}
|
||||
className="absolute"
|
||||
onPositionChanged={onPositionChangedHandler}
|
||||
onDelete={onDeleteHandler}
|
||||
></EditableField>
|
||||
onDelete={onDeleteHandler}></EditableField>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { classNames } from "@documenso/lib";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline";
|
||||
import { classNames } from "@documenso/lib";
|
||||
|
||||
const stc = require("string-to-color");
|
||||
|
||||
export default function RecipientSelector(props: any) {
|
||||
const [selectedRecipient, setSelectedRecipient]: any = useState(
|
||||
props?.recipients[0]
|
||||
);
|
||||
const [selectedRecipient, setSelectedRecipient]: any = useState(props?.recipients[0]);
|
||||
|
||||
useEffect(() => {
|
||||
props.onChange(selectedRecipient);
|
||||
@ -18,11 +17,10 @@ export default function RecipientSelector(props: any) {
|
||||
value={selectedRecipient}
|
||||
onChange={(e: any) => {
|
||||
setSelectedRecipient(e);
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{({ open }) => (
|
||||
<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="inline-block h-4 w-4 flex-shrink-0 rounded-full"
|
||||
@ -33,10 +31,7 @@ export default function RecipientSelector(props: any) {
|
||||
</span>
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
@ -45,20 +40,19 @@ export default function RecipientSelector(props: any) {
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-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">
|
||||
{props?.recipients.map((recipient: any) => (
|
||||
<Listbox.Option
|
||||
key={recipient?.id}
|
||||
disabled={!recipient?.email}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "text-white bg-neon-dark" : "text-gray-900",
|
||||
"relative cursor-default select-none py-2 pl-3 pr-9"
|
||||
active ? "bg-neon-dark text-white" : "text-gray-900",
|
||||
"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 }) => (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
@ -72,9 +66,8 @@ export default function RecipientSelector(props: any) {
|
||||
className={classNames(
|
||||
selected ? "font-semibold" : "font-normal",
|
||||
"ml-3 block truncate"
|
||||
)}
|
||||
>
|
||||
{`${selectedRecipient?.name} <${selectedRecipient?.email}>`}
|
||||
)}>
|
||||
{`${recipient?.name} <${recipient?.email || 'unknown'}>`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -83,9 +76,8 @@ export default function RecipientSelector(props: any) {
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-neon-dark",
|
||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
)}>
|
||||
<CheckIcon className="h-5 w-5" strokeWidth={3} aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import Draggable from "react-draggable";
|
||||
import { classNames } from "@documenso/lib";
|
||||
import { IconButton } from "@documenso/ui";
|
||||
import { XCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { classNames } from "@documenso/lib";
|
||||
import Draggable from "react-draggable";
|
||||
|
||||
const stc = require("string-to-color");
|
||||
|
||||
type FieldPropsType = {
|
||||
@ -37,31 +38,26 @@ export default function SignableField(props: FieldPropsType) {
|
||||
onMouseDown={(e: any) => {
|
||||
// e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<div
|
||||
onClick={(e: any) => {
|
||||
if (!field?.signature) props.onClick(props.field);
|
||||
}}
|
||||
ref={nodeRef}
|
||||
className={classNames(
|
||||
"opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none",
|
||||
field.type === "SIGNATURE"
|
||||
? "cursor-pointer hover:brightness-50"
|
||||
: "cursor-not-allowed"
|
||||
"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={{
|
||||
background: stc(props.field.Recipient.email),
|
||||
}}
|
||||
>
|
||||
<div hidden={field?.signature} className="font-medium my-4">
|
||||
}}>
|
||||
<div hidden={field?.signature} className="my-4 font-medium">
|
||||
{field.type === "SIGNATURE" ? "SIGN HERE" : ""}
|
||||
{field.type === "DATE" ? <small>Date (filled on sign)</small> : ""}
|
||||
</div>
|
||||
<div
|
||||
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" ? (
|
||||
<div className="my-4">{field?.signature.typedSignature}</div>
|
||||
) : (
|
||||
@ -69,7 +65,7 @@ export default function SignableField(props: FieldPropsType) {
|
||||
)}
|
||||
|
||||
{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 { localStorage } from "@documenso/lib";
|
||||
import { Button, IconButton } from "@documenso/ui";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import {
|
||||
LanguageIcon,
|
||||
PencilIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { LanguageIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import SignatureCanvas from "react-signature-canvas";
|
||||
import { localStorage } from "@documenso/lib";
|
||||
import { useDebouncedValue } from "../../hooks/use-debounced-value";
|
||||
|
||||
const tabs = [
|
||||
{ name: "Type", icon: LanguageIcon, current: true },
|
||||
@ -19,6 +16,9 @@ export default function SignatureDialog(props: any) {
|
||||
const [currentTab, setCurrentTab] = useState(tabs[0]);
|
||||
const [typedSignature, setTypedSignature] = useState("");
|
||||
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 occurring.
|
||||
const showCanvas = useDebouncedValue<boolean>(props.open, 1);
|
||||
let signCanvasRef: any | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
@ -34,8 +34,7 @@ export default function SignatureDialog(props: any) {
|
||||
onClose={() => {
|
||||
props.setOpen(false);
|
||||
setCurrent(tabs[0]);
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -43,8 +42,7 @@ export default function SignatureDialog(props: any) {
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
leaveTo="opacity-0">
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
@ -57,11 +55,10 @@ export default function SignatureDialog(props: any) {
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<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">
|
||||
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">
|
||||
<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">
|
||||
{tabs.map((tab) => (
|
||||
<a
|
||||
@ -72,11 +69,10 @@ export default function SignatureDialog(props: any) {
|
||||
className={classNames(
|
||||
tab.current
|
||||
? "border-neon text-neon"
|
||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
|
||||
"group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm cursor-pointer"
|
||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
||||
"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
|
||||
className={classNames(
|
||||
tab.current
|
||||
@ -93,7 +89,7 @@ export default function SignatureDialog(props: any) {
|
||||
</div>
|
||||
{isCurrentTab("Type") ? (
|
||||
<div>
|
||||
<div className="my-8 border-b border-gray-300 mb-3">
|
||||
<div className="my-7 mb-3 border-b border-gray-300">
|
||||
<input
|
||||
value={typedSignature}
|
||||
onChange={(e) => {
|
||||
@ -101,36 +97,31 @@ export default function SignatureDialog(props: any) {
|
||||
}}
|
||||
className={classNames(
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div className="float-right">
|
||||
<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"
|
||||
disabled={!typedSignature}
|
||||
onClick={() => {
|
||||
localStorage.setItem(
|
||||
"typedSignature",
|
||||
typedSignature
|
||||
);
|
||||
localStorage.setItem("typedSignature", typedSignature);
|
||||
props.onClose({
|
||||
type: "type",
|
||||
typedSignature: typedSignature,
|
||||
});
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
Sign
|
||||
</Button>
|
||||
</div>
|
||||
@ -139,52 +130,55 @@ export default function SignatureDialog(props: any) {
|
||||
""
|
||||
)}
|
||||
{isCurrentTab("Draw") ? (
|
||||
<div className="">
|
||||
<SignatureCanvas
|
||||
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]);
|
||||
<div className="" key={props.open ? "closed" : "open"}>
|
||||
{showCanvas && (
|
||||
<SignatureCanvas
|
||||
ref={(ref) => {
|
||||
signCanvasRef = ref;
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="ml-3"
|
||||
onClick={() => {
|
||||
props.onClose({
|
||||
type: "draw",
|
||||
signatureImage:
|
||||
signCanvasRef.toDataURL("image/png"),
|
||||
});
|
||||
canvasProps={{
|
||||
className: "sigCanvas border-b b-2 border-slate w-full h-full mb-3",
|
||||
}}
|
||||
disabled={signatureEmpty}
|
||||
>
|
||||
Sign
|
||||
</Button>
|
||||
clearOnResize={true}
|
||||
onEnd={() => {
|
||||
setSignatureEmpty(signCanvasRef?.isEmpty());
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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>
|
||||
) : (
|
||||
@ -200,11 +194,11 @@ export default function SignatureDialog(props: any) {
|
||||
</>
|
||||
);
|
||||
|
||||
function isCurrentTab(tabName: string): boolean {
|
||||
function isCurrentTab(tabName: string): boolean {
|
||||
return currentTab.name === tabName;
|
||||
}
|
||||
|
||||
function setCurrent(t: any) {
|
||||
function setCurrent(t: any) {
|
||||
tabs.forEach((tab) => {
|
||||
tab.current = tab.name === t.name;
|
||||
});
|
||||
|
||||
115
apps/web/components/forgot-password.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@documenso/ui";
|
||||
import Logo from "./logo";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
interface ForgotPasswordForm {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export default function ForgotPassword() {
|
||||
const { register, formState, resetField, handleSubmit } = useForm<ForgotPasswordForm>();
|
||||
const [resetSuccessful, setResetSuccessful] = useState(false);
|
||||
|
||||
const onSubmit = async (values: ForgotPasswordForm) => {
|
||||
const response = await toast.promise(
|
||||
fetch(`/api/auth/forgot-password`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(values),
|
||||
}),
|
||||
{
|
||||
loading: "Sending...",
|
||||
success: "Reset link sent.",
|
||||
error: "Could not send reset link :/",
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
toast.dismiss();
|
||||
|
||||
if (response.status == 404) {
|
||||
toast.error("Email address not found.");
|
||||
}
|
||||
|
||||
if (response.status == 400) {
|
||||
toast.error("Password reset requested.");
|
||||
}
|
||||
|
||||
if (response.status == 500) {
|
||||
toast.error("Something went wrong.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
setResetSuccessful(true);
|
||||
}
|
||||
|
||||
resetField("email");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div>
|
||||
<Logo className="mx-auto h-20 w-auto"></Logo>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
|
||||
{resetSuccessful ? "Reset Password" : "Forgot Password?"}
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
{resetSuccessful
|
||||
? "Please check your email for reset instructions."
|
||||
: "No worries, we'll send you reset instructions."}
|
||||
</p>
|
||||
</div>
|
||||
{!resetSuccessful && (
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="-space-y-px rounded-md shadow-sm">
|
||||
<div>
|
||||
<label htmlFor="email-address" className="sr-only">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
{...register("email")}
|
||||
id="email-address"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formState.isSubmitting}
|
||||
className="group relative flex w-full">
|
||||
Reset password
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
<div>
|
||||
<Link href="/login">
|
||||
<div className="relative mt-10 flex items-center justify-center gap-2 text-sm text-gray-500 hover:cursor-pointer hover:text-gray-900">
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
Back to log in
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,9 +1,13 @@
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
||||
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
import { BillingWarning } from "./billing-warning";
|
||||
import Navigation from "./navigation";
|
||||
import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
||||
import { SubscriptionStatus } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
function useRedirectToLoginIfUnauthenticated() {
|
||||
const { data: session, status } = useSession();
|
||||
@ -31,11 +35,16 @@ function useRedirectToLoginIfUnauthenticated() {
|
||||
export default function Layout({ children }: any) {
|
||||
useRedirectToLoginIfUnauthenticated();
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-full">
|
||||
<Navigation></Navigation>
|
||||
<Navigation />
|
||||
|
||||
<main>
|
||||
<BillingWarning />
|
||||
|
||||
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">{children}</div>
|
||||
</main>
|
||||
</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 Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
||||
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 {
|
||||
email: string;
|
||||
@ -22,10 +21,7 @@ export default function Login(props: any) {
|
||||
const methods = useForm<LoginValues>();
|
||||
const { register, formState } = methods;
|
||||
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 (!/^https?:\/\//.test(callbackUrl)) {
|
||||
@ -73,16 +69,13 @@ export default function Login(props: any) {
|
||||
<div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div>
|
||||
<Logo className="mx-auto h-10 w-auto"></Logo>
|
||||
<Logo className="mx-auto h-20 w-auto text-black"></Logo>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
|
||||
Sign in to your account
|
||||
</h2>
|
||||
</div>
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className="mt-8 space-y-6"
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
>
|
||||
<form className="mt-8 space-y-6" onSubmit={methods.handleSubmit(onSubmit)}>
|
||||
<input type="hidden" name="remember" defaultValue="true" />
|
||||
<div className="-space-y-px rounded-md shadow-sm">
|
||||
<div>
|
||||
@ -96,7 +89,7 @@ export default function Login(props: any) {
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@ -111,27 +104,28 @@ export default function Login(props: any) {
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm">
|
||||
<a href="#" className="font-medium text-neon hover:text-neon">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="hover:text-neon-700 font-medium text-gray-500">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
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">
|
||||
<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 duration-200 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
@ -140,10 +134,7 @@ export default function Login(props: any) {
|
||||
</div>
|
||||
<div>
|
||||
<div className="relative">
|
||||
<div
|
||||
className="absolute inset-0 flex items-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center"></div>
|
||||
@ -154,8 +145,7 @@ export default function Login(props: any) {
|
||||
Are you new here?{" "}
|
||||
<Link
|
||||
href="/signup"
|
||||
className="font-medium text-neon hover:text-neon"
|
||||
>
|
||||
className="hover:text-neon-700 font-medium text-gray-500 duration-200">
|
||||
Create a new Account
|
||||
</Link>
|
||||
</p>
|
||||
@ -164,9 +154,8 @@ export default function Login(props: any) {
|
||||
Like Documenso{" "}
|
||||
<Link
|
||||
href="https://documenso.com"
|
||||
className="font-medium text-neon hover:text-neon"
|
||||
>
|
||||
Hosted Documenso will be availible soon™
|
||||
className="text-neon hover:text-neon font-medium">
|
||||
Hosted Documenso is here!
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -1,28 +1,80 @@
|
||||
import { classNames } from "@documenso/lib";
|
||||
import Link from "next/link";
|
||||
import { classNames } from "@documenso/lib";
|
||||
|
||||
export default function Logo(props: any) {
|
||||
return (
|
||||
<>
|
||||
<Link href="/dashboard">
|
||||
<svg
|
||||
className="w-12"
|
||||
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)">
|
||||
<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"
|
||||
className={classNames(props.dark ? "fill-white" : "fill-brown")}
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</Link>
|
||||
<svg viewBox="0 0 64 64" {...props}>
|
||||
<rect width="88.6758041381836" height="32.18000030517578" fill="transparent"></rect>
|
||||
<path
|
||||
d="M27.07 9.25832C26.333 9.92796 25.5176 10.7145 24.5857 11.6341C23.9957 12.0973 23.2682 12.3587 22.5117 12.3733L19.3896 12.4333L20.2992 11.5237C25.815 6.0079 28.5729 3.25 32 3.25C35.4271 3.25 38.185 6.00789 43.7008 11.5237L44.6087 12.4317L41.5937 12.3749C40.7437 12.3588 39.9292 12.0311 39.3051 11.4539L37.4972 9.78198C37.3255 9.6212 37.1581 9.46631 36.9946 9.31712L36.897 9.22687L36.8953 9.22687C36.2778 8.667 35.7153 8.18958 35.1851 7.78508C33.6538 6.6167 32.7624 6.35263 32 6.35263C31.2376 6.35263 30.3462 6.6167 28.8149 7.78508C28.2783 8.19451 27.7085 8.67864 27.0821 9.24737L27.0814 9.24737L27.07 9.25832Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M54.6826 27.0051C54.5337 26.8419 54.3791 26.6748 54.2187 26.5035L52.5459 24.6946C51.9691 24.0709 51.6413 23.2571 51.6249 22.4077L51.5667 19.3896L52.4763 20.2992C57.9921 25.815 60.75 28.5729 60.75 32C60.75 35.4271 57.9921 38.185 52.4763 43.7008L51.5667 44.6104L51.6249 41.5923C51.6413 40.7429 51.9691 39.9291 52.5459 39.3054L54.2185 37.4968C54.379 37.3253 54.5337 37.1581 54.6827 36.9948L54.7731 36.897V36.8953C55.333 36.2778 55.8104 35.7153 56.2149 35.1851C57.3833 33.6538 57.6474 32.7624 57.6474 32C57.6474 31.2376 57.3833 30.3462 56.2149 28.8149C55.8104 28.2847 55.333 27.7222 54.7731 27.1047V27.103L54.6826 27.0051Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M36.9601 54.7143C37.1446 54.5464 37.334 54.3711 37.5289 54.1883L39.3054 52.5457C39.9294 51.9687 40.7435 51.6411 41.5932 51.6249L44.6096 51.5675L43.7008 52.4763C38.185 57.9921 35.4271 60.75 32 60.75C28.5729 60.75 25.815 57.9921 20.2992 52.4763L19.3896 51.5667L22.4141 51.6248C23.2599 51.641 24.0705 51.9659 24.6934 52.5383L25.9131 53.6592C27.0267 54.726 27.9626 55.5647 28.8149 56.2149C30.3462 57.3833 31.2376 57.6474 32 57.6474C32.7624 57.6474 33.6538 57.3833 35.1851 56.2149C35.7217 55.8055 36.2915 55.3214 36.9179 54.7526H36.9187L36.9601 54.7143Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M9.26202 36.9341C9.44675 37.1373 9.64036 37.3465 9.8432 37.5625L11.4547 39.3051C12.0317 39.929 12.3594 40.7431 12.3756 41.5927L12.4333 44.6104L11.5237 43.7008C6.0079 38.185 3.25 35.4271 3.25 32C3.25 28.5729 6.00789 25.815 11.5237 20.2992L12.4325 19.3904L12.3754 22.4067C12.3593 23.2567 12.0314 24.0711 11.4541 24.6952L9.79271 26.4913C9.62762 26.6675 9.46871 26.8392 9.3158 27.0069L9.22687 27.103L9.22687 27.1047C8.66699 27.7222 8.18958 28.2847 7.78508 28.8149C6.6167 30.3462 6.35263 31.2376 6.35263 32C6.35263 32.7624 6.6167 33.6538 7.78508 35.1851C8.1946 35.7219 8.67887 36.2918 9.24777 36.9184L9.24777 36.9187L9.26202 36.9341Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M9.24777 27.0804L11.4541 24.6952C11.9658 24.1421 12.2815 23.4395 12.3579 22.6951C12.367 21.4688 12.387 20.3991 12.4313 19.4536L12.4337 19.3242L12.4377 19.3202C12.4785 18.5034 12.5382 17.7805 12.6257 17.1297C12.8823 15.2207 13.3259 14.4037 13.865 13.8646C14.4041 13.3255 15.2211 12.882 17.1301 12.6253C17.7929 12.5362 18.5306 12.4759 19.3661 12.4351L19.3675 12.4337L19.4131 12.4329C20.3923 12.3861 21.5054 12.3657 22.7886 12.3569C23.5626 12.2798 24.2914 11.9441 24.8545 11.3998L27.0813 9.24742H25.7951C17.9946 9.24742 14.0944 9.24742 11.6711 11.6707C9.24777 14.094 9.24777 17.9943 9.24777 25.7948V27.0804Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M9.24777 36.9187V38.2053C9.24777 46.0058 9.24777 49.9061 11.6711 52.3294C14.0944 54.7527 17.9946 54.7527 25.7951 54.7527H38.2057C46.0062 54.7527 49.9064 54.7527 52.3297 52.3294C54.753 49.9061 54.753 46.0058 54.753 38.2053V36.9187L52.5459 39.3054C52.0356 39.8571 51.7203 40.5577 51.643 41.3C51.6337 42.5529 51.613 43.6424 51.5668 44.603L51.5663 44.6325L51.5654 44.6334C51.5246 45.4693 51.4643 46.2073 51.3752 46.8704C51.1185 48.7794 50.6749 49.5964 50.1358 50.1355C49.5967 50.6746 48.7797 51.1181 46.8707 51.3748C46.2197 51.4623 45.4965 51.522 44.6793 51.5628L44.6758 51.5663L44.5626 51.5684C43.6127 51.6132 42.5373 51.6334 41.3032 51.6426C40.5597 51.7193 39.858 52.0347 39.3054 52.5457L36.9187 54.7526L27.103 54.7526L24.6934 52.5383C24.1424 52.032 23.4445 51.7193 22.7052 51.6426C21.4558 51.6334 20.3688 51.6129 19.4101 51.5671L19.3675 51.5663L19.3662 51.565C18.5307 51.5242 17.7929 51.4639 17.1301 51.3748C15.2211 51.1181 14.4041 50.6746 13.865 50.1355C13.3259 49.5964 12.8823 48.7794 12.6257 46.8704C12.5365 46.2075 12.4763 45.4698 12.4355 44.6342L12.4337 44.6325L12.4326 44.5753C12.3874 43.6221 12.367 42.5422 12.3579 41.3022C12.281 40.559 11.9655 39.8575 11.4547 39.3051L9.24777 36.9187Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M51.643 22.7C51.7203 23.4423 52.0356 24.1428 52.5459 24.6946L54.753 27.0813V25.7948C54.753 17.9943 54.753 14.094 52.3297 11.6707C49.9064 9.24742 46.0062 9.24742 38.2057 9.24742H36.9192L39.3051 11.4539C39.8586 11.9658 40.5618 12.2815 41.3067 12.3575C42.5257 12.3666 43.5898 12.3865 44.531 12.4302L44.7192 12.4337L44.725 12.4396C45.5235 12.4803 46.2319 12.5394 46.8707 12.6253C48.7797 12.882 49.5967 13.3255 50.1358 13.8646C50.6749 14.4037 51.1185 15.2207 51.3752 17.1297C51.4643 17.7928 51.5246 18.5307 51.5654 19.3666L51.5663 19.3675L51.5668 19.3971C51.613 20.3577 51.6337 21.4471 51.643 22.7Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M29.6453 18.2543L27.5526 20.0304C27.1792 20.3474 26.7112 20.5317 26.2219 20.5545L22.7195 20.7177L24.5458 18.8913C28.071 15.3661 29.8336 13.6035 32.0239 13.6035C34.2142 13.6035 35.9768 15.3661 39.502 18.8913L41.3172 20.7065L37.7657 20.5526C37.2678 20.531 36.7917 20.3422 36.4143 20.0167L34.8345 18.6538C34.2799 18.1319 33.8096 17.7194 33.3805 17.392C32.5014 16.7212 32.168 16.706 32.0239 16.7059C31.8799 16.7059 31.5465 16.7212 30.6674 17.392C30.6533 17.4027 30.6393 17.4135 30.6252 17.4243L30.6232 17.4243L30.6024 17.4419C30.3079 17.6703 29.9934 17.9385 29.6453 18.2543Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M46.4935 30.4715C45.8957 29.7157 45.0376 28.8234 43.7954 27.5741C43.5923 27.2496 43.4753 26.8756 43.4596 26.4879L43.306 22.6953L45.1106 24.4999C48.6358 28.0251 50.3985 29.7878 50.3985 31.978C50.3985 34.1683 48.6358 35.9309 45.1106 39.4561L43.2963 41.2705L43.4711 37.6457C43.4954 37.142 43.6908 36.6617 44.025 36.284L45.352 34.7845C45.7709 34.3392 46.1192 33.9484 46.4095 33.5895L46.573 33.4048V33.3829C46.5854 33.3667 46.5978 33.3506 46.61 33.3346C47.2808 32.4555 47.296 32.1221 47.296 31.978C47.296 31.834 47.2808 31.5006 46.61 30.6215C46.5978 30.6054 46.5854 30.5894 46.573 30.5732V30.555L46.4935 30.4715Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M17.4826 30.5696L19.902 27.9386C20.2447 27.566 20.4494 27.0873 20.4821 26.5821L20.616 24.5168C20.6363 23.8629 20.6699 23.3105 20.7254 22.829L20.7349 22.6825L20.7445 22.673C20.7466 22.6564 20.7488 22.6398 20.751 22.6234C20.8983 21.5275 21.1233 21.2809 21.2251 21.1791C21.327 21.0772 21.5736 20.8523 22.6695 20.7049C23.0213 20.6576 23.4118 20.6238 23.8544 20.5996L26.8259 20.3064C27.3027 20.2594 27.7513 20.0591 28.1046 19.7357L30.6159 17.4365H28.0583C23.0729 17.4365 20.5802 17.4365 19.0314 18.9853C17.712 20.3048 17.5166 22.3093 17.4877 25.9566C17.4826 26.5905 17.4826 27.274 17.4826 28.0122L17.4826 30.5632V30.5696Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M17.4826 33.3865V33.3957L17.4826 35.9439C17.4826 36.6821 17.4826 37.3656 17.4877 37.9995C17.5166 41.6468 17.7119 43.6514 19.0314 44.9708C20.3509 46.2903 22.3554 46.4856 26.0028 46.5146C26.6366 46.5196 27.3201 46.5196 28.0583 46.5196H30.6059H33.4384H35.99C36.728 46.5196 37.4114 46.5196 38.0451 46.5146C41.6927 46.4856 43.6974 46.2903 45.0169 44.9708C46.5657 43.422 46.5657 40.9293 46.5657 35.9439V33.3787L43.9873 36.1746C43.7295 36.4542 43.5498 36.7933 43.462 37.158C43.457 38.9702 43.4324 40.2302 43.3142 41.2003L43.313 41.252L43.3071 41.2579C43.3039 41.283 43.3006 41.308 43.2973 41.3327C43.15 42.4286 42.925 42.6752 42.8231 42.7771C42.7213 42.8789 42.4747 43.1039 41.3788 43.2512C41.3541 43.2545 41.3291 43.2578 41.304 43.261L41.2979 43.2671L41.2111 43.2724C40.6594 43.3378 40.0141 43.3737 39.2282 43.3934L37.4602 43.5013C36.9569 43.5321 36.4791 43.7335 36.1058 44.0725L33.4107 46.5196H30.5938L27.8997 44.0008C27.5451 43.6693 27.0925 43.4643 26.612 43.4152C26.4039 43.4144 26.2033 43.4132 26.0098 43.4117C24.5576 43.3999 23.5048 43.3635 22.6695 43.2512C21.5736 43.1039 21.327 42.8789 21.2251 42.7771C21.1233 42.6752 20.8983 42.4286 20.751 41.3327C20.7488 41.3163 20.7466 41.2998 20.7445 41.2832L20.7349 41.2737L20.734 41.2529L20.7304 41.1702C20.6321 40.3446 20.6002 39.3097 20.5899 37.9068L20.5744 37.5474C20.5703 37.451 20.5598 37.3554 20.5434 37.2613C20.4714 36.8491 20.2836 36.4635 19.9993 36.1511L17.4826 33.3865Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M43.4361 24.6464L43.5814 26.617C43.6181 27.1141 43.8212 27.5842 44.158 27.9516L46.5657 30.5773V28.0122C46.5657 23.0268 46.5657 20.5341 45.0169 18.9853C43.4681 17.4365 40.9754 17.4365 35.99 17.4365H33.4104L35.9941 19.7902C36.3562 20.1201 36.8173 20.3206 37.3055 20.3606L40.3018 20.6057C40.5881 20.6227 40.8522 20.6441 41.098 20.6709L41.3195 20.689L41.3288 20.6983C41.3455 20.7005 41.3622 20.7027 41.3788 20.7049C42.4747 20.8523 42.7213 21.0772 42.8231 21.1791C42.925 21.2809 43.15 21.5275 43.2973 22.6234C43.3724 23.1822 43.4136 23.8384 43.4361 24.6464Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M20.7437 41.2626L20.734 41.2529L20.7349 41.2737L20.7445 41.2832L20.7437 41.2626Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M22.7418 43.2607L26.0098 43.4117C24.5992 43.4003 23.5654 43.3656 22.7418 43.2607Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M20.6049 37.695C20.5987 37.5479 20.578 37.4027 20.5434 37.2613C20.5598 37.3554 20.5703 37.451 20.5744 37.5474L20.5899 37.9068C20.6002 39.3097 20.6321 40.3446 20.7304 41.1702L20.734 41.2529L20.7437 41.2626L20.6049 37.695Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M13.6494 31.978C13.6494 33.8441 14.9288 35.3997 17.4877 37.9995C17.4826 37.3656 17.4826 36.6821 17.4826 35.9439L17.4826 33.3957L17.4772 33.3895L17.4772 33.3858C17.464 33.3687 17.4508 33.3516 17.4379 33.3346C16.7671 32.4555 16.7518 32.1221 16.7518 31.978C16.7518 31.834 16.7671 31.5006 17.4379 30.6215C17.4526 30.6021 17.4675 30.5827 17.4826 30.5632L17.4826 28.0122C17.4826 27.274 17.4826 26.5905 17.4877 25.9566C14.9288 28.5563 13.6494 30.112 13.6494 31.978Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M30.6674 46.5641C30.6549 46.5546 30.6425 46.5451 30.63 46.5354H30.6232L30.6059 46.5196H28.0583C27.3201 46.5196 26.6366 46.5196 26.0028 46.5146C28.6023 49.0732 30.1579 50.3526 32.0239 50.3526C33.8899 50.3526 35.4455 49.0732 38.0451 46.5146C37.4114 46.5196 36.728 46.5196 35.99 46.5196H33.4384C33.419 46.5346 33.3997 46.5494 33.3805 46.5641C32.5014 47.2348 32.168 47.2501 32.0239 47.2501C31.8799 47.2501 31.5465 47.2348 30.6674 46.5641Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,23 +1,22 @@
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import avatarFromInitials from "avatar-from-initials";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { getUser } from "@documenso/lib/api";
|
||||
import Logo from "./logo";
|
||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||
import {
|
||||
ArrowRightOnRectangleIcon,
|
||||
Bars3Icon,
|
||||
BellIcon,
|
||||
XMarkIcon,
|
||||
UserCircleIcon,
|
||||
ArrowRightOnRectangleIcon,
|
||||
DocumentTextIcon,
|
||||
ChartBarIcon,
|
||||
DocumentTextIcon,
|
||||
UserCircleIcon,
|
||||
WrenchIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import Logo from "./logo";
|
||||
import { getUser } from "@documenso/lib/api";
|
||||
import avatarFromInitials from "avatar-from-initials";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
const navigation = [
|
||||
{
|
||||
@ -113,9 +112,12 @@ export default function TopNavigation() {
|
||||
<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">
|
||||
<div className="flex flex-shrink-0 items-center">
|
||||
<Logo></Logo>
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="flex flex-shrink-0 items-center gap-x-2 self-center overflow-hidden">
|
||||
<Logo className="h-8 w-8 text-black" />
|
||||
</Link>
|
||||
|
||||
<div className="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
@ -125,14 +127,12 @@ export default function TopNavigation() {
|
||||
item.current
|
||||
? "border-neon text-brown"
|
||||
: "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
|
||||
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
|
||||
aria-hidden="true"
|
||||
></item.icon>
|
||||
className="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
|
||||
aria-hidden="true"></item.icon>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
@ -142,8 +142,7 @@ export default function TopNavigation() {
|
||||
onClick={() => {
|
||||
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">
|
||||
<p className="font-bold">{user?.name || ""}</p>
|
||||
<p>{user?.email}</p>
|
||||
@ -152,8 +151,7 @@ export default function TopNavigation() {
|
||||
<div>
|
||||
<Menu.Button
|
||||
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>
|
||||
<div
|
||||
key={user?.email}
|
||||
@ -170,8 +168,7 @@ export default function TopNavigation() {
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
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">
|
||||
{userNavigation.map((item) => (
|
||||
<Menu.Item key={item.name}>
|
||||
@ -182,12 +179,10 @@ export default function TopNavigation() {
|
||||
className={classNames(
|
||||
active ? "bg-gray-100" : "",
|
||||
"block px-4 py-2 text-sm text-gray-700"
|
||||
)}
|
||||
>
|
||||
)}>
|
||||
<item.icon
|
||||
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
|
||||
aria-hidden="true"
|
||||
></item.icon>
|
||||
className="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
|
||||
aria-hidden="true"></item.icon>
|
||||
{item.name}
|
||||
</Link>
|
||||
)}
|
||||
@ -219,15 +214,14 @@ export default function TopNavigation() {
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current
|
||||
? "bg-teal-50 border-teal-500 text-teal-700"
|
||||
: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800",
|
||||
"block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
|
||||
? "border-teal-500 bg-teal-50 text-teal-700"
|
||||
: "border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800",
|
||||
"block border-l-4 py-2 pl-3 pr-4 text-base font-medium"
|
||||
)}
|
||||
aria-current={item.current ? "page" : undefined}
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
@ -259,8 +253,7 @@ export default function TopNavigation() {
|
||||
: item.click
|
||||
}
|
||||
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}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
143
apps/web/components/reset-password.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Button } from "@documenso/ui";
|
||||
import Logo from "./logo";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import * as z from "zod";
|
||||
|
||||
const ZResetPasswordFormSchema = z
|
||||
.object({
|
||||
password: z.string().min(8, { message: "Password must be at least 8 characters" }),
|
||||
confirmPassword: z.string().min(8, { message: "Password must be at least 8 characters" }),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
path: ["confirmPassword"],
|
||||
message: "Password don't match",
|
||||
});
|
||||
|
||||
type TResetPasswordFormSchema = z.infer<typeof ZResetPasswordFormSchema>;
|
||||
|
||||
export default function ResetPassword() {
|
||||
const router = useRouter();
|
||||
const { token } = router.query;
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
} = useForm<TResetPasswordFormSchema>({
|
||||
resolver: zodResolver(ZResetPasswordFormSchema),
|
||||
});
|
||||
|
||||
const [resetSuccessful, setResetSuccessful] = useState(false);
|
||||
|
||||
const onSubmit = async ({ password }: TResetPasswordFormSchema) => {
|
||||
const response = await toast.promise(
|
||||
fetch(`/api/auth/reset-password`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ password, token }),
|
||||
}),
|
||||
{
|
||||
loading: "Resetting...",
|
||||
success: `Reset password successful`,
|
||||
error: "Could not reset password :/",
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
toast.dismiss();
|
||||
const error = await response.json();
|
||||
toast.error(error.message);
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
setResetSuccessful(true);
|
||||
setTimeout(() => {
|
||||
router.push("/login");
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div>
|
||||
<Logo className="mx-auto h-20 w-auto"></Logo>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
|
||||
Reset Password
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
{resetSuccessful ? "Your password has been reset." : "Please chose your new password"}
|
||||
</p>
|
||||
</div>
|
||||
{!resetSuccessful && (
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="-space-y-px rounded-md shadow-sm">
|
||||
<div>
|
||||
<label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
{...register("password", { required: "Password is required" })}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
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="New password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="sr-only">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
{...register("confirmPassword")}
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
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="Confirm new password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errors && (
|
||||
<span className="text-xs text-red-500">{errors.confirmPassword?.message}</span>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="group relative flex w-full">
|
||||
Reset password
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Link href="/login">
|
||||
<div className="relative mt-10 flex items-center justify-center gap-2 text-sm text-gray-500 hover:cursor-pointer hover:text-gray-900">
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
Back to log in
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,15 @@
|
||||
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 { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { updateUser } from "@documenso/features";
|
||||
import { Button } from "@documenso/ui";
|
||||
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 = [
|
||||
{
|
||||
@ -23,17 +26,26 @@ const subNavigation = [
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true") {
|
||||
subNavigation.push({
|
||||
name: "Billing",
|
||||
href: "/settings/billing",
|
||||
icon: CreditCardIcon,
|
||||
current: false,
|
||||
});
|
||||
}
|
||||
|
||||
function classNames(...classes: any) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export default function Setttings() {
|
||||
const session = useSession();
|
||||
const { subscription, hasSubscription } = useSubscription();
|
||||
const [user, setUser] = useState({
|
||||
email: "",
|
||||
name: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getUser().then((res: any) => {
|
||||
res.json().then((j: any) => {
|
||||
@ -48,6 +60,7 @@ export default function Setttings() {
|
||||
});
|
||||
|
||||
const [savingTimeout, setSavingTimeout] = useState<any>();
|
||||
const [password, setPassword] = useState("");
|
||||
function handleNameChange(e: ChangeEvent<HTMLInputElement>): void {
|
||||
let u = { ...user };
|
||||
u.name = e.target.value;
|
||||
@ -74,15 +87,12 @@ export default function Setttings() {
|
||||
</Head>
|
||||
<header className="py-10">
|
||||
<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">
|
||||
Settings
|
||||
</h1>
|
||||
<h1 className="text-brown text-3xl font-bold leading-tight tracking-tight">Settings</h1>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
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="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">
|
||||
@ -93,18 +103,17 @@ export default function Setttings() {
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
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",
|
||||
"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
|
||||
className={classNames(
|
||||
item.current
|
||||
? "text-teal-500 group-hover:text-teal-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"
|
||||
/>
|
||||
@ -115,20 +124,14 @@ export default function Setttings() {
|
||||
</aside>
|
||||
|
||||
<form
|
||||
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
|
||||
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9"
|
||||
action="#"
|
||||
method="POST"
|
||||
hidden={
|
||||
subNavigation.filter((e) => e.current)[0]?.name !==
|
||||
subNavigation[0].name
|
||||
}
|
||||
>
|
||||
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation[0].name}>
|
||||
{/* Profile 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">
|
||||
Profile
|
||||
</h2>
|
||||
<h2 className="text-lg font-medium leading-6 text-gray-900">Profile</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Let people know who they are dealing with builds trust.
|
||||
</p>
|
||||
@ -136,10 +139,7 @@ export default function Setttings() {
|
||||
|
||||
<div className="my-6 grid grid-cols-12 gap-6">
|
||||
<div className="col-span-12 sm:col-span-6">
|
||||
<label
|
||||
htmlFor="first-name"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
@ -150,14 +150,11 @@ export default function Setttings() {
|
||||
onChange={(e) => handleNameChange(e)}
|
||||
onKeyDown={handleKeyPress}
|
||||
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 className="col-span-12 sm:col-span-6">
|
||||
<label
|
||||
htmlFor="first-name"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
@ -167,36 +164,117 @@ export default function Setttings() {
|
||||
name="first-name"
|
||||
id="first-name"
|
||||
autoComplete="given-name"
|
||||
className="mt-1 block w-full rounded-md border disabled:bg-neutral-100 border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
|
||||
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>
|
||||
<Button onClick={() => updateUser(user)}>Save</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
hidden={
|
||||
subNavigation.filter((e) => e.current)[0]?.name !==
|
||||
subNavigation[1].name
|
||||
}
|
||||
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
|
||||
>
|
||||
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>
|
||||
<h2 className="text-lg font-medium leading-6 text-gray-900">Update Password</h2>
|
||||
|
||||
<div className="my-6 grid grid-cols-12 gap-6">
|
||||
<div className="col-span-12 sm:col-span-6">
|
||||
<label
|
||||
htmlFor="first-name"
|
||||
className="block text-sm font-medium text-gray-700">
|
||||
New Password
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
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>
|
||||
<Button
|
||||
disabled={password.length < 6}
|
||||
onClick={() => updateUser({ ...user, password })}>
|
||||
Save
|
||||
</Button>
|
||||
</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 className="mt-10 max-w-[1100px]" hidden={!!user.email}>
|
||||
<div className="ph-item">
|
||||
<div className="ph-col-12">
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import Link from "next/link";
|
||||
import { signup } from "@documenso/lib/api";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
|
||||
import { Button } from "@documenso/ui";
|
||||
import { XCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { signIn } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
@ -107,8 +107,7 @@ export default function Signup(props: { source: string }) {
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
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
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@ -130,8 +129,7 @@ export default function Signup(props: { source: string }) {
|
||||
form.clearErrors();
|
||||
trigger();
|
||||
}}
|
||||
className="mt-8 space-y-6"
|
||||
>
|
||||
className="mt-8 space-y-6">
|
||||
<input type="hidden" name="remember" defaultValue="true" />
|
||||
<div className="-space-y-px rounded-md shadow-sm">
|
||||
<div>
|
||||
@ -145,7 +143,7 @@ export default function Signup(props: { source: string }) {
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@ -157,8 +155,7 @@ export default function Signup(props: { source: string }) {
|
||||
{...register("password", {
|
||||
minLength: {
|
||||
value: 7,
|
||||
message:
|
||||
"Your password has to be at least 7 characters long.",
|
||||
message: "Your password has to be at least 7 characters long.",
|
||||
},
|
||||
})}
|
||||
id="password"
|
||||
@ -166,7 +163,7 @@ export default function Signup(props: { source: string }) {
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@ -177,16 +174,12 @@ export default function Signup(props: { source: string }) {
|
||||
onClick={() => {
|
||||
form.clearErrors();
|
||||
}}
|
||||
className="sgroup relative flex w-full"
|
||||
>
|
||||
className="sgroup relative flex w-full">
|
||||
Create Account
|
||||
</Button>
|
||||
<div className="pt-2">
|
||||
<div className="relative">
|
||||
<div
|
||||
className="absolute inset-0 flex items-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center"></div>
|
||||
@ -194,10 +187,7 @@ export default function Signup(props: { source: string }) {
|
||||
</div>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="font-medium text-neon hover:text-neon"
|
||||
>
|
||||
<Link href="/login" className="text-gray-500 hover:text-neon-700 font-medium">
|
||||
Sign In
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
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,24 +1,18 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
require("dotenv").config({ path: "../../.env" });
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: false,
|
||||
transpilePackages: [
|
||||
"@documenso/prisma",
|
||||
"@documenso/lib",
|
||||
"@documenso/ui",
|
||||
"@documenso/pdf",
|
||||
"@documenso/features",
|
||||
"@documenso/signing",
|
||||
"react-signature-canvas",
|
||||
],
|
||||
};
|
||||
|
||||
const withTM = require("next-transpile-modules")([
|
||||
"@documenso/prisma",
|
||||
"@documenso/lib",
|
||||
"@documenso/ui",
|
||||
"@documenso/pdf",
|
||||
"@documenso/features",
|
||||
"@documenso/signing",
|
||||
"react-signature-canvas",
|
||||
]);
|
||||
const plugins = [];
|
||||
plugins.push(withTM);
|
||||
|
||||
const moduleExports = () =>
|
||||
plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||
|
||||
module.exports = moduleExports;
|
||||
module.exports = nextConfig;
|
||||
|
||||
@ -5,38 +5,29 @@
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "next start -p $PORT",
|
||||
"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": {
|
||||
"@documenso/lib": "*",
|
||||
"@documenso/pdf": "*",
|
||||
"@documenso/prisma": "*",
|
||||
"@documenso/ui": "*",
|
||||
"@headlessui/react": "^1.7.4",
|
||||
"@heroicons/react": "^2.0.13",
|
||||
"@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",
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"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",
|
||||
"install": "^0.13.0",
|
||||
"next": "13.0.3",
|
||||
"next-auth": ">=4.20.1",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"next": "13.2.4",
|
||||
"next-auth": "^4.22.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-signpdf": "^1.5.0",
|
||||
"nodemailer": "^6.9.0",
|
||||
"nodemailer-sendgrid": "^1.0.3",
|
||||
"npm": "^9.1.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"placeholder-loading": "^0.6.0",
|
||||
"react": "18.2.0",
|
||||
@ -46,20 +37,29 @@
|
||||
"react-pdf": "^6.2.2",
|
||||
"react-resizable": "^3.0.4",
|
||||
"react-tooltip": "^5.7.2",
|
||||
"sass": "^1.57.1",
|
||||
"short-uuid": "^4.2.2",
|
||||
"string-to-color": "^2.2.2",
|
||||
"typescript": "4.8.4"
|
||||
"string-to-color": "^2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/filesystem": "^0.0.32",
|
||||
"@types/formidable": "^2.0.5",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/nodemailer": "^6.4.7",
|
||||
"@types/nodemailer-sendgrid": "^1.0.0",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@types/react-pdf": "^6.2.0",
|
||||
"@types/react-resizable": "^3.0.3",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "8.27.0",
|
||||
"eslint-config-next": "13.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"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,31 @@
|
||||
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
import { Button } from "@documenso/ui";
|
||||
import Logo from "../components/logo";
|
||||
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Custom404() {
|
||||
return (
|
||||
<>
|
||||
<main className="relative min-h-full bg-gray-100 isolate">
|
||||
<div className="absolute top-10 left-10">
|
||||
<Logo className="w-10 md:w-20" />
|
||||
</div>
|
||||
<main className="relative isolate min-h-full bg-gray-100">
|
||||
<Link href="/" className="absolute top-10 left-10 flex gap-x-2 items-center">
|
||||
<Logo className="w-10 text-black" />
|
||||
<h2 className="text-2xl font-semibold">Documenso</h2>
|
||||
</Link>
|
||||
|
||||
<div className="px-6 py-48 mx-auto text-center max-w-7xl sm:py-40 lg:px-8">
|
||||
<p className="text-base font-semibold leading-8 text-brown">404</p>
|
||||
<h1 className="mt-4 text-3xl font-bold tracking-tight text-brown sm:text-5xl">
|
||||
<div className="mx-auto max-w-7xl px-6 py-48 text-center sm:py-40 lg:px-8">
|
||||
<p className="text-brown text-base font-semibold leading-8">404</p>
|
||||
<h1 className="text-brown mt-4 text-3xl font-bold tracking-tight sm:text-5xl">
|
||||
Page not found
|
||||
</h1>
|
||||
<p className="mt-4 text-base text-gray-700 sm:mt-6">
|
||||
Sorry, we couldn’t find the page you’re looking for.
|
||||
</p>
|
||||
<div className="flex justify-center mt-10">
|
||||
<div className="mt-10 flex justify-center">
|
||||
<Button
|
||||
color="secondary"
|
||||
href="/"
|
||||
icon={ArrowSmallLeftIcon}
|
||||
className="text-base font-semibold leading-7 text-brown"
|
||||
>
|
||||
className="text-brown text-base font-semibold leading-7">
|
||||
Back to home
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -1,27 +1,27 @@
|
||||
import Logo from "../components/logo";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@documenso/ui";
|
||||
import Logo from "../components/logo";
|
||||
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
|
||||
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
|
||||
|
||||
export default function Custom500() {
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex flex-col items-center justify-center min-h-full text-white bg-black">
|
||||
<div className="absolute top-10 left-10">
|
||||
<Logo dark className="w-10 md:w-20" />
|
||||
</div>
|
||||
<div className="relative flex min-h-full flex-col items-center justify-center bg-black text-white">
|
||||
<Link href="/" className="absolute top-10 left-10 flex items-center gap-x-2 invert">
|
||||
<Logo className="w-10 text-black" />
|
||||
<h2 className="text-2xl font-semibold text-black">Documenso</h2>
|
||||
</Link>
|
||||
|
||||
<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">
|
||||
500
|
||||
<span className="relative px-3 font-thin sm:text-6xl -top-1.5">
|
||||
|
|
||||
</span>{" "}
|
||||
<span className="text-base font-semibold align-middle sm:text-2xl">
|
||||
<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">
|
||||
Something went wrong.
|
||||
</span>
|
||||
</p>
|
||||
<div className="flex justify-center mt-10">
|
||||
<div className="mt-10 flex justify-center">
|
||||
<Button color="secondary" href="/" icon={ArrowSmallLeftIcon}>
|
||||
Back to home
|
||||
</Button>
|
||||
|
||||
@ -1,13 +1,30 @@
|
||||
import "../styles/tailwind.css";
|
||||
import { ReactElement, ReactNode } from "react";
|
||||
import { NextPage } from "next";
|
||||
import type { AppProps } from "next/app";
|
||||
import { Montserrat, Qwigley } from "next/font/google";
|
||||
import { SubscriptionProvider } from "@documenso/lib/stripe/providers/subscription-provider";
|
||||
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
|
||||
import "../../../node_modules/react-resizable/css/styles.css";
|
||||
import "react-tooltip/dist/react-tooltip.css";
|
||||
import { ReactElement, ReactNode } from "react";
|
||||
import type { AppProps } from "next/app";
|
||||
import { NextPage } from "next";
|
||||
import "../styles/tailwind.css";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
export { coloredConsole } from "@documenso/lib";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import "react-tooltip/dist/react-tooltip.css";
|
||||
|
||||
export { coloredConsole } from "@documenso/lib";
|
||||
|
||||
const montserrat = Montserrat({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "700"],
|
||||
display: "swap",
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
const qwigley = Qwigley({
|
||||
subsets: ["latin"],
|
||||
weight: ["400"],
|
||||
display: "swap",
|
||||
variable: "--font-qwigley",
|
||||
});
|
||||
|
||||
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||
getLayout?: (page: ReactElement) => ReactNode;
|
||||
@ -19,13 +36,17 @@ type AppPropsWithLayout = AppProps & {
|
||||
|
||||
export default function App({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps },
|
||||
pageProps: { session, initialSubscription, ...pageProps },
|
||||
}: AppPropsWithLayout) {
|
||||
const getLayout = Component.getLayout || ((page: any) => page);
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<Toaster position="top-center"></Toaster>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
<SubscriptionProvider initialSubscription={initialSubscription}>
|
||||
<main className={`${montserrat.variable} h-full font-sans`}>
|
||||
<Toaster position="top-center" />
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</main>
|
||||
</SubscriptionProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
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;
|
||||
|
||||
return (
|
||||
<Html
|
||||
className="h-full bg-gray-100 scroll-smooth font-normal antialiased"
|
||||
lang="en"
|
||||
>
|
||||
<Head>
|
||||
<meta name="color-scheme"></meta>
|
||||
</Head>
|
||||
<body className="flex h-full flex-col">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
19
apps/web/pages/_document.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Head, Html, Main, NextScript } from "next/document";
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html className="h-full scroll-smooth bg-gray-100 font-normal antialiased" lang="en">
|
||||
<Head>
|
||||
<meta name="color-scheme"></meta>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
</Head>
|
||||
<body className="flex h-full flex-col">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
@ -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 prisma from "@documenso/prisma";
|
||||
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({
|
||||
secret: process.env.AUTH_SECRET,
|
||||
@ -27,8 +27,7 @@ export default NextAuth({
|
||||
password: {
|
||||
label: "Password",
|
||||
type: "password",
|
||||
placeholder:
|
||||
"Select a password. Here is some inspiration: https://xkcd.com/936/",
|
||||
placeholder: "Select a password. Here is some inspiration: https://xkcd.com/936/",
|
||||
},
|
||||
},
|
||||
async authorize(credentials: any) {
|
||||
@ -57,10 +56,7 @@ export default NextAuth({
|
||||
throw new Error(ErrorCode.UserMissingPassword);
|
||||
}
|
||||
|
||||
const isCorrectPassword = await verifyPassword(
|
||||
credentials.password,
|
||||
user.password
|
||||
);
|
||||
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
||||
|
||||
if (!isCorrectPassword) {
|
||||
throw new Error(ErrorCode.IncorrectPassword);
|
||||
|
||||
63
apps/web/pages/api/auth/forgot-password.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { sendResetPassword } from "@documenso/lib/mail";
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import crypto from "crypto";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { email } = req.body;
|
||||
const cleanEmail = email.toLowerCase();
|
||||
|
||||
if (!cleanEmail || !/.+@.+/.test(cleanEmail)) {
|
||||
res.status(400).json({ message: "Invalid email" });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: cleanEmail,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(200).json({ message: "A password reset email has been sent." });
|
||||
}
|
||||
|
||||
const existingToken = await prisma.passwordResetToken.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
createdAt: {
|
||||
gte: new Date(Date.now() - 1000 * 60 * 60),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingToken) {
|
||||
return res.status(200).json({ message: "A password reset email has been sent." });
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(64).toString("hex");
|
||||
const expiry = new Date();
|
||||
expiry.setHours(expiry.getHours() + 24); // Set expiry to one hour from now
|
||||
|
||||
let passwordResetToken;
|
||||
try {
|
||||
passwordResetToken = await prisma.passwordResetToken.create({
|
||||
data: {
|
||||
token,
|
||||
expiry,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({ message: "Something went wrong" });
|
||||
}
|
||||
|
||||
await sendResetPassword(user, passwordResetToken.token);
|
||||
|
||||
return res.status(200).json({ message: "A password reset email has been sent." });
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||
});
|
||||
69
apps/web/pages/api/auth/reset-password.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { hashPassword, verifyPassword } from "@documenso/lib/auth";
|
||||
import { sendResetPasswordSuccessMail } from "@documenso/lib/mail";
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { token, password } = req.body;
|
||||
|
||||
if (!token) {
|
||||
res.status(400).json({ message: "Invalid token" });
|
||||
return;
|
||||
}
|
||||
|
||||
const foundToken = await prisma.passwordResetToken.findUnique({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
include: {
|
||||
User: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!foundToken) {
|
||||
return res.status(404).json({ message: "Invalid token." });
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
if (now > foundToken.expiry) {
|
||||
return res.status(400).json({ message: "Token has expired" });
|
||||
}
|
||||
|
||||
const isSamePassword = await verifyPassword(password, foundToken.User.password!);
|
||||
|
||||
if (isSamePassword) {
|
||||
return res.status(400).json({ message: "New password must be different" });
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
const transaction = await prisma.$transaction([
|
||||
prisma.user.update({
|
||||
where: {
|
||||
id: foundToken.userId,
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
},
|
||||
}),
|
||||
prisma.passwordResetToken.deleteMany({
|
||||
where: {
|
||||
userId: foundToken.userId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!transaction) {
|
||||
return res.status(500).json({ message: "Error resetting password." });
|
||||
}
|
||||
|
||||
await sendResetPasswordSuccessMail(foundToken.User);
|
||||
|
||||
res.status(200).json({ message: "Password reset successful." });
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||
});
|
||||
@ -1,21 +1,20 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@documenso/prisma";
|
||||
import { hashPassword } from "@documenso/lib/auth";
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { email, password, source } = req.body;
|
||||
const cleanEmail = email.toLowerCase();
|
||||
|
||||
if (!cleanEmail || !cleanEmail.includes("@")) {
|
||||
res.status(422).json({ message: "Invalid email" });
|
||||
if (!cleanEmail || !/.+@.+/.test(cleanEmail)) {
|
||||
res.status(400).json({ message: "Invalid email" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password || password.trim().length < 7) {
|
||||
return res.status(422).json({
|
||||
return res.status(400).json({
|
||||
message: "Password should be at least 7 characters long.",
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
import {
|
||||
defaultHandler,
|
||||
defaultResponder,
|
||||
getUserFromToken,
|
||||
} from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
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 { Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { id: documentId } = req.query;
|
||||
@ -46,8 +42,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
document = await getDocument(+documentId, req, res);
|
||||
}
|
||||
|
||||
if (!document)
|
||||
res.status(404).end(`No document with id ${documentId} found.`);
|
||||
if (!document) res.status(404).end(`No document with id ${documentId} found.`);
|
||||
|
||||
const signaturesCount = await prisma.signature.count({
|
||||
where: {
|
||||
@ -61,18 +56,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
// No need to add a signature, if no one signed yet.
|
||||
if (signaturesCount > 0) {
|
||||
signedDocumentAsBase64 = await addDigitalSignature(
|
||||
document?.document || ""
|
||||
);
|
||||
signedDocumentAsBase64 = await addDigitalSignature(document?.document || "");
|
||||
}
|
||||
|
||||
const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64");
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader("Content-Length", buffer.length);
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename=${document?.title}`
|
||||
);
|
||||
res.setHeader("Content-Disposition", `attachment; filename=${document?.title}`);
|
||||
|
||||
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 short from "short-uuid";
|
||||
import { Document as PrismaDocument, FieldType } from "@prisma/client";
|
||||
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) {
|
||||
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 { Document as PrismaDocument, FieldType } from "@prisma/client";
|
||||
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) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
@ -61,18 +57,14 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
});
|
||||
|
||||
if (!recipient || recipient?.documentId !== +documentId)
|
||||
return res
|
||||
.status(401)
|
||||
.send("Recipient does not have access to this document.");
|
||||
return res.status(401).send("Recipient does not have access to this document.");
|
||||
}
|
||||
|
||||
if (user) {
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
// todo entity ownerships checks
|
||||
if (document.userId !== user.id) {
|
||||
return res
|
||||
.status(401)
|
||||
.send("User does not have access to this document.");
|
||||
return res.status(401).send("User does not have access to this document.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
import {
|
||||
defaultHandler,
|
||||
defaultResponder,
|
||||
getUserFromToken,
|
||||
} from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import short from "short-uuid";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
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) {
|
||||
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 short from "short-uuid";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
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) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
|
||||
@ -1,63 +1,70 @@
|
||||
import {
|
||||
defaultHandler,
|
||||
defaultResponder,
|
||||
getUserFromToken,
|
||||
} from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { sendSigningRequest } from "@documenso/lib/mail";
|
||||
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";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
const { id: documentId } = req.query;
|
||||
const { resendTo: resendTo = [] } = req.body;
|
||||
try {
|
||||
const user = await getUserFromToken(req, res);
|
||||
const { id: documentId } = req.query;
|
||||
const { resendTo: resendTo = [] } = req.body;
|
||||
|
||||
if (!user) return;
|
||||
if (!user) {
|
||||
return res.status(401).send("Unauthorized");
|
||||
}
|
||||
|
||||
if (!documentId) {
|
||||
res.status(400).send("Missing parameter documentId.");
|
||||
return;
|
||||
}
|
||||
if (!documentId) {
|
||||
return res.status(400).send("Missing parameter documentId.");
|
||||
}
|
||||
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
const document: PrismaDocument = await getDocument(+documentId, req, res);
|
||||
|
||||
if (!document)
|
||||
res.status(404).end(`No document with id ${documentId} found.`);
|
||||
if (!document) {
|
||||
res.status(404).end(`No document with id ${documentId} found.`);
|
||||
}
|
||||
|
||||
let recipientCondition: any = {
|
||||
documentId: +documentId,
|
||||
sendStatus: SendStatus.NOT_SENT,
|
||||
};
|
||||
|
||||
if (resendTo.length) {
|
||||
recipientCondition = {
|
||||
let recipientCondition: any = {
|
||||
documentId: +documentId,
|
||||
id: { in: resendTo },
|
||||
sendStatus: SendStatus.NOT_SENT,
|
||||
};
|
||||
}
|
||||
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
...recipientCondition,
|
||||
},
|
||||
});
|
||||
if (resendTo.length) {
|
||||
recipientCondition = {
|
||||
documentId: +documentId,
|
||||
id: { in: resendTo },
|
||||
};
|
||||
}
|
||||
|
||||
if (!recipients.length) return res.status(200).send(recipients.length);
|
||||
|
||||
let sentRequests = 0;
|
||||
recipients.forEach(async (recipient) => {
|
||||
await sendSigningRequest(recipient, document, user).catch((err) => {
|
||||
console.log(err);
|
||||
return res.status(502).end("Coud not send request for signing.");
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
...recipientCondition,
|
||||
},
|
||||
});
|
||||
|
||||
sentRequests++;
|
||||
if (!recipients.length) {
|
||||
return res.status(200).send(recipients.length);
|
||||
}
|
||||
|
||||
let sentRequests = 0;
|
||||
|
||||
await Promise.all(
|
||||
recipients.map(async (recipient) => {
|
||||
await sendSigningRequest(recipient, document, user);
|
||||
|
||||
sentRequests++;
|
||||
})
|
||||
);
|
||||
|
||||
if (sentRequests === recipients.length) {
|
||||
return res.status(200).send(recipients.length);
|
||||
}
|
||||
});
|
||||
|
||||
return res.status(502).end("Could not send request for signing.");
|
||||
} catch (err) {
|
||||
return res.status(502).end("Could not send request for signing.");
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
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 { 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) {
|
||||
const { token: recipientToken } = req.query;
|
||||
@ -44,7 +44,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
for (const signature of signaturesFromBody) {
|
||||
if (!signature.signatureImage && !signature.typedSignature) {
|
||||
documentWithInserts = document.document;
|
||||
throw new Error("Cant't save invalid signature.");
|
||||
throw new Error("Can't save invalid signature.");
|
||||
}
|
||||
|
||||
await saveSignature(signature);
|
||||
@ -63,6 +63,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
data: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
signedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
@ -73,13 +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({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
type: { in: [FieldType.DATE, FieldType.TEXT] },
|
||||
recipientId: { in: signedRecipients.map((r) => r.id) },
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
}
|
||||
});
|
||||
|
||||
// Insert fields other than signatures
|
||||
@ -91,7 +103,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
}).format(new Date())
|
||||
}).format(field.Recipient?.signedAt ?? new Date())
|
||||
: field.customText || "",
|
||||
field.positionX,
|
||||
field.positionY,
|
||||
@ -115,10 +127,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
data: {
|
||||
document: documentWithInserts,
|
||||
status:
|
||||
unsignedRecipients.length > 0
|
||||
? DocumentStatus.PENDING
|
||||
: DocumentStatus.COMPLETED,
|
||||
status: unsignedRecipients.length > 0 ? DocumentStatus.PENDING : DocumentStatus.COMPLETED,
|
||||
},
|
||||
});
|
||||
|
||||
@ -129,8 +138,11 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
});
|
||||
|
||||
document.document = documentWithInserts;
|
||||
if (documentOwner)
|
||||
await sendSigningDoneMail(recipient, document, documentOwner);
|
||||
if (documentOwner) await sendSigningDoneMail(document, documentOwner);
|
||||
|
||||
for (const signer of signedRecipients) {
|
||||
await sendSigningDoneMail(document, signer);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).end();
|
||||
@ -139,9 +151,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (signedField?.Signature?.signatureImageAsBase64) {
|
||||
documentWithInserts = await insertImageInPDF(
|
||||
documentWithInserts,
|
||||
signedField.Signature
|
||||
? signedField.Signature?.signatureImageAsBase64
|
||||
: "",
|
||||
signedField.Signature ? signedField.Signature?.signatureImageAsBase64 : "",
|
||||
signedField.positionX,
|
||||
signedField.positionY,
|
||||
signedField.page
|
||||
@ -169,12 +179,8 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
create: {
|
||||
recipientId: recipient.id,
|
||||
fieldId: signature.fieldId,
|
||||
signatureImageAsBase64: signature.signatureImage
|
||||
? signature.signatureImage
|
||||
: null,
|
||||
typedSignature: signature.typedSignature
|
||||
? signature.typedSignature
|
||||
: null,
|
||||
signatureImageAsBase64: signature.signatureImage ? signature.signatureImage : 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 { getUserFromToken } from "@documenso/lib/server";
|
||||
import formidable from "formidable";
|
||||
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 = {
|
||||
api: {
|
||||
@ -15,7 +16,17 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const form = formidable();
|
||||
|
||||
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) => {
|
||||
if (err) throw err;
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
|
||||
|
||||
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
@ -0,0 +1 @@
|
||||
export { portalSessionHandler as default } from "@documenso/lib/stripe/handlers/portal-session";
|
||||
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
@ -0,0 +1,5 @@
|
||||
export const config = {
|
||||
api: { bodyParser: false },
|
||||
};
|
||||
|
||||
export { webhookHandler as default } from "@documenso/lib/stripe/handlers/webhook";
|
||||
@ -1,8 +1,9 @@
|
||||
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
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 { Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
// todo remove before launch
|
||||
|
||||
@ -12,10 +13,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const signedDocument = await addDigitalSignature(document.document);
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader("Content-Length", signedDocument.length);
|
||||
res.setHeader(
|
||||
"Content-Disposition",
|
||||
`attachment; filename=${document.title}`
|
||||
);
|
||||
res.setHeader("Content-Disposition", `attachment; filename=${document.title}`);
|
||||
|
||||
return res.status(200).send(signedDocument);
|
||||
}
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
import {
|
||||
defaultHandler,
|
||||
defaultResponder,
|
||||
getUserFromToken,
|
||||
} from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { hashPassword } from "@documenso/lib/auth";
|
||||
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
|
||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { method, body } = req;
|
||||
@ -29,6 +25,13 @@ async function patchHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!user) return;
|
||||
|
||||
const updatedUser = req.body;
|
||||
|
||||
let password: string | undefined = undefined;
|
||||
|
||||
if (typeof updatedUser.password === "string" && updatedUser.password.length >= 6) {
|
||||
password = await hashPassword(updatedUser.password);
|
||||
}
|
||||
|
||||
await prisma.user
|
||||
.update({
|
||||
where: {
|
||||
@ -36,6 +39,7 @@ async function patchHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
data: {
|
||||
name: updatedUser.name,
|
||||
password,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
import {
|
||||
defaultHandler,
|
||||
defaultResponder,
|
||||
getUserFromToken,
|
||||
} from "@documenso/lib/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
|
||||
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) {
|
||||
const user = await getUserFromToken(req, res);
|
||||
|
||||
30
apps/web/pages/auth/reset/[token].tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import Head from "next/head";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import ResetPassword from "../../../components/reset-password";
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Reset Password | Documenso</title>
|
||||
</Head>
|
||||
<ResetPassword />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const user = await getUserFromToken(context.req, context.res);
|
||||
if (user)
|
||||
return {
|
||||
redirect: {
|
||||
source: "/login",
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
20
apps/web/pages/auth/reset/index.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import Logo from "../../../components/logo";
|
||||
|
||||
export default function ResetPage() {
|
||||
return (
|
||||
<div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="w-full max-w-md space-y-8">
|
||||
<div>
|
||||
<Logo className="mx-auto h-20 w-auto"></Logo>
|
||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
|
||||
Reset Password
|
||||
</h2>
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
The token you provided is invalid. Please try again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,10 @@
|
||||
import { ChangeEvent, ReactElement } from "react";
|
||||
import Head from "next/head";
|
||||
import { ReactElement } from "react";
|
||||
import Layout from "../components/layout";
|
||||
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 {
|
||||
CheckBadgeIcon,
|
||||
@ -9,23 +12,23 @@ import {
|
||||
ExclamationTriangleIcon,
|
||||
UsersIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { uploadDocument } from "@documenso/features";
|
||||
import {
|
||||
DocumentStatus,
|
||||
Document as PrismaDocument,
|
||||
SendStatus,
|
||||
SigningStatus,
|
||||
Document as PrismaDocument,
|
||||
} from "@prisma/client";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
|
||||
import { truncate } from "fs";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
|
||||
type FormValues = {
|
||||
document: File;
|
||||
};
|
||||
|
||||
const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
const { hasSubscription } = useSubscription();
|
||||
|
||||
const stats = [
|
||||
{
|
||||
name: "Draft",
|
||||
@ -59,30 +62,30 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
Dashboard
|
||||
</h1>
|
||||
</header>
|
||||
<dl className="grid gap-5 mt-8 md:grid-cols-3 ">
|
||||
<dl className="mt-8 grid gap-5 md:grid-cols-3 ">
|
||||
{stats.map((item) => (
|
||||
<Link href={item.link} key={item.name}>
|
||||
<div className="px-4 py-3 overflow-hidden bg-white rounded-lg shadow md:p-6 sm:py-5">
|
||||
<dt className="text-sm font-medium text-gray-500 truncate ">
|
||||
<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-700 ">
|
||||
<item.icon
|
||||
className="flex-shrink-0 inline w-5 h-5 mr-3 text-neon sm:w-6 sm:h-6"
|
||||
aria-hidden="true"
|
||||
></item.icon>
|
||||
className="text-neon-600 mr-3 inline h-5 w-5 flex-shrink-0 sm:h-6 sm:w-6"
|
||||
aria-hidden="true"></item.icon>
|
||||
{item.name}
|
||||
</dt>
|
||||
<dd className="mt-1 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
|
||||
<dd className="mt-3 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
|
||||
{getStat(item.name, props)}
|
||||
</dd>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</dl>
|
||||
|
||||
<div className="mt-12">
|
||||
<input
|
||||
id="fileUploadHelper"
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
onChange={(event: any) => {
|
||||
onChange={(event: ChangeEvent) => {
|
||||
uploadDocument(event);
|
||||
}}
|
||||
hidden
|
||||
@ -90,27 +93,27 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
if (hasSubscription) {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}
|
||||
}}
|
||||
className="relative block w-full p-12 text-center border-2 border-gray-300 border-dashed rounded-lg cursor-pointer hover:border-neon focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
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
|
||||
className="w-12 h-12 mx-auto text-gray-400"
|
||||
className="mx-auto h-12 w-12 text-gray-400 group-hover:text-gray-700 duration-200"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 00 20 25"
|
||||
aria-hidden="true"
|
||||
>
|
||||
aria-hidden="true">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
id="add_document"
|
||||
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">
|
||||
Add a new PDF document.
|
||||
</span>
|
||||
</div>
|
||||
@ -147,9 +150,7 @@ export async function getServerSideProps(context: any) {
|
||||
|
||||
const documents: any[] = await getDocumentsForUserFromToken(context);
|
||||
|
||||
const drafts: PrismaDocument[] = documents.filter(
|
||||
(d) => d.status === DocumentStatus.DRAFT
|
||||
);
|
||||
const drafts: PrismaDocument[] = documents.filter((d) => d.status === DocumentStatus.DRAFT);
|
||||
|
||||
const waiting: any[] = documents.filter(
|
||||
(e) =>
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
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 { useSubscription } from "@documenso/lib/stripe";
|
||||
import { Button, IconButton, SelectBox } from "@documenso/ui";
|
||||
import Layout from "../components/layout";
|
||||
import type { NextPageWithLayout } from "./_app";
|
||||
import Head from "next/head";
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
CheckBadgeIcon,
|
||||
@ -13,21 +19,23 @@ import {
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { useRouter } from "next/router";
|
||||
import { uploadDocument } from "@documenso/features";
|
||||
import { DocumentStatus } from "@prisma/client";
|
||||
import { Button, IconButton, SelectBox } from "@documenso/ui";
|
||||
import { NextPageContext } from "next";
|
||||
import { deleteDocument, getDocuments } from "@documenso/lib/api";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
|
||||
const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
const router = useRouter();
|
||||
const { hasSubscription } = useSubscription();
|
||||
const [documents, setDocuments]: any[] = useState([]);
|
||||
const [filteredDocuments, setFilteredDocuments] = useState([]);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const statusFilters = [
|
||||
|
||||
type statusFilterType = {
|
||||
label: string;
|
||||
value: DocumentStatus | "ALL";
|
||||
};
|
||||
|
||||
const statusFilters: statusFilterType[] = [
|
||||
{ label: "All", value: "ALL" },
|
||||
{ label: "Draft", value: "DRAFT" },
|
||||
{ label: "Waiting for others", value: "PENDING" },
|
||||
@ -42,12 +50,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
{ label: "Last 12 months", value: 366 },
|
||||
];
|
||||
|
||||
const [selectedStatusFilter, setSelectedStatusFilter] = useState(
|
||||
statusFilters[0]
|
||||
);
|
||||
const [selectedCreatedFilter, setSelectedCreatedFilter] = useState(
|
||||
createdFilter[0]
|
||||
);
|
||||
const [selectedStatusFilter, setSelectedStatusFilter] = useState(statusFilters[0]);
|
||||
const [selectedCreatedFilter, setSelectedCreatedFilter] = useState(createdFilter[0]);
|
||||
|
||||
const loadDocuments = async () => {
|
||||
if (!documents.length) setLoading(true);
|
||||
@ -62,9 +66,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
useEffect(() => {
|
||||
loadDocuments().finally(() => {
|
||||
setSelectedStatusFilter(
|
||||
statusFilters.filter(
|
||||
(status) => status.value === props.filter.toUpperCase()
|
||||
)[0]
|
||||
statusFilters.filter((status) => status.value === props.filter.toUpperCase())[0]
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
@ -79,9 +81,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
|
||||
function filterDocumentes(documents: []): any {
|
||||
let filteredDocuments = documents.filter(
|
||||
(d: any) =>
|
||||
d.status === selectedStatusFilter.value ||
|
||||
selectedStatusFilter.value === "ALL"
|
||||
(d: any) => d.status === selectedStatusFilter.value || selectedStatusFilter.value === "ALL"
|
||||
);
|
||||
|
||||
filteredDocuments = filteredDocuments.filter((document: any) =>
|
||||
@ -91,6 +91,20 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
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 {
|
||||
if (lastXDays < 0) return true;
|
||||
|
||||
@ -98,9 +112,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
const today: Date = new Date(); // Today's date
|
||||
|
||||
// Calculate the difference between the two dates in days
|
||||
const diffInDays = Math.floor(
|
||||
(today.getTime() - documentDate.getTime()) / millisecondsInDay
|
||||
);
|
||||
const diffInDays = Math.floor((today.getTime() - documentDate.getTime()) / millisecondsInDay);
|
||||
|
||||
console.log(diffInDays);
|
||||
|
||||
@ -114,7 +126,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<title>Documents | Documenso</title>
|
||||
</Head>
|
||||
<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">
|
||||
<header>
|
||||
<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">
|
||||
<Button
|
||||
icon={DocumentPlusIcon}
|
||||
disabled={!hasSubscription}
|
||||
onClick={() => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
Add Document
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 mb-12">
|
||||
<div className="w-fit block float-right ml-3 mt-7">
|
||||
{filteredDocuments.length != 1
|
||||
? filteredDocuments.length + " Documents"
|
||||
: "1 Document"}
|
||||
</div>
|
||||
<div className="mt-3 mb-12 flex flex-wrap items-center justify-start gap-x-4 md:justify-end gap-y-4">
|
||||
<SelectBox
|
||||
className="w-1/4 block float-right"
|
||||
className="block flex-1 md:flex-none md:w-1/4"
|
||||
label="Status"
|
||||
options={statusFilters}
|
||||
value={selectedStatusFilter}
|
||||
onChange={handleStatusFilterChange}
|
||||
/>
|
||||
<SelectBox
|
||||
className="block flex-1 md:flex-none md:w-1/4"
|
||||
label="Created"
|
||||
options={createdFilter}
|
||||
value={selectedCreatedFilter}
|
||||
onChange={setSelectedCreatedFilter}
|
||||
/>
|
||||
<SelectBox
|
||||
className="w-1/4 block float-right ml-3"
|
||||
label="Status"
|
||||
options={statusFilters}
|
||||
value={selectedStatusFilter}
|
||||
onChange={setSelectedStatusFilter}
|
||||
/>
|
||||
<div className="block w-fit pt-5">
|
||||
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
|
||||
</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-col-12">
|
||||
<div className="ph-picture"></div>
|
||||
@ -171,14 +181,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="mt-28 flex flex-col"
|
||||
hidden={!documents.length || loading}
|
||||
>
|
||||
<div className="mt-8 flex flex-col" hidden={!documents.length || loading}>
|
||||
<div
|
||||
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="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-300">
|
||||
@ -186,32 +192,25 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<tr>
|
||||
<th
|
||||
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
|
||||
</th>
|
||||
<th
|
||||
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
|
||||
</th>
|
||||
<th
|
||||
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
|
||||
</th>
|
||||
<th
|
||||
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
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="relative py-3.5 pl-3 pr-4 sm:pr-6"
|
||||
>
|
||||
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span className="sr-only">Delete</span>
|
||||
</th>
|
||||
</tr>
|
||||
@ -220,38 +219,30 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
{filteredDocuments.map((document: any, index: number) => (
|
||||
<tr
|
||||
key={document.id}
|
||||
className="hover:bg-gray-100 cursor-pointer"
|
||||
onClick={(event) => showDocument(document.id)}
|
||||
>
|
||||
className="cursor-pointer hover:bg-gray-100"
|
||||
onClick={(event) => showDocument(document.id)}>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
{document.title || "#" + document.id}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<td className="inline-flex max-w-[250px] flex-wrap gap-x-2 gap-y-1 whitespace-nowrap py-3 text-sm text-gray-500">
|
||||
{document.Recipient.map((item: any) => (
|
||||
<div key={item.id}>
|
||||
{item.sendStatus === "NOT_SENT" ? (
|
||||
<span
|
||||
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"
|
||||
>
|
||||
{item.name
|
||||
? item.name + " <" + item.email + ">"
|
||||
: item.email}
|
||||
className="inline-flex h-6 flex-shrink-0 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}
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{item.sendStatus === "SENT" &&
|
||||
item.readStatus !== "OPENED" ? (
|
||||
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
|
||||
<span id="sent_icon">
|
||||
<span
|
||||
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"
|
||||
>
|
||||
<EnvelopeIcon className="inline h-5 mr-1"></EnvelopeIcon>
|
||||
{item.name
|
||||
? item.name + " <" + item.email + ">"
|
||||
: item.email}
|
||||
className="inline-flex h-6 flex-shrink-0 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>
|
||||
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
@ -262,13 +253,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<span id="read_icon">
|
||||
<span
|
||||
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"
|
||||
>
|
||||
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
|
||||
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
|
||||
{item.name
|
||||
? item.name + " <" + item.email + ">"
|
||||
: item.email}
|
||||
className="inline-flex h-6 flex-shrink-0 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="mr-1 inline h-4"></CheckIcon>
|
||||
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
@ -276,8 +264,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
)}
|
||||
{item.signingStatus === "SIGNED" ? (
|
||||
<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">
|
||||
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>{" "}
|
||||
<span className="inline-flex h-6 flex-shrink-0 items-center 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>{" "}
|
||||
{item.email}
|
||||
</span>
|
||||
</span>
|
||||
@ -307,9 +295,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
{formatDocumentStatus(document.status)}
|
||||
<p>
|
||||
<small hidden={document.Recipient.length === 0}>
|
||||
{document.Recipient.filter(
|
||||
(r: any) => r.signingStatus === "SIGNED"
|
||||
).length || 0}
|
||||
{document.Recipient.filter((r: any) => r.signingStatus === "SIGNED")
|
||||
.length || 0}
|
||||
/{document.Recipient.length || 0}
|
||||
</small>
|
||||
</p>
|
||||
@ -327,6 +314,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
event.stopPropagation();
|
||||
router.push("/documents/" + document.id);
|
||||
}}
|
||||
disabled={document.status === "COMPLETED"}
|
||||
/>
|
||||
<IconButton
|
||||
icon={ArrowDownTrayIcon}
|
||||
@ -342,30 +330,20 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
onClick={(event: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (
|
||||
confirm(
|
||||
"Are you sure you want to delete this document"
|
||||
)
|
||||
) {
|
||||
if (confirm("Are you sure you want to delete this document")) {
|
||||
const documentsWithoutIndex = [...documents];
|
||||
const removedItem: any =
|
||||
documentsWithoutIndex.splice(index, 1);
|
||||
const removedItem: any = documentsWithoutIndex.splice(index, 1);
|
||||
setDocuments(documentsWithoutIndex);
|
||||
deleteDocument(document.id)
|
||||
.catch((err) => {
|
||||
documentsWithoutIndex.splice(
|
||||
index,
|
||||
0,
|
||||
removedItem
|
||||
);
|
||||
documentsWithoutIndex.splice(index, 0, removedItem);
|
||||
setDocuments(documentsWithoutIndex);
|
||||
})
|
||||
.then(() => {
|
||||
loadDocuments();
|
||||
});
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
}}></IconButton>
|
||||
<span className="sr-only">, {document.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
@ -374,29 +352,21 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
hidden={filteredDocuments.length > 0}
|
||||
className="mx-auto w-fit mt-12 p-3"
|
||||
>
|
||||
<FunnelIcon className="w-5 inline mr-1 align-middle" /> Nothing
|
||||
here. Maybe try a different filter.
|
||||
<div hidden={filteredDocuments.length > 0} className="mx-auto mt-12 w-fit p-3">
|
||||
<FunnelIcon className="mr-1 inline w-5 align-middle" /> Nothing here. Maybe try a
|
||||
different filter.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="text-center mt-24"
|
||||
id="empty"
|
||||
hidden={documents.length > 0 || loading}
|
||||
>
|
||||
<div className="mt-24 text-center" id="empty" hidden={documents.length > 0 || loading}>
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
aria-hidden="true">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@ -411,10 +381,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
icon={PlusIcon}
|
||||
disabled={!hasSubscription}
|
||||
onClick={() => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
Add Document
|
||||
</Button>
|
||||
<input
|
||||
|
||||
@ -1,23 +1,21 @@
|
||||
import { ReactElement } from "react";
|
||||
import Layout from "../../../components/layout";
|
||||
import { NextPageWithLayout } from "../../_app";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
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 { Document as PrismaDocument } from "@prisma/client";
|
||||
import { Button, Breadcrumb } from "@documenso/ui";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
import { Breadcrumb, Button } from "@documenso/ui";
|
||||
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 router = useRouter();
|
||||
const { hasSubscription } = useSubscription();
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
@ -32,8 +30,7 @@ const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
|
||||
},
|
||||
{
|
||||
title: props.document.title,
|
||||
href:
|
||||
NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
|
||||
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@ -67,21 +64,13 @@ const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
|
||||
<Button
|
||||
icon={PaperAirplaneIcon}
|
||||
className="ml-3"
|
||||
href={
|
||||
NEXT_PUBLIC_WEBAPP_URL +
|
||||
"/documents/" +
|
||||
props.document.id +
|
||||
"/recipients"
|
||||
}
|
||||
href={NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients"}
|
||||
onClick={() => {
|
||||
if (
|
||||
confirm(
|
||||
`Send document out to ${props?.document?.Recipient?.length} recipients?`
|
||||
)
|
||||
confirm(`Send document out to ${props?.document?.Recipient?.length} recipients?`)
|
||||
) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
Prepare to Send
|
||||
</Button>
|
||||
</div>
|
||||
@ -120,11 +109,7 @@ export async function getServerSideProps(context: any) {
|
||||
const { id: documentId } = context.query;
|
||||
|
||||
try {
|
||||
const document: PrismaDocument = await getDocument(
|
||||
+documentId,
|
||||
context.req,
|
||||
context.res
|
||||
);
|
||||
const document: PrismaDocument = await getDocument(+documentId, context.req, context.res);
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@ -1,32 +1,36 @@
|
||||
import Head from "next/head";
|
||||
import { ReactElement, useRef, useState } from "react";
|
||||
import Head from "next/head";
|
||||
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 { NextPageWithLayout } from "../../_app";
|
||||
import { classNames, NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
CheckBadgeIcon,
|
||||
CheckIcon,
|
||||
EnvelopeIcon,
|
||||
PaperAirplaneIcon,
|
||||
PencilSquareIcon,
|
||||
TrashIcon,
|
||||
UserPlusIcon,
|
||||
EnvelopeIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
import { Document as PrismaDocument, DocumentStatus } from "@prisma/client";
|
||||
import { Breadcrumb, Button, Dialog, IconButton } from "@documenso/ui";
|
||||
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
|
||||
|
||||
import { DocumentStatus, Document as PrismaDocument, Recipient } from "@prisma/client";
|
||||
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useSubscription } from "@documenso/lib/stripe";
|
||||
|
||||
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 { hasSubscription } = useSubscription();
|
||||
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
|
||||
const breadcrumbItems = [
|
||||
{
|
||||
@ -35,7 +39,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
},
|
||||
{
|
||||
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",
|
||||
@ -61,7 +68,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
});
|
||||
const formValues = useWatch({ control, name: "signers" });
|
||||
const cancelButtonRef = useRef(null);
|
||||
const hasEmailError = (formValue: any): boolean => {
|
||||
const hasEmailError = (formValue: FormSigner): boolean => {
|
||||
const index = formValues.findIndex((e) => e.id === formValue.id);
|
||||
return !!errors?.signers?.[index]?.email;
|
||||
};
|
||||
@ -71,80 +78,85 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<div className="px-6 mt-10 sm:px-0">
|
||||
<div className="mt-10 px-6 sm:px-0">
|
||||
<div>
|
||||
<Breadcrumb document={props.document} items={breadcrumbItems} />
|
||||
</div>
|
||||
<div className="mt-2 md:flex md:items-center md:justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.document.title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 mt-4 md:mt-0 md:ml-4">
|
||||
<div className="mt-4 flex flex-shrink-0 md:mt-0 md:ml-4">
|
||||
<Button
|
||||
icon={ArrowDownTrayIcon}
|
||||
color="secondary"
|
||||
className="mr-2"
|
||||
href={"/api/documents/" + props.document.id}
|
||||
>
|
||||
href={"/api/documents/" + props.document.id}>
|
||||
Download
|
||||
</Button>
|
||||
<Button
|
||||
icon={PencilSquareIcon}
|
||||
disabled={props.document.status === DocumentStatus.COMPLETED}
|
||||
color={props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"}
|
||||
className="mr-2"
|
||||
href={breadcrumbItems[1].href}
|
||||
>
|
||||
Edit Document
|
||||
</Button>
|
||||
<Button
|
||||
className="min-w-[125px]"
|
||||
color="primary"
|
||||
icon={PaperAirplaneIcon}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
disabled={
|
||||
(formValues.length || 0) === 0 ||
|
||||
!formValues.some(
|
||||
(r: any) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
||||
) ||
|
||||
loading
|
||||
}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
{props.document.status !== DocumentStatus.COMPLETED && (
|
||||
<>
|
||||
<Button
|
||||
icon={PencilSquareIcon}
|
||||
disabled={props.document.status === DocumentStatus.COMPLETED}
|
||||
color={
|
||||
props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"
|
||||
}
|
||||
className="mr-2"
|
||||
href={breadcrumbItems[1].href}>
|
||||
Edit Document
|
||||
</Button>
|
||||
<Button
|
||||
className="min-w-[125px]"
|
||||
color="primary"
|
||||
icon={PaperAirplaneIcon}
|
||||
onClick={() => {
|
||||
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 className="p-4 mt-10 overflow-hidden bg-white rounded-md shadow sm:p-6">
|
||||
<div className="pb-3 border-b border-gray-200 sm:pb-5">
|
||||
<div className="mt-10 overflow-hidden rounded-md bg-white p-4 shadow sm:p-6">
|
||||
<div className="border-b border-gray-200 pb-3 sm:pb-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900 ">Signers</h3>
|
||||
<p className="max-w-4xl mt-2 text-sm text-gray-500">
|
||||
The people who will sign the document.
|
||||
<p className="mt-2 max-w-4xl text-sm text-gray-500">
|
||||
{props.document.status !== DocumentStatus.COMPLETED
|
||||
? "The people who will sign the document."
|
||||
: "The people who signed the document."}
|
||||
</p>
|
||||
</div>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onChange={() => {
|
||||
trigger();
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<ul role="list" className="divide-y divide-gray-200">
|
||||
{fields.map((item: any, index: number) => (
|
||||
{fields.map((item, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="w-full px-2 py-3 border-0 hover:bg-green-50 group sm:py-4"
|
||||
>
|
||||
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 className="block space-y-2 md:space-x-2 md:space-y-0 md:flex">
|
||||
<div className="block space-y-2 md:flex md:space-x-2 md:space-y-0">
|
||||
<div
|
||||
className={classNames(
|
||||
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
|
||||
"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">
|
||||
Email
|
||||
</label>
|
||||
@ -170,8 +182,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
|
||||
placeholder="john.dorian@loremipsum.com"
|
||||
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"
|
||||
/>
|
||||
{errors?.signers?.[index] ? (
|
||||
<p className="mt-2 text-sm text-red-600" id="email-error">
|
||||
@ -183,10 +194,9 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
|
||||
"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>
|
||||
@ -209,121 +219,118 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
documentId: props.document.id,
|
||||
});
|
||||
}}
|
||||
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
|
||||
placeholder="John Dorian"
|
||||
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 className="flex items-center space-x-2 lg:ml-2">
|
||||
<div className="flex mb-2 mr-2 lg:mr-0">
|
||||
<div className="mb-2 mr-2 flex lg:mr-0">
|
||||
<div key={item.id} className="space-x-2">
|
||||
{item.sendStatus === "NOT_SENT" ? (
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
|
||||
>
|
||||
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">
|
||||
Not Sent
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
) : null}
|
||||
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
|
||||
<span id="sent_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800 "
|
||||
>
|
||||
<CheckIcon className="inline h-5 mr-1" /> Sent
|
||||
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 ">
|
||||
<CheckIcon className="mr-1 inline h-5" /> Sent
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
) : null}
|
||||
{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>
|
||||
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">
|
||||
<CheckIcon className="-mr-2 inline h-5"></CheckIcon>
|
||||
<CheckIcon className="mr-1 inline h-5"></CheckIcon>
|
||||
Seen
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
) : null}
|
||||
{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>
|
||||
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>
|
||||
Signed
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex mr-1">
|
||||
<IconButton
|
||||
icon={PaperAirplaneIcon}
|
||||
disabled={
|
||||
!item.id ||
|
||||
item.sendStatus !== "SENT" ||
|
||||
item.signingStatus === "SIGNED" ||
|
||||
loading
|
||||
}
|
||||
color="secondary"
|
||||
className="my-auto mr-4 h-9"
|
||||
onClick={() => {
|
||||
if (confirm("Resend this signing request?")) {
|
||||
setLoading(true);
|
||||
sendSigningRequests(props.document, [item.id]).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Resend
|
||||
</IconButton>
|
||||
<IconButton
|
||||
icon={TrashIcon}
|
||||
disabled={!item.id || item.sendStatus === "SENT" || loading}
|
||||
onClick={() => {
|
||||
const removedItem = { ...fields }[index];
|
||||
remove(index);
|
||||
deleteRecipient(item)?.catch((err) => {
|
||||
append(removedItem);
|
||||
});
|
||||
}}
|
||||
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
{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>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button
|
||||
icon={UserPlusIcon}
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
createOrUpdateRecipient({
|
||||
id: "",
|
||||
email: "",
|
||||
name: "",
|
||||
documentId: props.document.id,
|
||||
}).then((res) => {
|
||||
append(res);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add Signer
|
||||
</Button>
|
||||
{props.document.status !== "COMPLETED" && (
|
||||
<Button
|
||||
icon={UserPlusIcon}
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
createOrUpdateRecipient({
|
||||
id: "",
|
||||
email: "",
|
||||
name: "",
|
||||
documentId: props.document.id,
|
||||
}).then((res) => {
|
||||
append(res);
|
||||
});
|
||||
}}>
|
||||
Add Signer
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
@ -336,7 +343,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
open={open}
|
||||
setLoading={setLoading}
|
||||
setOpen={setOpen}
|
||||
icon={<EnvelopeIcon className="w-6 h-6 text-green-600" aria-hidden="true" />}
|
||||
icon={<EnvelopeIcon className="h-6 w-6 text-green-600" aria-hidden="true" />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import prisma from "@documenso/prisma";
|
||||
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 prisma from "@documenso/prisma";
|
||||
import PDFSigner from "../../../components/editor/pdf-signer";
|
||||
import { NextPageWithLayout } from "../../_app";
|
||||
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) => {
|
||||
return (
|
||||
@ -14,36 +14,22 @@ const SignPage: NextPageWithLayout = (props: any) => {
|
||||
<title>Sign | Documenso</title>
|
||||
</Head>
|
||||
{!props.expired ? (
|
||||
<PDFSigner
|
||||
document={props.document}
|
||||
recipient={props.recipient}
|
||||
fields={props.fields}
|
||||
/>
|
||||
<PDFSigner 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">
|
||||
<ClockIcon className="text-neon w-10 inline mr-1"></ClockIcon>
|
||||
<h1 className="text-base font-medium text-neon inline align-middle">
|
||||
Time flies.
|
||||
</h1>
|
||||
<p className="mt-2 text-4xl font-bold tracking-tight">
|
||||
This signing link is expired.
|
||||
</p>
|
||||
<ClockIcon className="text-neon mr-1 inline w-10"></ClockIcon>
|
||||
<h1 className="text-neon inline align-middle text-base font-medium">Time flies.</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">
|
||||
Please ask{" "}
|
||||
{props.document.User.name
|
||||
? `${props.document.User.name}`
|
||||
: `the sender`}{" "}
|
||||
Please ask {props.document.User.name ? `${props.document.User.name}` : `the sender`}{" "}
|
||||
to resend it.
|
||||
</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 className="relative mx-96">
|
||||
<div
|
||||
className="absolute inset-0 flex items-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center"></div>
|
||||
@ -51,10 +37,7 @@ const SignPage: NextPageWithLayout = (props: any) => {
|
||||
</div>
|
||||
<p className="mt-4 text-center text-sm text-gray-600">
|
||||
Want to send of your own?{" "}
|
||||
<Link
|
||||
href="/signup?source=expired"
|
||||
className="font-medium text-neon hover:text-neon"
|
||||
>
|
||||
<Link href="/signup?source=expired" className="text-neon hover:text-neon font-medium">
|
||||
Create your own Account
|
||||
</Link>
|
||||
</p>
|
||||
@ -76,7 +59,7 @@ export async function getServerSideProps(context: any) {
|
||||
},
|
||||
});
|
||||
|
||||
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token: recipientToken,
|
||||
},
|
||||
@ -85,12 +68,21 @@ export async function getServerSideProps(context: any) {
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipient) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/404",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Document is already signed
|
||||
if (recipient.Document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: `/documents/${recipient.Document.id}/signed`,
|
||||
destination: `/documents/${recipient.Document.id}/signed?token=${recipientToken}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -118,13 +110,9 @@ export async function getServerSideProps(context: any) {
|
||||
return {
|
||||
props: {
|
||||
recipient: JSON.parse(JSON.stringify(recipient)),
|
||||
document: JSON.parse(
|
||||
JSON.stringify({ ...recipient.Document, document: "" })
|
||||
),
|
||||
document: JSON.parse(JSON.stringify({ ...recipient.Document, document: "" })),
|
||||
fields: JSON.parse(JSON.stringify(unsignedFields)),
|
||||
expired: recipient.expired
|
||||
? new Date(recipient.expired) < new Date()
|
||||
: false,
|
||||
expired: recipient.expired ? new Date(recipient.expired) < new Date() : false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import prisma from "@documenso/prisma";
|
||||
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 { 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";
|
||||
import { truncate } from "@documenso/lib/helpers";
|
||||
|
||||
const Signed: NextPageWithLayout = (props: any) => {
|
||||
const router = useRouter();
|
||||
@ -18,29 +19,18 @@ const Signed: NextPageWithLayout = (props: any) => {
|
||||
<title>Sign | Documenso</title>
|
||||
</Head>
|
||||
<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>
|
||||
<h1 className="text-base font-medium text-neon inline align-middle">
|
||||
It's done!
|
||||
</h1>
|
||||
<CheckBadgeIcon className="text-neon mr-1 inline w-10"></CheckBadgeIcon>
|
||||
<h1 className="text-neon inline align-middle text-base font-medium">It's done!</h1>
|
||||
<p className="mt-2 text-4xl font-bold tracking-tight">
|
||||
You signed "{props.document.title}"
|
||||
You signed "{truncate(props.document.title)}"
|
||||
</p>
|
||||
<p
|
||||
className="mt-2 text-base text-gray-500 max-w-sm"
|
||||
hidden={allRecipientsSigned}
|
||||
>
|
||||
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={allRecipientsSigned}>
|
||||
You will be notfied when all recipients have signed.
|
||||
</p>
|
||||
<p
|
||||
className="mt-2 text-base text-gray-500 max-w-sm"
|
||||
hidden={!allRecipientsSigned}
|
||||
>
|
||||
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={!allRecipientsSigned}>
|
||||
All recipients signed.
|
||||
</p>
|
||||
<div
|
||||
className="mx-auto w-fit text-xl pt-20"
|
||||
hidden={!allRecipientsSigned}
|
||||
>
|
||||
<div className="mx-auto w-fit pt-20 text-xl" hidden={!allRecipientsSigned}>
|
||||
<Button
|
||||
icon={ArrowDownTrayIcon}
|
||||
color="secondary"
|
||||
@ -48,23 +38,16 @@ const Signed: NextPageWithLayout = (props: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
router.push(
|
||||
"/api/documents/" +
|
||||
props.document.id +
|
||||
"?token=" +
|
||||
props.recipient.token
|
||||
"/api/documents/" + props.document.id + "?token=" + props.recipient.token
|
||||
);
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
Download "{props.document.title}"
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="relative mx-96">
|
||||
<div
|
||||
className="absolute inset-0 flex items-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center"></div>
|
||||
@ -72,11 +55,8 @@ const Signed: NextPageWithLayout = (props: any) => {
|
||||
</div>
|
||||
<p className="mt-4 text-center text-sm text-gray-600">
|
||||
Want to send slick signing links like this one?{" "}
|
||||
<Link
|
||||
href="https://documenso.com"
|
||||
className="font-medium text-neon hover:text-neon"
|
||||
>
|
||||
Hosted Documenso is coming soon™
|
||||
<Link href="https://documenso.com" className="text-neon hover:text-neon font-medium">
|
||||
Hosted Documenso is here!
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
|
||||
32
apps/web/pages/forgot-password.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { GetServerSideProps, GetServerSidePropsContext } from "next";
|
||||
import Head from "next/head";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import ForgotPassword from "../components/forgot-password";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Forgot Password | Documenso</title>
|
||||
</Head>
|
||||
<ForgotPassword />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps({ req }: GetServerSidePropsContext) {
|
||||
const user = await getUserFromToken(req);
|
||||
|
||||
if (user)
|
||||
return {
|
||||
redirect: {
|
||||
source: "/login",
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import Head from "next/head";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import Login from "../components/login";
|
||||
|
||||
export default function LoginPage(props: any) {
|
||||
@ -13,11 +14,21 @@ export default function LoginPage(props: any) {
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true";
|
||||
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: ALLOW_SIGNUP,
|
||||
ALLOW_SIGNUP,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
import SettingsPage from ".";
|
||||
|
||||
export default SettingsPage;
|
||||
|
||||
1
apps/web/pages/settings/billing.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { default } from ".";
|
||||
@ -1,2 +1,3 @@
|
||||
import SettingsPage from ".";
|
||||
|
||||
export default SettingsPage;
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
import SettingsPage from ".";
|
||||
|
||||
export default SettingsPage;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { NextPageContext } from "next";
|
||||
import Head from "next/head";
|
||||
import { getUserFromToken } from "@documenso/lib/server";
|
||||
import Signup from "../components/signup";
|
||||
|
||||
export default function SignupPage(props: { source: string }) {
|
||||
@ -14,7 +15,7 @@ export default function SignupPage(props: { source: string }) {
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: any) {
|
||||
if (process.env.ALLOW_SIGNUP !== "true")
|
||||
if (process.env.NEXT_PUBLIC_ALLOW_SIGNUP !== "true")
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
@ -22,6 +23,16 @@ export async function getServerSideProps(context: any) {
|
||||
},
|
||||
};
|
||||
|
||||
const user = await getUserFromToken(context.req, context.res);
|
||||
if (user)
|
||||
return {
|
||||
redirect: {
|
||||
source: "/signup",
|
||||
destination: "/dashboard",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
|
||||
const signupSource: string = context.query["source"];
|
||||
return {
|
||||
props: {
|
||||
|
||||
@ -3,4 +3,4 @@ module.exports = {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
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/public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/web/public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
apps/web/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/web/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 529 B |
BIN
apps/web/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 15 KiB |
BIN
apps/web/public/logo.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 29 KiB |
19
apps/web/public/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Documenso",
|
||||
"short_name": "Documenso",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#A2E771",
|
||||
"background_color": "#FFFFFF",
|
||||
"display": "standalone"
|
||||
}
|
||||
@ -6,37 +6,3 @@
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
:host {
|
||||
font-family: montserrat;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Qwigley";
|
||||
src: url("/fonts/Qwigley-Regular.ttf");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Montserrat";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/fonts/montserrat.woff2") format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
||||
U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Montserrat";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("/fonts/montserrat.woff2") format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
|
||||
U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@ -12,13 +12,65 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
monteserrat: ["Monteserrat", "serif"],
|
||||
qwigley: ["Qwigley", "serif"],
|
||||
sans: ["var(--font-sans)", ...defaultTheme.fontFamily.sans],
|
||||
qwigley: ["var(--font-qwigley)", "serif"],
|
||||
},
|
||||
colors: {
|
||||
neon: "#37f095",
|
||||
"neon-dark": "#2CC077",
|
||||
brown: "#353434",
|
||||
neon: {
|
||||
DEFAULT: "#37F095",
|
||||
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",
|
||||
},
|
||||
brand: {
|
||||
DEFAULT: "#A2E771",
|
||||
100: "#F4FCEE",
|
||||
200: "#E8F9DC",
|
||||
300: "#D1F3B9",
|
||||
400: "#BBED96",
|
||||
500: "#A2E771",
|
||||
600: "#8DE151",
|
||||
700: "#76DC2E",
|
||||
800: "#63C021",
|
||||
900: "#519D1B",
|
||||
950: "#488C18",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
"4xl": "2rem",
|
||||
|
||||
@ -21,6 +21,6 @@
|
||||
"../../packages/types/next-auth.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
, "../../packages/lib/process-env.d.ts" ],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
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
@ -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 "documenso:latest" \
|
||||
-t "documenso:$GIT_SHA" \
|
||||
-t "documenso:$APP_VERSION" \
|
||||
"$MONOREPO_ROOT"
|
||||
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
|
||||
18
docker/compose-without-app.yml
Normal file
@ -0,0 +1,18 @@
|
||||
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
@ -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
|
||||
9417
package-lock.json
generated
44
package.json
@ -2,12 +2,17 @@
|
||||
"name": "documenso-monorepo",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "cd apps && cd web && next dev",
|
||||
"build": "npm i && cd apps && cd web && npm i && next build",
|
||||
"start": "cd apps && cd web && next start",
|
||||
"dev": "turbo run dev --filter=web",
|
||||
"build": "turbo run build --filter=web",
|
||||
"start": "turbo run start --filter=web",
|
||||
"db-migrate:dev": "prisma migrate dev",
|
||||
"db-seed": "prisma db seed",
|
||||
"db-studio": "prisma studio"
|
||||
"db-studio": "prisma studio",
|
||||
"docker:compose-up": "docker compose -p documenso -f ./docker/compose-without-app.yml up -d || docker-compose -p documenso -f ./docker/compose-without-app.yml up -d",
|
||||
"docker:compose-down": "docker compose -p documenso -f ./docker/compose-without-app.yml down || docker-compose -p documenso -f ./docker/compose-without-app.yml 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": [
|
||||
"apps/*",
|
||||
@ -21,27 +26,34 @@
|
||||
"@documenso/prisma": "*",
|
||||
"@headlessui/react": "^1.7.4",
|
||||
"@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",
|
||||
"@types/react-signature-canvas": "^1.0.2",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"avatar-from-initials": "^1.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "8.27.0",
|
||||
"eslint-config-next": "13.0.3",
|
||||
"install": "^0.13.0",
|
||||
"next": "13.0.3",
|
||||
"next": "13.2.4",
|
||||
"next-auth": ">=4.20.1",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"npm": "^9.1.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.41.5",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-signature-canvas": "^1.0.6",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"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",
|
||||
"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",
|
||||
"turbo": "^1.9.9",
|
||||
"typescript": "4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,10 +14,8 @@ export const createField = (
|
||||
if (newFieldX < 0) newFieldX = 0;
|
||||
if (newFieldY < 0) newFieldY = 0;
|
||||
|
||||
if (newFieldX + fieldSize.width > rect.width)
|
||||
newFieldX = rect.width - fieldSize.width;
|
||||
if (newFieldY + fieldSize.height > rect.height)
|
||||
newFieldY = rect.height - fieldSize.height;
|
||||
if (newFieldX + fieldSize.width > rect.width) newFieldX = rect.width - fieldSize.width;
|
||||
if (newFieldY + fieldSize.height > rect.height) newFieldY = rect.height - fieldSize.height;
|
||||
|
||||
const signatureField = {
|
||||
id: -1,
|
||||
|
||||
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
@ -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,9 +1,10 @@
|
||||
import { ChangeEvent } from "react";
|
||||
import router from "next/router";
|
||||
import toast from "react-hot-toast";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export const uploadDocument = async (event: any) => {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
export const uploadDocument = async (event: ChangeEvent) => {
|
||||
if (event.target instanceof HTMLInputElement && event.target?.files && event.target.files[0]) {
|
||||
const body = new FormData();
|
||||
const document = event.target.files[0];
|
||||
const fileName: string = event.target.files[0].name;
|
||||
@ -12,25 +13,31 @@ export const uploadDocument = async (event: any) => {
|
||||
toast.error("Non-PDF documents are not supported yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
body.append("document", document || "");
|
||||
const response: any = await toast
|
||||
.promise(
|
||||
fetch("/api/documents", {
|
||||
method: "POST",
|
||||
body,
|
||||
}),
|
||||
{
|
||||
loading: "Uploading document...",
|
||||
success: `${fileName} uploaded successfully.`,
|
||||
error: "Could not upload document :/",
|
||||
|
||||
await toast.promise(
|
||||
fetch("/api/documents", {
|
||||
method: "POST",
|
||||
body,
|
||||
}).then((response: Response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Could not upload document");
|
||||
}
|
||||
)
|
||||
.then((response: Response) => {
|
||||
|
||||
response.json().then((createdDocumentIdFromBody) => {
|
||||
router.push(
|
||||
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${createdDocumentIdFromBody}/recipients`
|
||||
);
|
||||
});
|
||||
});
|
||||
}),
|
||||
{
|
||||
loading: "Uploading document...",
|
||||
success: `${fileName} uploaded successfully.`,
|
||||
error: "Could not upload document :/",
|
||||
}
|
||||
).catch((_err) => {
|
||||
// Do nothing
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -6,16 +6,13 @@ export const deleteRecipient = (recipient: any) => {
|
||||
}
|
||||
|
||||
return toast.promise(
|
||||
fetch(
|
||||
"/api/documents/" + recipient.documentId + "/recipients/" + recipient.id,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(recipient),
|
||||
}
|
||||
),
|
||||
fetch("/api/documents/" + recipient.documentId + "/recipients/" + recipient.id, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(recipient),
|
||||
}),
|
||||
{
|
||||
loading: "Deleting...",
|
||||
success: "Deleted.",
|
||||
|
||||