Compare commits

..

197 Commits

Author SHA1 Message Date
652951ed7f Fix prisma studio opening twice 2023-08-11 20:48:42 +00:00
40db5baa17 Add gitpod button to readme 2023-08-11 20:44:09 +00:00
4cdfa2d1f0 Add gitpod config files 2023-08-11 20:38:41 +00:00
ba82b1fca8 chore: update dockerfile 2023-08-10 13:51:22 +10:00
e5a80a701f Merge pull request #242 from documenso/docs/railway-one-click-deploy
docs: add railway one click deploy
2023-08-09 16:23:40 +10:00
f8aebbc484 Merge branch 'main' into docs/railway-one-click-deploy 2023-08-09 16:22:56 +10:00
4e60d4ac09 Merge pull request #247 from criadoperez/fix/criadoperez
Fixed typos
2023-08-04 12:36:49 +10:00
b5328eebde Corrected ts and tsx files 2023-08-04 02:14:04 +02:00
e87c57c97c Merge branch 'main' into docs/railway-one-click-deploy 2023-08-03 10:51:49 +10:00
cef5c8e33f Merge pull request #245 from documenso/fix-comunity-link
Update link to community (Discord)
2023-08-02 15:13:48 +02:00
b03dd1553f Update link to community (Discord) 2023-08-02 15:12:31 +02:00
f6d1b8c8a1 add hint that more deployment methods are coming 2023-08-01 14:12:33 +00:00
cb29ffef37 docs: add railway one click deploy 2023-08-01 13:47:50 +00:00
f7d0bb9823 Merge pull request #227 from documenso/docs-coventional-commits
docs: conventional commits
2023-07-31 13:00:55 +10:00
fc2809c4cf Merge branch 'main' into docs-coventional-commits 2023-07-28 11:42:48 +02:00
14536bda1e Merge pull request #234 from documenso/chore-manifest-filename
Rename manifest.md to MANIFEST.md
2023-07-21 14:35:39 +02:00
5fcd54ae09 Rename manifest.md to MANIFEST.md 2023-07-21 14:35:19 +02:00
35fecfffa9 Merge pull request #231 from documenso/chore/claim
chore: claim
2023-07-21 14:26:12 +02:00
da07bf135c chore: phrasing 2023-07-21 09:55:50 +02:00
1a06dd261c chore: claim 2023-07-21 09:47:05 +02:00
9c3fbbdb3a chore: dub slack slug 2023-07-19 07:54:03 +02:00
c20123ec75 merge main 2023-07-18 12:03:14 +02:00
4c25e85f01 doc: branchformat 2023-07-18 11:59:59 +02:00
ed3dbefbd7 Merge pull request #229 from fmerian/patch-1
fix: Update link to Slack in CONTRIBUTING.md
2023-07-15 12:19:04 +10:00
7d8f83750b Update link to Slack in CONTRIBUTING.md 2023-07-14 16:40:31 +02:00
fda3472e67 docs: conventional commits 2023-07-13 15:50:38 +02:00
2dd89b1bc1 Merge pull request #225 from documenso/feat/manifest
feat: added manifest
2023-07-13 12:08:02 +02:00
cc87eeb8e0 feat: added manifest 2023-07-13 11:10:21 +02:00
6c78332258 fix: await signing requests 2023-07-08 18:52:13 +10:00
5922725e1a Merge pull request #219 from PeerRich/patch-2
added new cal.com badge for booking link
2023-06-29 13:55:24 +02:00
36232c7817 added new cal.com badge for booking link 2023-06-28 19:53:39 +02:00
723339f812 fix: add smtp troubleshooting 2023-06-17 13:08:15 +10:00
f279b41b89 Merge pull request #172 from doug-andrade/logo
added dynamic coloring of logo mark
2023-06-17 11:56:36 +10:00
3d0836c39c Merge branch 'main' into logo 2023-06-17 11:55:54 +10:00
fcaa59699c Merge pull request #188 from Ashutosh-Bhadauriya/fix/selectbox-alignment
Fixed alignment of div containing selectboxes in mobile and tablet screens
2023-06-17 11:54:01 +10:00
5063b60a8a fix: further style updates 2023-06-17 11:53:04 +10:00
1322f7333f Merge branch 'main' into fix/selectbox-alignment 2023-06-17 11:48:11 +10:00
0278495896 Merge pull request #190 from dephraiim/fix-typo
Fix typo in contributing.md
2023-06-17 11:47:19 +10:00
ef34c4fe5d Merge pull request #179 from documenso/fix/improve-stripe-webhook-endpoint
fix: improve stripe webhook endpoint
2023-06-17 11:46:37 +10:00
6dad379943 Merge pull request #196 from documenso/feat/password-reset
feat: reset password
2023-06-17 11:45:38 +10:00
d3f82e1eb0 fix: code style updates and email wording changes 2023-06-17 11:44:34 +10:00
6838b953a8 Merge pull request #209 from documenso/fix/send-error-double-send
fix: don't double send error reponses when sending fails
2023-06-17 11:28:13 +10:00
fdd5a6114e Merge pull request #211 from danieltonel/doc-210-fix-docker-compose-name-attribute-error
Doc 210 fix docker compose name attribute error
2023-06-16 18:24:54 +10:00
b05ab9fbb4 set name using docker compose 2023-06-16 10:50:20 +03:00
e34be16d3d fix: Docker Compose 'name' attribute causing invalid file issue 2023-06-16 10:44:23 +03:00
9fd9bc2893 fix: reformat 2023-06-16 00:24:13 +10:00
3f14d8007a fix: don't double send error reponses when sending fails 2023-06-15 23:25:35 +10:00
e3bc41934c Fixes from code review 2023-06-09 03:55:30 +00:00
13a840ff78 Password validation with zod 2023-06-07 12:33:33 +00:00
fe6561f596 Set reset token expiry to 24 hours 2023-06-07 11:02:50 +00:00
9cfbb1dec9 Avoid leaking that a user has an account 2023-06-07 10:59:20 +00:00
9dd8c2842c Match emails with regex 2023-06-07 10:44:07 +00:00
54a965e2b4 Remove unused props from components 2023-06-07 10:37:47 +00:00
7cc1ae2de0 Refactor forgot password and reset component 2023-06-07 10:33:05 +00:00
5ec97657c1 Merge pull request #199 from documenso/fix/new-slack-link
fix: expired slack link
2023-06-07 20:15:13 +10:00
f08836216e Remove unused input fields 2023-06-07 10:12:05 +00:00
7184c47ac4 Rename component interfaces 2023-06-07 10:10:56 +00:00
02f9c38e1e Replace slack link with documen.so/slack 2023-06-07 09:59:40 +00:00
aa651fb4e0 Merge pull request #191 from eltociear/patch-1
Fix typo in pdf-editor.tsx
2023-06-06 19:01:31 +10:00
9ff8527336 fix: expired slack link 2023-06-05 21:49:39 +00:00
79bd410687 Remove tokens on successful password reset 2023-06-05 17:15:41 +00:00
3a0648c85d Expire token after 1 hour 2023-06-05 16:54:12 +00:00
2b9a2ff250 Avoid user from setting the same old password 2023-06-05 16:36:16 +00:00
4136811e32 Avoid consecutive password reset requests 2023-06-05 16:01:01 +00:00
e9cee23c15 Error handling for invalid users 2023-06-05 15:52:00 +00:00
5d2349086d Send email on password reset complete 2023-06-05 15:33:27 +00:00
c47e01b2b8 Display sucessful password reset request 2023-06-05 14:59:50 +00:00
7c30ee0c3e Redirect to /login on password reset 2023-06-05 14:47:10 +00:00
6e2b05f835 Change password in database to new reset password 2023-06-05 14:36:20 +00:00
8dc9c9d72d Add reset password page 2023-06-05 14:17:45 +00:00
66b529a841 feat: send reset password email 2023-06-05 13:44:47 +00:00
8293b50195 Create reset password token for user 2023-06-05 13:05:25 +00:00
002b22b1a8 Add forgot password page 2023-06-05 12:53:51 +00:00
447bf0cb76 Add password reset to prisma schema 2023-06-05 12:23:52 +00:00
4e65ff3a47 Merge pull request #195 from PeerRich/patch-1
chore: fix readme Product Hunt Badges
2023-06-05 21:47:39 +10:00
effe781ce7 chore: fix readme Product Hunt Badges
Product Hunt is over, its probably better to move it into its own section.

also added product of the day!
2023-06-05 12:33:08 +01:00
a1bb360b6f Fix typo in pdf-editor.tsx
postion -> position
2023-06-03 21:58:14 +09:00
37c4e68aac Fix typo in contributing.md 2023-06-02 20:01:10 +00:00
11c1b6841f Merge pull request #185 from ahiho/fix/ipv6
docs: update troubleshooting for IPv6
2023-06-03 00:44:04 +10:00
c41007e026 Revert "fix: support ipv6 for nextjs"
This reverts commit f9de6671e0aa29e25e872a80aa334d3319e3e522.
2023-06-02 18:04:52 +07:00
db99bf3674 Revert "fix: custom nextjs server"
This reverts commit 8f9a5f4ec7d834970a3e2b0778ce94218c997a8f.
2023-06-02 18:04:52 +07:00
3caa01ab53 Revert "fix: add custom nextjs server to docker"
This reverts commit 5dbe7b26286234db542921d9ded000c522c9a31e.
2023-06-02 18:04:52 +07:00
20b618c70f docs: update troubleshooting for IPv6 2023-06-02 18:04:52 +07:00
bbedd6d3de fix: add custom nextjs server to docker 2023-06-02 18:04:52 +07:00
054480500f fix: custom nextjs server 2023-06-02 18:04:52 +07:00
15b5f31a74 fix: support ipv6 for nextjs 2023-06-02 18:04:52 +07:00
a07febef46 Fix: alignment of div containing selecboxes in mobile and tab screens 2023-06-02 15:06:26 +05:30
316fb49339 fix: disable subscriptions in example env 2023-06-02 19:03:59 +10:00
fc1b3be5ad Merge pull request #184 from The-Robin-Hood/bugfix/docker_script_update
docker script updated 🐳
2023-06-02 18:26:21 +10:00
79c037216d Delete turbo-build.log 2023-06-01 22:13:45 -04:00
aa584c1495 Merge branch 'logo' of https://github.com/doug-andrade/documenso into logo 2023-06-01 22:09:39 -04:00
8b9738f6d5 fix: updated all logo component to set color 2023-06-01 22:05:15 -04:00
20b51198b4 docker script updated 🐳 2023-06-01 22:24:58 +05:30
f80edf3f94 Merge pull request #181 from ahiho/fix/docker-image-typo
typo: documentso >> documenso
2023-06-02 00:06:28 +10:00
08faabc813 Merge pull request #182 from JonasPardon/patch-1
Fix typos in example env
2023-06-02 00:04:50 +10:00
0a7ed0701c fix: add turbo entries for other platforms to package-lock
Package managers such as NPM behave strangely when adding
packages such as swc and turborepo which contain platform
variants.

During a first time install they will include only the current
devices platform while a clean node_modules and package-lock
will result in all platforms being included.

This change adds those missed platforms by performing the above step and porting it back to our existing package-lock.
2023-06-01 23:25:49 +10:00
488cf58f0e Fix typos in example env
Just noticed some typos while setting up a local copy and thought I'd fix them up real quick.
2023-06-01 10:04:26 +02:00
dd4568b7fa typo: documentso >> documenso 2023-06-01 13:58:18 +07:00
19e960f593 fix: improve stripe webhook endpoint
Improve the stripe webhook endpoint by checking for
subscriptions prior to performing an update to handle
cases where accounts have no created subscription.

This can happen in sitations such as when a checkout_session has been created but the payment fails.
2023-05-31 21:11:54 +10:00
893ab9bea5 Merge pull request #167 from dephraiim/fix/dashboard-logo
fix: dashboard logo
2023-05-30 23:29:57 +10:00
2aaeab3217 Merge pull request #176 from The-Robin-Hood/bugfix/docker_compose
Package.json Docker script fix
2023-05-30 22:50:57 +10:00
0a5de18235 minor script fix 2023-05-30 15:43:58 +05:30
b5e03359c1 fix: mark truncateTitle as optional 2023-05-30 20:11:23 +10:00
a266e4f9d4 Merge pull request #150 from The-Robin-Hood/bugfix/long_filename
Long filename fix 🗄
2023-05-30 20:04:19 +10:00
eccd9b5cd3 Merge branch 'main' into bugfix/long_filename 2023-05-30 20:03:19 +10:00
fdbcf33210 Merge pull request #164 from doug-andrade/next
Simplified next.config.js and removed next-transpile-modules dependency.
2023-05-30 20:02:17 +10:00
6048555e4a Merge branch 'main' into next 2023-05-30 19:55:31 +10:00
e33f31c483 Merge pull request #165 from doug-andrade/fix-typos
typo: /ressources to /resources
2023-05-30 19:54:32 +10:00
fe82e3c84f Merge pull request #171 from leerob/turbo
Add turborepo to monorepo.
2023-05-30 19:34:53 +10:00
7684a49b7d Merge branch 'main' into bugfix/long_filename 2023-05-30 14:30:55 +05:30
d8ad4b4b2b fix: add turbo dep and start command 2023-05-30 18:56:41 +10:00
e40ebd84d4 Merge pull request #174 from doug-andrade/fonts
loading fonts with next/font for no layout shift
2023-05-30 18:48:12 +10:00
a340b9c481 Merge pull request #173 from piyushkrmaurya/prisma_deprecations
fix: deprecated type checks and imports from @prisma/client
2023-05-30 18:35:32 +10:00
307b0cc9d9 fixed height on login/signup pages 2023-05-30 02:36:43 -04:00
3e94491474 fixed next/font load on ALL pages and toast. 2023-05-30 02:17:34 -04:00
007fe44db8 loading fonts with next/font 2023-05-29 22:25:36 -04:00
1e6f65f92d Explicit deps 2023-05-29 19:46:24 -05:00
82fbedf8e3 fix: deprecated type checks and imports from @prisma 2023-05-30 00:24:18 +05:30
37ded07a92 Merge branch 'main' into logo 2023-05-29 13:23:14 -04:00
df2294b43b dynamic coloring of logo mark 2023-05-29 13:17:40 -04:00
2f3be1cfe5 Add turborepo to monorepo. 2023-05-29 10:38:24 -05:00
8ecd5cf215 fix: respect truncate title prop 2023-05-29 18:47:54 +10:00
f5091dd4d7 Merge pull request #166 from doug-andrade/tailwind
update: add new brand color as palette in tailwind config
2023-05-29 18:40:09 +10:00
4c06b5e640 fix: logo only on dashboard 2023-05-29 08:01:12 +00:00
b477799d70 update: add new brand color as palette in tailwind config 2023-05-28 23:17:52 -04:00
b928993510 typo: /ressources >> /resources 2023-05-28 20:06:43 -04:00
ad4d844b4d remove: next-transpile-modules dependency 2023-05-28 19:53:44 -04:00
3444d7fd93 task: simplify next.config.js 2023-05-28 19:44:00 -04:00
3e220135be fix: readme format 2023-05-28 17:42:54 +02:00
095c391d45 Merge pull request #163 from documenso/ElTimuro-patch-1
We are LIVE on Product Hunt 🚨 Come say hi :)
2023-05-28 10:16:35 +02:00
b0e4fa9e1d We are LIVE on Product Hunt 🚨 Come say hi :)
We are LIVE on Product Hunt 🚨 Come say hi :) 
https://www.producthunt.com/posts/documenso
2023-05-28 10:16:22 +02:00
f6bff1649b fix: disable empty state add doc without subscr. 2023-05-28 06:38:55 +02:00
b2b499f397 chore: update signup link 2023-05-28 06:16:48 +02:00
eb18a7e11c feat: update password in dashboard 2023-05-28 13:11:09 +10:00
89d9e02464 fix: update logo 2023-05-28 13:10:09 +10:00
a83b09f4db fix: update favicon 2023-05-28 13:07:47 +10:00
e445830ffb fix: convert readFile to buffer 2023-05-28 08:04:16 +10:00
bfff81dd3c fix: apply encoding on buffer level 2023-05-28 07:43:02 +10:00
02129aab73 Merge pull request #162 from documenso/feature/update-branding
Feature/update branding
2023-05-27 18:23:22 +02:00
e7386928fa Merge branch 'main' into feature/update-branding 2023-05-27 18:22:13 +02:00
7890b4adf1 feat: update logo 2023-05-27 18:18:34 +02:00
6aa40b2547 feat: update logo 2023-05-27 18:18:21 +02:00
c142c1bd54 feat: favicon logo 2023-05-27 18:11:49 +02:00
980bd0d485 fix: convert readFile to buffer 2023-05-28 00:53:43 +10:00
17d51354d7 fix: support cert file encodings 2023-05-28 00:39:07 +10:00
0881abdee4 Merge pull request #159 from documenso/feat/support-custom-cert-paths
feat: support leading cert from custom path
2023-05-27 23:39:08 +10:00
a300c3fb3a Merge branch 'main' into feat/support-custom-cert-paths 2023-05-27 23:38:33 +10:00
5e07b8bd92 Merge pull request #161 from documenso/ElTimuro-patch-1
PRODUCT HUNT LAUNCH TOMORROW
2023-05-27 12:04:55 +02:00
7b1d626f9a PRODUCT HUNT LAUNCH TOMORROW
WE LAUNCH ON PRODUCT HUNT TOMORROW.
https://dub.sh/documenso-launch
2023-05-27 12:04:44 +02:00
de46d0f4ab fix: support passphrase env var 2023-05-27 01:31:48 +10:00
cc7ab171b1 Merge pull request #160 from documenso/ElTimuro-patch-1
Update README.md
2023-05-26 17:23:16 +02:00
466941dbc2 Update README.md 2023-05-26 17:22:56 +02:00
0564792604 fix: remove unused import 2023-05-27 01:08:33 +10:00
32f904ad68 feat: support leading cert from custom path 2023-05-27 01:07:07 +10:00
748f842115 fix: update prop typo, extract truncate method 2023-05-25 18:43:37 +10:00
ecaec356a1 Product Hunt Hint 2023-05-23 17:22:40 +02:00
38f730c730 added EE folder and EE license scaffold 2023-05-23 17:09:42 +02:00
2b4a9fbe21 Merge pull request #145 from documenso/feat/add-subscriptions
Add subscriptions
2023-05-22 16:43:10 +02:00
106ac40fb1 texts, monthly billing default 2023-05-21 21:01:34 +02:00
62ac181193 Merge branch 'main' into feat/add-subscriptions 2023-05-21 20:44:07 +02:00
9580100d66 Merge pull request #153 from documenso/doc-215-link-token
fix: pass recipient token to signed page
2023-05-21 20:37:39 +02:00
38a8279757 fix 2023-05-21 20:36:38 +02:00
ed77000746 fix: pass recipient token to signed page 2023-05-21 20:28:06 +02:00
73b72c6cce fix NEXT_PUBLIC_ALLOW_SIGNUP reference 2023-05-21 20:10:06 +02:00
b2aa4d6587 Merge branch 'main' into feat/add-subscriptions 2023-05-21 19:08:23 +02:00
bde80bf2c9 clean up debug 2023-05-21 18:52:35 +02:00
1e505088ad fix: hide billing if the feature flag hasn't been passed 2023-05-21 23:00:54 +10:00
ae0799168a clean up submodule 2023-05-19 20:02:39 +02:00
b5ec3cc817 Merge branch 'main' into feat/add-subscriptions 2023-05-19 19:47:23 +02:00
4a2162478e Merge branch 'main' into bugfix/long_filename 2023-05-19 19:33:22 +02:00
370f38457b Merge pull request #151 from abielzulio/abielzulio-fix-typo
fix(`typo`): `availible` → `available`
2023-05-19 18:58:55 +02:00
1a5948c50e Merge branch 'bugfix/long_filename' of https://github.com/The-Robin-Hood/documenso into bugfix/long_filename 2023-05-15 15:43:32 +05:30
8bf6594cf4 prop name changed 📛 2023-05-15 15:43:29 +05:30
b6ff01ef86 Merge branch 'main' into bugfix/long_filename 2023-05-15 15:40:04 +05:30
9b993c08f1 Truncate prop added 2023-05-15 15:39:37 +05:30
f34813e450 Update login.tsx 2023-05-14 20:13:45 +07:00
8f6c6dccf4 Merge pull request #148 from piyushkrmaurya/main
Add tooltip to recipients action buttons
2023-05-12 07:42:24 +10:00
826704c21f Adds animation to tooltip 2023-05-11 21:00:05 +05:30
4f47bbb552 Adds consistent 'className' to action buttons 2023-05-11 14:04:40 +05:30
825231fe2a Adds tooltip to recipient action buttons 2023-05-11 12:15:41 +05:30
051e681701 Long filename fix 🗄 2023-05-10 20:13:07 +05:30
012d2a9a09 Merge pull request #147 from dephraiim/readme-fixes
Readme Fixes
2023-05-09 17:28:27 +10:00
85c593d8e3 fix typo 2023-05-09 07:24:38 +00:00
0f28692a39 Update readme 2023-05-07 11:25:12 +00:00
22bc854cac feat: add warnings for subscription lapses and cancellations 2023-05-06 16:40:37 +10:00
d2c5657093 fix: update signup env var 2023-05-06 16:11:56 +10:00
6934e573d5 feat: add guards and subscription ui 2023-05-06 16:08:21 +10:00
7eaa00b836 feat: add stripe api handlers 2023-05-06 14:34:43 +10:00
e7e881be01 fix: update env types 2023-05-06 14:34:20 +10:00
e80997f462 fix: update env vars 2023-05-06 14:33:42 +10:00
da0166b746 fix: tidy stripe feature and add provider 2023-05-06 14:33:27 +10:00
900b816ae0 feat: stripe handlers and fetchers 2023-05-05 20:08:18 +10:00
ed3e4d22ef feat: scaffhold subscription table and ui 2023-05-05 19:29:42 +10:00
bf84ec8962 Merge pull request #74 from Mythie/chore/optimise-deps
chore: optimise depedency tree
2023-05-01 20:38:24 +10:00
1abfa93551 Merge branch 'main' into chore/optimise-deps 2023-05-01 20:36:37 +10:00
039cc75882 Merge pull request #139 from documenso/fix/improve-general-styling
chore: improve general styling
2023-05-01 20:05:17 +10:00
95c3be9a77 chore: optimise depedency tree 2023-04-19 23:30:14 +10:00
117 changed files with 5528 additions and 5431 deletions

View File

@ -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
View 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
View File

@ -36,3 +36,6 @@ yarn-error.log*
next-env.d.ts
.env
.env.example
# turborepo
.turbo

60
.gitpod.yml Normal file
View 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
View File

@ -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"
]
}

View File

@ -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
View 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
View File

@ -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&#0032;open&#0032;source&#0032;DocuSign&#0032;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&#0032;Open&#0032;Source&#0032;DocuSign&#0032;Alternative&#0046; | 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.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/documenso/documenso)
## Updating
- If you pull the newest version from main, using <code>git pull</code>, it may be necessary to regenerate your database client
- 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
[![Deploy on Railway](https://railway.app/button.svg)](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
- "::"
```

View File

@ -1,4 +0,0 @@
{
"presets": ["next/babel"],
"plugins": []
}

View File

@ -1,3 +1,8 @@
{
"extends": ["next/babel", "next/core-web-vitals"]
}
"extends": [
"next/core-web-vitals"
],
"rules": {
"react/no-unescaped-entities": "off"
}
}

View 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>
);
};

View 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>
)}
</>
)
}

View File

@ -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);
}

View File

@ -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})`

View File

@ -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;

View 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>
</>
);
}

View File

@ -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>

View File

@ -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>
)}

View File

@ -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>
</>
);
}

View File

@ -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

View 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>
</>
);
}

View File

@ -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">

View File

@ -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;

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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">

View File

@ -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>
);
}

View File

@ -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 />

View 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) }),
});

View 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) }),
});

View File

@ -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.",
});
}

View File

@ -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({

View File

@ -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);

View File

@ -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;

View File

@ -0,0 +1 @@
export { checkoutSessionHandler as default } from '@documenso/lib/stripe/handlers/checkout-session'

View File

@ -0,0 +1 @@
export { portalSessionHandler as default } from "@documenso/lib/stripe/handlers/portal-session";

View File

@ -0,0 +1 @@
export { getSubscriptionHandler as default } from '@documenso/lib/stripe/handlers/get-subscription'

View File

@ -0,0 +1,5 @@
export const config = {
api: { bodyParser: false },
};
export { webhookHandler as default } from "@documenso/lib/stripe/handlers/webhook";

View File

@ -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(() => {

View 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: {},
};
}

View 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>
);
}

View File

@ -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"

View File

@ -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();
}}>

View File

@ -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">

View File

@ -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>

View File

@ -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}`,
},
};
}

View File

@ -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>
</>

View 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: {},
};
}

View File

@ -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,
},
};
}

View File

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

View File

@ -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
View 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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
apps/web/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View 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"
}

View File

@ -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;
}

View File

@ -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",

View File

@ -21,6 +21,6 @@
"../../packages/types/next-auth.d.ts",
"**/*.ts",
"**/*.tsx"
],
, "../../packages/lib/process-env.d.ts" ],
"exclude": ["node_modules"]
}

View File

@ -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

View File

@ -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"

View File

@ -1,4 +1,3 @@
name: documenso
services:
database:
image: postgres:15

View File

@ -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:

Submodule documenso deleted from 8039871ab1

8150
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

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

View 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.

View File

@ -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
});
}
};

View File

@ -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;

View File

@ -0,0 +1 @@
export * from './strings';

View 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)}`;
};

View File

@ -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}

View File

@ -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";

View 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;

View 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;

View File

@ -1,4 +1,3 @@
import { ReadStream } from "fs";
import nodemailer from "nodemailer";
import nodemailerSendgrid from "nodemailer-sendgrid";

View 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;
});
};

View 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;
}
);
};

View File

@ -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);

View File

@ -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;

View File

@ -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
View 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;
}
}

View File

@ -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.`,

View File

@ -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;
}

View 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,
});

View 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 ?? "",
},
},
},
];

View 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;
}

View 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;
};

View 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;
};

View 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;
};

View 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,
});
};

View 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,
});
};

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