Compare commits
197 Commits
fix/improv
...
docs/add-g
| Author | SHA1 | Date | |
|---|---|---|---|
| 652951ed7f | |||
| 40db5baa17 | |||
| 4cdfa2d1f0 | |||
| ba82b1fca8 | |||
| 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 | |||
| 95c3be9a77 |
18
.env.example
@ -4,8 +4,8 @@
|
||||
# Option 3: Use the provided dx setup (RECOMMENDED)
|
||||
# => postgres://documenso:password@127.0.0.1:54320/documenso
|
||||
#
|
||||
# ⚠ WARNING: The test database can be resetted or taken offline at any point.
|
||||
# ⚠ WARNING: Please be aware that nothing written to the test databae is private.
|
||||
# ⚠ WARNING: 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
|
||||
@ -16,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
|
||||
@ -37,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
|
||||
|
||||
29
.env.gitpod
Normal file
@ -0,0 +1,29 @@
|
||||
DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
|
||||
NEXT_PUBLIC_WEBAPP_URL=""
|
||||
|
||||
# AUTH
|
||||
NEXTAUTH_SECRET='lorem ipsum sit dolor random string for encryption this could literally be anything'
|
||||
NEXTAUTH_URL=""
|
||||
|
||||
# SIGNING
|
||||
CERT_FILE_PATH=""
|
||||
CERT_PASSPHRASE=""
|
||||
CERT_FILE_ENCODING=""
|
||||
|
||||
# EMAIL
|
||||
SMTP_MAIL_HOST='127.0.0.1'
|
||||
SMTP_MAIL_PORT='2500'
|
||||
SMTP_MAIL_USER='documenso'
|
||||
SMTP_MAIL_PASSWORD='documenso'
|
||||
MAIL_FROM='documenso@gitpod.io'
|
||||
|
||||
# STRIPE
|
||||
STRIPE_API_KEY=""
|
||||
STRIPE_WEBHOOK_SECRET=""
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=""
|
||||
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=""
|
||||
|
||||
#FEATURE FLAGS
|
||||
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
|
||||
NEXT_PUBLIC_ALLOW_SIGNUP=true
|
||||
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=false
|
||||
3
.gitignore
vendored
@ -36,3 +36,6 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
.env
|
||||
.env.example
|
||||
|
||||
# turborepo
|
||||
.turbo
|
||||
60
.gitpod.yml
Normal file
@ -0,0 +1,60 @@
|
||||
tasks:
|
||||
- name: Dependencies & Database
|
||||
init: |
|
||||
npm install &&
|
||||
npm run docker:compose-up &&
|
||||
cp .env.gitpod .env &&
|
||||
next_auth_secret=$(openssl rand -base64 32) &&
|
||||
sed -i -e "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=$next_auth_secret|" .env &&
|
||||
sed -i "s|NEXTAUTH_URL=\"\"|NEXTAUTH_URL=https://3000-${HOSTNAME}.${GITPOD_WORKSPACE_CLUSTER_HOST}|" .env &&
|
||||
sed -i "s|NEXT_PUBLIC_WEBAPP_URL=\"\"|NEXT_PUBLIC_WEBAPP_URL=https://3000-${HOSTNAME}.${GITPOD_WORKSPACE_CLUSTER_HOST}|" .env
|
||||
command: npm run d
|
||||
|
||||
- name: Database Studio
|
||||
command: |
|
||||
gp ports await 3000
|
||||
npm run db-studio
|
||||
|
||||
ports:
|
||||
- name: App
|
||||
port: 3000
|
||||
visibility: public
|
||||
onOpen: open-browser
|
||||
|
||||
- name: Mailbox
|
||||
port: 9000
|
||||
visibility: public
|
||||
onOpen: open-browser
|
||||
|
||||
- name: Database
|
||||
port: 54320
|
||||
visibility: private
|
||||
onOpen: ignore
|
||||
|
||||
- name: Mailserver
|
||||
port: 2500
|
||||
visibility: private
|
||||
onOpen: ignore
|
||||
|
||||
- name: Database Studio
|
||||
port: 5555
|
||||
visibility: public
|
||||
|
||||
- port: 1100
|
||||
visibility: private
|
||||
onOpen: ignore
|
||||
|
||||
github:
|
||||
prebuilds:
|
||||
master: true
|
||||
pullRequests: true
|
||||
pullRequestsFromForks: true
|
||||
addCheck: true
|
||||
addComment: true
|
||||
addBadge: true
|
||||
|
||||
vscode:
|
||||
extensions:
|
||||
- esbenp.prettier-vscode
|
||||
- bradlc.vscode-tailwindcss
|
||||
- Prisma.prisma
|
||||
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.
|
||||
139
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,7 +74,11 @@ 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
|
||||
|
||||
@ -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
|
||||
@ -111,12 +118,12 @@ Want to get up and running quickly? Follow these steps:
|
||||
git clone https://github.com/documenso/documenso
|
||||
```
|
||||
|
||||
- Set up your .env file using the recommendations in the .env.example file.
|
||||
- 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
|
||||
```sh
|
||||
npm run d
|
||||
```
|
||||
|
||||
@ -124,61 +131,77 @@ 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 `5432`. You can connect to it using your favorite database client.
|
||||
Your database will also be available on port `54320`. You can connect to it using your favorite database client.
|
||||
|
||||
## Developer Setup
|
||||
|
||||
### Manual Setup
|
||||
|
||||
Follow these steps to setup documenso on you local machine:
|
||||
|
||||
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||
```sh
|
||||
git clone https://github.com/documenso/documenso
|
||||
```
|
||||
- Run <code>npm i</code> in root directory
|
||||
- Rename <code>.env.example</code> to <code>.env</code>
|
||||
- Run `npm i` in root directory
|
||||
- Rename `.env.example` to `.env`
|
||||
- Set DATABASE_URL value in .env file
|
||||
- You can use the provided test database url (may be wiped at any point)
|
||||
- Or setup a local postgres sql instance (recommended)
|
||||
- Create the database scheme by running <code>db-migrate:dev</code>
|
||||
- Create the database scheme by running `db-migrate:dev`
|
||||
- Setup your mail provider
|
||||
- Set <code>SENDGRID_API_KEY</code> value in .env file
|
||||
- Set `SENDGRID_API_KEY` value in .env file
|
||||
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
|
||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* variables</code> in your .env
|
||||
- Run <code>npm run dev</code> root directory to start
|
||||
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the `SMTP
|
||||
\_
|
||||
* variables` in your .env
|
||||
- Run `npm run dev` root directory to start
|
||||
- Register a new user at http://localhost:3000/signup
|
||||
|
||||
---
|
||||
|
||||
- Optional: Seed the database using <code>npm run db-seed</code> to create a test user and document
|
||||
- Optional: Upload and sign <code>apps\web\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 your own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signing certificate**.
|
||||
- A demo certificate is provided in `/app/web/resources/certificate.p12`
|
||||
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](#creating-your-own-signing-certificate)**.
|
||||
|
||||
### Run in Gitpod
|
||||
|
||||
- Click below to launch a ready-to-use Gitpod workspace in your browser.
|
||||
|
||||
[](https://gitpod.io/#https://github.com/documenso/documenso)
|
||||
|
||||
## Updating
|
||||
|
||||
- If you pull the newest version from main, using <code>git pull</code>, it may be necessary to regenerate your database client
|
||||
- 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 necessary on first clone
|
||||
- This is not necessary on first clone.
|
||||
|
||||
# Creating your own signing certificate
|
||||
|
||||
For the digital signature of your documents you need a signing 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`
|
||||
|
||||
# Docker
|
||||
|
||||
@ -189,7 +212,45 @@ Want to create a production ready docker image? Follow these steps:
|
||||
- Run `./docker/build.sh` in the root directory.
|
||||
- Publish the image to your docker registry of choice.
|
||||
|
||||
# Deploying - Coming Soon™
|
||||
# Deployment
|
||||
|
||||
- Docker support
|
||||
- One-Click-Deploy on Render.com Deploy
|
||||
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
||||
|
||||
## Railway
|
||||
|
||||
[](https://railway.app/template/DjrRRX)
|
||||
|
||||
# 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -70,10 +70,12 @@ 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="-mt-2.5 h-12 w-12"></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 items-center justify-start text-center md:flex md:justify-between">
|
||||
|
||||
<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})`
|
||||
|
||||
@ -17,7 +17,7 @@ export default function SignatureDialog(props: any) {
|
||||
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 occuring.
|
||||
// we also need the debounce to avoid rendering while transitions are occurring.
|
||||
const showCanvas = useDebouncedValue<boolean>(props.open, 1);
|
||||
let signCanvasRef: any | undefined;
|
||||
|
||||
|
||||
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,7 +1,12 @@
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
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() {
|
||||
@ -30,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>
|
||||
|
||||
@ -69,7 +69,7 @@ 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>
|
||||
@ -111,9 +111,11 @@ export default function Login(props: any) {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm">
|
||||
<a href="#" className="text-gray-500 hover:text-neon-700 font-medium">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="hover:text-neon-700 font-medium text-gray-500">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@ -123,7 +125,7 @@ export default function Login(props: any) {
|
||||
className="group relative flex w-full">
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<LockClosedIcon
|
||||
className="text-neon-700 group-hover:text-neon-dark-700 h-5 w-5 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600 duration-200"
|
||||
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>
|
||||
@ -141,7 +143,9 @@ export default function Login(props: any) {
|
||||
{props.allowSignup ? (
|
||||
<p className="mt-2 text-center text-sm text-gray-600">
|
||||
Are you new here?{" "}
|
||||
<Link href="/signup" className="text-gray-500 hover:text-neon-700 duration-200 font-medium">
|
||||
<Link
|
||||
href="/signup"
|
||||
className="hover:text-neon-700 font-medium text-gray-500 duration-200">
|
||||
Create a new Account
|
||||
</Link>
|
||||
</p>
|
||||
@ -151,7 +155,7 @@ export default function Login(props: any) {
|
||||
<Link
|
||||
href="https://documenso.com"
|
||||
className="text-neon hover:text-neon font-medium">
|
||||
Hosted Documenso will be availible soon™
|
||||
Hosted Documenso is here!
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@ -4,16 +4,77 @@ 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -112,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
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -4,8 +4,11 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { updateUser } from "@documenso/features";
|
||||
import { getUser } from "@documenso/lib/api";
|
||||
import { fetchPortalSession, isSubscriptionsEnabled, useSubscription } from "@documenso/lib/stripe";
|
||||
import { Button } from "@documenso/ui";
|
||||
import { KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
|
||||
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;
|
||||
@ -158,22 +171,110 @@ export default function Setttings() {
|
||||
<Button onClick={() => updateUser(user)}>Save</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation[1].name}
|
||||
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9">
|
||||
{/* Passwords section */}
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium leading-6 text-gray-900">Password</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Forgot your passwort? Email <b>hi@documenso.com</b> to reset it.
|
||||
</p>
|
||||
<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,23 +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;
|
||||
|
||||
@ -7,36 +7,27 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"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,14 +1,16 @@
|
||||
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 isolate min-h-full bg-gray-100">
|
||||
<div className="absolute top-10 left-10">
|
||||
<Logo className="w-10 md:w-20" />
|
||||
</div>
|
||||
<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="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>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import Link from "next/link";
|
||||
import { Button } from "@documenso/ui";
|
||||
import Logo from "../components/logo";
|
||||
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
|
||||
@ -7,9 +8,10 @@ export default function Custom500() {
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex min-h-full flex-col items-center justify-center bg-black text-white">
|
||||
<div className="absolute top-10 left-10">
|
||||
<Logo dark className="w-10 md:w-20" />
|
||||
</div>
|
||||
<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="mt-20 max-w-7xl px-4 py-10">
|
||||
<p className="inline-flex items-center text-3xl font-bold sm:text-5xl">
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
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 "../styles/tailwind.css";
|
||||
@ -10,6 +12,20 @@ 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;
|
||||
};
|
||||
@ -20,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,13 +1,14 @@
|
||||
import { Head, Html, Main, NextScript } from "next/document";
|
||||
import Script from "next/script";
|
||||
|
||||
export default function Document(props) {
|
||||
let pageProps = props.__NEXT_DATA__?.props?.pageProps;
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<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 />
|
||||
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) }),
|
||||
});
|
||||
@ -8,13 +8,13 @@ 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.",
|
||||
});
|
||||
}
|
||||
|
||||
@ -6,53 +6,65 @@ 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({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -4,6 +4,7 @@ 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
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,4 +1,5 @@
|
||||
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";
|
||||
|
||||
@ -24,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: {
|
||||
@ -31,6 +39,7 @@ async function patchHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
data: {
|
||||
name: updatedUser.name,
|
||||
password,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -20,12 +20,15 @@ import {
|
||||
} from "@prisma/client";
|
||||
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",
|
||||
@ -90,9 +93,12 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
if (hasSubscription) {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}
|
||||
}}
|
||||
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={!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="mx-auto h-12 w-12 text-gray-400 group-hover:text-gray-700 duration-200"
|
||||
|
||||
@ -4,6 +4,7 @@ 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";
|
||||
@ -23,6 +24,7 @@ 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([]);
|
||||
|
||||
@ -135,6 +137,7 @@ 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();
|
||||
}}>
|
||||
@ -142,24 +145,24 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 mb-12 flex flex-row-reverse items-center gap-x-4">
|
||||
<div className="pt-5 block w-fit">
|
||||
{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="block w-1/4"
|
||||
label="Created"
|
||||
options={createdFilter}
|
||||
value={selectedCreatedFilter}
|
||||
onChange={setSelectedCreatedFilter}
|
||||
/>
|
||||
<SelectBox
|
||||
className="block w-1/4"
|
||||
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}
|
||||
/>
|
||||
<div className="block w-fit pt-5">
|
||||
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 max-w-[1100px]" hidden={!loading}>
|
||||
<div className="ph-item">
|
||||
@ -221,13 +224,13 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
{document.title || "#" + document.id}
|
||||
</td>
|
||||
<td className="whitespace-nowrap inline-flex py-3 gap-x-2 gap-y-1 flex-wrap max-w-[250px] 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="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
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>
|
||||
) : (
|
||||
@ -237,7 +240,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<span id="sent_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||
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>
|
||||
@ -250,7 +253,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<span id="read_icon">
|
||||
<span
|
||||
id="sent_icon"
|
||||
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||
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}
|
||||
@ -261,7 +264,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
)}
|
||||
{item.signingStatus === "SIGNED" ? (
|
||||
<span id="signed_icon">
|
||||
<span className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||
<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>
|
||||
@ -378,6 +381,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
icon={PlusIcon}
|
||||
disabled={!hasSubscription}
|
||||
onClick={() => {
|
||||
document?.getElementById("fileUploadHelper")?.click();
|
||||
}}>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
|
||||
import { getDocument } from "@documenso/lib/query";
|
||||
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";
|
||||
@ -14,6 +15,7 @@ import { Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
|
||||
const router = useRouter();
|
||||
const { hasSubscription } = useSubscription();
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
|
||||
@ -4,7 +4,7 @@ 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 } from "@documenso/ui";
|
||||
import { Breadcrumb, Button, Dialog, IconButton, Tooltip } from "@documenso/ui";
|
||||
import Layout from "../../../components/layout";
|
||||
import { NextPageWithLayout } from "../../_app";
|
||||
import {
|
||||
@ -21,6 +21,7 @@ import {
|
||||
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: Array<Pick<Recipient, 'id' | 'email' | 'name' | 'sendStatus' | 'readStatus' | 'signingStatus'>>;
|
||||
@ -29,6 +30,7 @@ export type FormValues = {
|
||||
type FormSigner = FormValues["signers"][number];
|
||||
|
||||
const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
const { hasSubscription } = useSubscription();
|
||||
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
|
||||
const breadcrumbItems = [
|
||||
{
|
||||
@ -116,6 +118,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
: setOpen(true);
|
||||
}}
|
||||
disabled={
|
||||
!hasSubscription ||
|
||||
(formValues.length || 0) === 0 ||
|
||||
!formValues.some(
|
||||
(r) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
|
||||
@ -264,38 +267,46 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
|
||||
</div>
|
||||
{props.document.status !== DocumentStatus.COMPLETED && (
|
||||
<div className="mr-1 flex">
|
||||
<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);
|
||||
});
|
||||
<Tooltip label="Resend">
|
||||
<IconButton
|
||||
icon={PaperAirplaneIcon}
|
||||
disabled={
|
||||
!item.id ||
|
||||
item.sendStatus !== "SENT" ||
|
||||
item.signingStatus === "SIGNED" ||
|
||||
loading
|
||||
}
|
||||
}}>
|
||||
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"
|
||||
/>
|
||||
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>
|
||||
|
||||
@ -59,7 +59,7 @@ export async function getServerSideProps(context: any) {
|
||||
},
|
||||
});
|
||||
|
||||
const recipient = await prisma.recipient.findFirstOrThrow({
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
token: recipientToken,
|
||||
},
|
||||
@ -68,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}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ 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();
|
||||
@ -21,7 +22,7 @@ const Signed: NextPageWithLayout = (props: any) => {
|
||||
<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 max-w-sm text-base text-gray-500" hidden={allRecipientsSigned}>
|
||||
You will be notfied when all recipients have signed.
|
||||
@ -55,7 +56,7 @@ const Signed: NextPageWithLayout = (props: any) => {
|
||||
<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="text-neon hover:text-neon font-medium">
|
||||
Hosted Documenso is coming soon™
|
||||
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: {},
|
||||
};
|
||||
}
|
||||
@ -24,11 +24,11 @@ export async function getServerSideProps(context: any) {
|
||||
},
|
||||
};
|
||||
|
||||
const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true";
|
||||
const ALLOW_SIGNUP = process.env.NEXT_PUBLIC_ALLOW_SIGNUP === "true";
|
||||
|
||||
return {
|
||||
props: {
|
||||
ALLOW_SIGNUP: ALLOW_SIGNUP,
|
||||
ALLOW_SIGNUP,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
1
apps/web/pages/settings/billing.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { default } from ".";
|
||||
@ -15,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",
|
||||
|
||||
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,35 +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,8 +12,8 @@ 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: {
|
||||
@ -58,6 +58,19 @@ module.exports = {
|
||||
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"]
|
||||
}
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
ARG NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
ARG NEXT_PUBLIC_ALLOW_SIGNUP=true
|
||||
ARG NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=false
|
||||
ARG NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
||||
ARG NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID
|
||||
|
||||
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.
|
||||
@ -14,6 +21,13 @@ RUN npm ci --production
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS builder
|
||||
|
||||
ARG NEXT_PUBLIC_WEBAPP_URL
|
||||
ARG NEXT_PUBLIC_ALLOW_SIGNUP
|
||||
ARG NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS
|
||||
ARG NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID
|
||||
ARG NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
@ -28,6 +42,7 @@ RUN npm run build --workspaces
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
@ -22,7 +22,7 @@ echo "Git SHA: $GIT_SHA"
|
||||
|
||||
docker build -f "$SCRIPT_DIR/Dockerfile" \
|
||||
--progress=plain \
|
||||
-t "documentso:latest" \
|
||||
-t "documenso:latest" \
|
||||
-t "documenso:$GIT_SHA" \
|
||||
-t "documenso:$APP_VERSION" \
|
||||
"$MONOREPO_ROOT"
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
name: documenso
|
||||
services:
|
||||
database:
|
||||
image: postgres:15
|
||||
|
||||
@ -33,7 +33,7 @@ services:
|
||||
- SMTP_MAIL_USER=username
|
||||
- SMTP_MAIL_PASSWORD=password
|
||||
- MAIL_FROM=admin@example.com
|
||||
- ALLOW_SIGNUP=true
|
||||
- NEXT_PUBLIC_ALLOW_SIGNUP=true
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
|
||||
8150
package-lock.json
generated
41
package.json
@ -2,15 +2,15 @@
|
||||
"name": "documenso-monorepo",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "npm run dev -w apps/web",
|
||||
"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",
|
||||
"docker:compose": "docker-compose -f ./docker/compose-without-app.yml",
|
||||
"docker:compose-up": "npm run docker:compose -- up -d",
|
||||
"docker:compose-down": "npm run docker:compose -- down",
|
||||
"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"
|
||||
},
|
||||
@ -26,33 +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",
|
||||
"typescript": "4.8.4"
|
||||
"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"
|
||||
"prettier-plugin-tailwindcss": "^0.2.5",
|
||||
"turbo": "^1.9.9",
|
||||
"typescript": "4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
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,7 +1,7 @@
|
||||
import { ChangeEvent } from "react";
|
||||
import router from "next/router";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
|
||||
import toast from "react-hot-toast";
|
||||
import { ChangeEvent } from "react";
|
||||
|
||||
export const uploadDocument = async (event: ChangeEvent) => {
|
||||
if (event.target instanceof HTMLInputElement && event.target?.files && event.target.files[0]) {
|
||||
@ -16,24 +16,28 @@ export const uploadDocument = async (event: ChangeEvent) => {
|
||||
|
||||
body.append("document", document || "");
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
export const NEXT_PUBLIC_WEBAPP_URL =
|
||||
process.env.IS_PULL_REQUEST === "true"
|
||||
? process.env.RENDER_EXTERNAL_URL
|
||||
: process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||
|
||||
console.log("IS_PULL_REQUEST:" + process.env.IS_PULL_REQUEST);
|
||||
console.log("RENDER_EXTERNAL_URL:" + process.env.RENDER_EXTERNAL_URL);
|
||||
console.log("NEXT_PUBLIC_WEBAPP_URL:" + process.env.NEXT_PUBLIC_WEBAPP_URL);
|
||||
: process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||
1
packages/lib/helpers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './strings';
|
||||
13
packages/lib/helpers/strings.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Truncates a title to a given max length substituting the middle with an ellipsis.
|
||||
*/
|
||||
export const truncate = (str: string, maxLength: number = 20) => {
|
||||
if (str.length <= maxLength) {
|
||||
return str;
|
||||
}
|
||||
|
||||
const startLength = Math.ceil((maxLength - 3) / 2);
|
||||
const endLength = Math.floor((maxLength - 3) / 2);
|
||||
|
||||
return `${str.slice(0, startLength)}...${str.slice(-endLength)}`;
|
||||
};
|
||||
@ -1,10 +1,9 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
export const baseEmailTemplate = (message: string, content: string) => {
|
||||
const html = `
|
||||
<div style="background-color: #eaeaea; padding: 2%;">
|
||||
<div style="text-align:center; margin: auto; font-size: 14px; font-color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
|
||||
<div style="text-align:center; margin: auto; font-size: 14px; color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
|
||||
<img src="${NEXT_PUBLIC_WEBAPP_URL}/logo_h.png" alt="Documenso Logo" style="width: 180px; display: block; margin: auto; margin-bottom: 14px;">
|
||||
${message}
|
||||
${content}
|
||||
|
||||
@ -2,3 +2,7 @@ export { signingRequestTemplate } from "./signingRequestTemplate";
|
||||
export { signingCompleteTemplate } from "./signingCompleteTemplate";
|
||||
export { sendSigningRequest as sendSigningRequest } from "./sendSigningRequest";
|
||||
export { sendSigningDoneMail } from "./sendSigningDoneMail";
|
||||
export { resetPasswordTemplate } from "./resetPasswordTemplate";
|
||||
export { sendResetPassword } from "./sendResetPassword";
|
||||
export { resetPasswordSuccessTemplate } from "./resetPasswordSuccessTemplate";
|
||||
export { sendResetPasswordSuccessMail } from "./sendResetPasswordSuccessMail";
|
||||
|
||||
51
packages/lib/mail/resetPasswordSuccessTemplate.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
export const resetPasswordSuccessTemplate = (user: User) => {
|
||||
return `
|
||||
<div style="background-color: #eaeaea; padding: 2%;">
|
||||
<div
|
||||
style="text-align:left; margin: auto; font-size: 14px; color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
|
||||
<img src="${NEXT_PUBLIC_WEBAPP_URL}/logo_h.png" alt="Documenso Logo"
|
||||
style="width: 180px; display: block; margin-bottom: 14px;" />
|
||||
|
||||
<h2 style="text-align: left; margin-top: 20px; font-size: 24px; font-weight: bold">Password updated!</h2>
|
||||
|
||||
<p style="margin-top: 15px">
|
||||
Hi ${user.name ? user.name : user.email},
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 15px">
|
||||
We've changed your password as you asked. You can now sign in with your new password.
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 15px">
|
||||
Didn't request a password change? We are here to help you secure your account, just <a href="https://documenso.com">contact us</a>.
|
||||
</p>
|
||||
|
||||
<p style="margin-top: 15px">
|
||||
<p style="font-weight: bold">
|
||||
The Documenso Team
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<p style="text-align:left; margin-top: 30px">
|
||||
<small>Want to send you own signing links?
|
||||
<a href="https://documenso.com">Hosted Documenso is here!</a>.</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: left; line-height: 18px; color: #666666; margin: 24px">
|
||||
<div style="margin-top: 12px">
|
||||
<b>Need help?</b>
|
||||
<br>
|
||||
Contact us at <a href="mailto:hi@documenso.com">hi@documenso.com</a>
|
||||
</div>
|
||||
<hr size="1" style="height: 1px; border: none; color: #D8D8D8; background-color: #D8D8D8">
|
||||
<div style="text-align: center">
|
||||
<small>Easy and beautiful document signing by Documenso.</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
export default resetPasswordSuccessTemplate;
|
||||
46
packages/lib/mail/resetPasswordTemplate.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||
|
||||
export const resetPasswordTemplate = (ctaLink: string, ctaLabel: string) => {
|
||||
const customContent = `
|
||||
<h2 style="margin-top: 36px; font-size: 24px; font-weight: bold;">Forgot your password?</h2>
|
||||
<p style="margin-top: 8px;">
|
||||
That's okay, it happens! Click the button below to reset your password.
|
||||
</p>
|
||||
|
||||
<p style="margin: 30px 0px; text-align: center">
|
||||
<a href="${ctaLink}" style="background-color: #37f095; white-space: nowrap; color: white; border-color: transparent; border-width: 1px; border-radius: 0.375rem; font-size: 18px; padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; text-decoration: none; margin-top: 4px; margin-bottom: 4px;">
|
||||
${ctaLabel}
|
||||
</a>
|
||||
</p>
|
||||
<p style="margin-top: 20px;">
|
||||
<small>Want to send you own signing links? <a href="https://documenso.com">Hosted Documenso is here!</a>.</small>
|
||||
</p>`;
|
||||
|
||||
const html = `
|
||||
<div style="background-color: #eaeaea; padding: 2%;">
|
||||
<div
|
||||
style="text-align:center; margin: auto; font-size: 14px; color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
|
||||
<img src="${NEXT_PUBLIC_WEBAPP_URL}/logo_h.png" alt="Documenso Logo"
|
||||
style="width: 180px; display: block; margin: auto; margin-bottom: 14px;" />
|
||||
${customContent}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const footer = `
|
||||
<div style="text-align: left; line-height: 18px; color: #666666; margin: 24px">
|
||||
<div style="margin-top: 12px">
|
||||
<b>Need help?</b>
|
||||
<br>
|
||||
Contact us at <a href="mailto:hi@documenso.com">hi@documenso.com</a>
|
||||
</div>
|
||||
<hr size="1" style="height: 1px; border: none; color: #D8D8D8; background-color: #D8D8D8">
|
||||
<div style="text-align: center">
|
||||
<small>Easy and beautiful document signing by Documenso.</small>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
return html + footer;
|
||||
};
|
||||
|
||||
export default resetPasswordTemplate;
|
||||
@ -1,4 +1,3 @@
|
||||
import { ReadStream } from "fs";
|
||||
import nodemailer from "nodemailer";
|
||||
import nodemailerSendgrid from "nodemailer-sendgrid";
|
||||
|
||||
|
||||
14
packages/lib/mail/sendResetPassword.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { resetPasswordTemplate } from "@documenso/lib/mail";
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||
import { sendMail } from "./sendMail";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
export const sendResetPassword = async (user: User, token: string) => {
|
||||
await sendMail(
|
||||
user.email,
|
||||
"Forgot password?",
|
||||
resetPasswordTemplate(`${NEXT_PUBLIC_WEBAPP_URL}/auth/reset/${token}`, "Reset Your Password")
|
||||
).catch((err) => {
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
11
packages/lib/mail/sendResetPasswordSuccessMail.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import resetPasswordSuccessTemplate from "./resetPasswordSuccessTemplate";
|
||||
import { sendMail } from "./sendMail";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
export const sendResetPasswordSuccessMail = async (user: User) => {
|
||||
await sendMail(user.email, "Password Reset Success!", resetPasswordSuccessTemplate(user)).catch(
|
||||
(err) => {
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -1,6 +1,5 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||
import { baseEmailTemplate } from "./baseTemplate";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
export const signingCompleteTemplate = (message: string) => {
|
||||
const customContent = `
|
||||
@ -17,7 +16,7 @@ export const signingCompleteTemplate = (message: string) => {
|
||||
A copy of the signed document has been attached to this email.
|
||||
</p>
|
||||
<p style="margin-top: 14px;">
|
||||
<small>Like Documenso? <a href="https://documenso.com">Hosted Documenso is coming soon™</a>.</small>
|
||||
<small>Like Documenso? <a href="https://documenso.com">Hosted Documenso is here!</a>.</small>
|
||||
</p>`;
|
||||
|
||||
const html = baseEmailTemplate(message, customContent);
|
||||
|
||||
@ -21,7 +21,7 @@ export const signingRequestTemplate = (
|
||||
<small>If you have questions about this document, you should ask ${user.name}.</small>
|
||||
<hr size="1" style="height:1px;border:none;color:#e0e0e0;background-color:#e0e0e0">
|
||||
<p style="margin-top: 14px;">
|
||||
<small>Want to send you own signing links? <a href="https://documenso.com">Hosted Documenso is coming soon™</a>.</small>
|
||||
<small>Want to send you own signing links? <a href="https://documenso.com">Hosted Documenso is here!</a>.</small>
|
||||
</p>`;
|
||||
|
||||
const html = baseEmailTemplate(message, customContent);
|
||||
@ -29,4 +29,4 @@ export const signingRequestTemplate = (
|
||||
return html;
|
||||
};
|
||||
|
||||
export default signingRequestTemplate;
|
||||
export default signingRequestTemplate;
|
||||
@ -4,6 +4,10 @@
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3"
|
||||
"@documenso/prisma": "*",
|
||||
"@prisma/client": "^4.8.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"micro": "^10.0.1",
|
||||
"stripe": "^12.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
24
packages/lib/process-env.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
DATABASE_URL: string;
|
||||
NEXT_PUBLIC_WEBAPP_URL: string;
|
||||
NEXTAUTH_SECRET: string;
|
||||
NEXTAUTH_URL: string;
|
||||
|
||||
SENDGRID_API_KEY?: string;
|
||||
SMTP_MAIL_HOST?: string;
|
||||
SMTP_MAIL_PORT?: string;
|
||||
SMTP_MAIL_USER?: string;
|
||||
SMTP_MAIL_PASSWORD?: string;
|
||||
|
||||
MAIL_FROM: string;
|
||||
|
||||
STRIPE_API_KEY?: string;
|
||||
STRIPE_WEBHOOK_SECRET?: string;
|
||||
STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID?: string;
|
||||
STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID?: string;
|
||||
|
||||
NEXT_PUBLIC_ALLOW_SIGNUP?: string;
|
||||
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS?: string;
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { HttpError } from "@documenso/lib/server";
|
||||
import { NotFoundError, PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
|
||||
export function getServerErrorFromUnknown(cause: unknown): HttpError {
|
||||
// Error was manually thrown and does not need to be parsed.
|
||||
@ -18,7 +18,7 @@ export function getServerErrorFromUnknown(cause: unknown): HttpError {
|
||||
return new HttpError({ statusCode: 400, message: cause.message, cause });
|
||||
}
|
||||
|
||||
if (cause instanceof NotFoundError) {
|
||||
if (cause instanceof PrismaClientKnownRequestError) {
|
||||
return new HttpError({ statusCode: 404, message: cause.message, cause });
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ export function getServerErrorFromUnknown(cause: unknown): HttpError {
|
||||
return new Error(cause, { cause });
|
||||
}
|
||||
|
||||
// Catch-All if none of the above triggered and something (even more) unexpected happend
|
||||
// Catch-All if none of the above triggered and something (even more) unexpected happened
|
||||
return new HttpError({
|
||||
statusCode: 500,
|
||||
message: `Unhandled error of type '${typeof cause}'. Please reach out for our customer support.`,
|
||||
|
||||
@ -1,23 +1,17 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next";
|
||||
import { NextRequest } from "next/server";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { User as PrismaUser } from "@prisma/client";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { signOut } from "next-auth/react";
|
||||
|
||||
export async function getUserFromToken(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
|
||||
res?: NextApiResponse // TODO: Remove this optional parameter
|
||||
): Promise<PrismaUser | null> {
|
||||
const token = await getToken({ req });
|
||||
const tokenEmail = token?.email?.toString();
|
||||
|
||||
if (!token) {
|
||||
if (res.status) res.status(401).send("No session token found for request.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!tokenEmail) {
|
||||
res.status(400).send("No email found in session token.");
|
||||
if (!token || !tokenEmail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -26,7 +20,6 @@ export async function getUserFromToken(
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
if (res && res.status) res.status(401).end();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
7
packages/lib/stripe/client.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
|
||||
export const stripe = new Stripe(process.env.STRIPE_API_KEY!, {
|
||||
apiVersion: "2022-11-15",
|
||||
typescript: true,
|
||||
});
|
||||
15
packages/lib/stripe/data/plans.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export const STRIPE_PLANS = [
|
||||
{
|
||||
name: "Community Plan",
|
||||
prices: {
|
||||
monthly: {
|
||||
price: 30,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID ?? "",
|
||||
},
|
||||
yearly: {
|
||||
price: 300,
|
||||
priceId: process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID ?? "",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
23
packages/lib/stripe/fetchers/checkout-session.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { CheckoutSessionRequest, CheckoutSessionResponse } from "../handlers/checkout-session"
|
||||
|
||||
export type FetchCheckoutSessionOptions = CheckoutSessionRequest['body']
|
||||
|
||||
export const fetchCheckoutSession = async ({
|
||||
id,
|
||||
priceId
|
||||
}: FetchCheckoutSessionOptions) => {
|
||||
const response = await fetch('/api/stripe/checkout-session', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
priceId
|
||||
})
|
||||
});
|
||||
|
||||
const json: CheckoutSessionResponse = await response.json();
|
||||
|
||||
return json;
|
||||
}
|
||||
14
packages/lib/stripe/fetchers/get-subscription.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { GetSubscriptionResponse } from "../handlers/get-subscription";
|
||||
|
||||
export const fetchSubscription = async () => {
|
||||
const response = await fetch("/api/stripe/subscription", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json: GetSubscriptionResponse = await response.json();
|
||||
|
||||
return json;
|
||||
};
|
||||
19
packages/lib/stripe/fetchers/portal-session.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { PortalSessionRequest, PortalSessionResponse } from "../handlers/portal-session";
|
||||
|
||||
export type FetchPortalSessionOptions = PortalSessionRequest["body"];
|
||||
|
||||
export const fetchPortalSession = async ({ id }: FetchPortalSessionOptions) => {
|
||||
const response = await fetch("/api/stripe/portal-session", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
}),
|
||||
});
|
||||
|
||||
const json: PortalSessionResponse = await response.json();
|
||||
|
||||
return json;
|
||||
};
|
||||
35
packages/lib/stripe/guards/subscriptions.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { GetServerSideProps, GetServerSidePropsContext, NextApiRequest } from "next";
|
||||
import { SubscriptionStatus } from "@prisma/client";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
export const isSubscriptionsEnabled = () => {
|
||||
return process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true";
|
||||
};
|
||||
|
||||
export const isSubscribedServer = async (
|
||||
req: NextApiRequest | GetServerSidePropsContext["req"]
|
||||
) => {
|
||||
const { default: prisma } = await import("@documenso/prisma");
|
||||
|
||||
if (!isSubscriptionsEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const token = await getToken({
|
||||
req,
|
||||
});
|
||||
|
||||
if (!token || !token.email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const subscription = await prisma.subscription.findFirst({
|
||||
where: {
|
||||
User: {
|
||||
email: token.email,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return subscription !== null && subscription.status !== SubscriptionStatus.INACTIVE;
|
||||
};
|
||||
92
packages/lib/stripe/handlers/checkout-session.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { stripe } from "../client";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
export type CheckoutSessionRequest = {
|
||||
body: {
|
||||
id?: string;
|
||||
priceId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type CheckoutSessionResponse =
|
||||
| {
|
||||
success: false;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
success: true;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export const checkoutSessionHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Subscriptions are not enabled",
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({
|
||||
success: false,
|
||||
message: "Method not allowed",
|
||||
});
|
||||
}
|
||||
|
||||
const token = await getToken({
|
||||
req,
|
||||
});
|
||||
|
||||
if (!token || !token.email) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized",
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: token.email,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "No user found",
|
||||
});
|
||||
}
|
||||
|
||||
const { id, priceId } = req.body;
|
||||
|
||||
if (typeof priceId !== "string") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "No id or priceId found in request",
|
||||
});
|
||||
}
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer: id,
|
||||
customer_email: user.email,
|
||||
client_reference_id: String(user.id),
|
||||
payment_method_types: ["card"],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
success_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing?success=true`,
|
||||
cancel_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing?canceled=true`,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
url: session.url,
|
||||
});
|
||||
};
|
||||
63
packages/lib/stripe/handlers/get-subscription.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import prisma from "@documenso/prisma";
|
||||
import { Subscription } from "@prisma/client";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
|
||||
export type GetSubscriptionRequest = never;
|
||||
|
||||
export type GetSubscriptionResponse =
|
||||
| {
|
||||
success: false;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
success: true;
|
||||
subscription: Subscription;
|
||||
};
|
||||
|
||||
export const getSubscriptionHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (!process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "Subscriptions are not enabled",
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return res.status(405).json({
|
||||
success: false,
|
||||
message: "Method not allowed",
|
||||
});
|
||||
}
|
||||
|
||||
const token = await getToken({
|
||||
req,
|
||||
});
|
||||
|
||||
if (!token || !token.email) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Unauthorized",
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = await prisma.subscription.findFirst({
|
||||
where: {
|
||||
User: {
|
||||
email: token.email,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!subscription) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "No subscription found",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
subscription,
|
||||
});
|
||||
};
|
||||