Compare commits

...

271 Commits

Author SHA1 Message Date
6b3696d879 docs: add render deploy button to readme 2023-08-10 01:53:05 +00:00
ca1522a31e Merge branch 'main' into feat/add-render-deploy 2023-08-10 01:52:25 +00:00
201c0ac22a chore: update start command for render deployment 2023-08-10 01:34:42 +00:00
c573e15ac2 Add render.yaml 2023-08-10 00:52:43 +00: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
8457823d8e fix: improve sign in and sign up pages 2023-05-01 20:01:36 +10:00
d135df827a fix: improve general styling
Improve the general styling of the app by removing floats and replacing it `flex`. Additionally, improve the constrast of certain parts of the app and add some transitions to hover changes.
2023-05-01 20:01:35 +10:00
d2301a923b Merge pull request #140 from documenso/feat/DOC-210-sign-dialog-broken-on-second-opening
fix: debounce display of signing canvas
2023-04-28 19:12:10 +02:00
108614bf46 Merge branch 'main' into feat/DOC-210-sign-dialog-broken-on-second-opening 2023-04-28 18:22:57 +02:00
adf69edd54 Merge pull request #141 from documenso/fix/DOC-214-date-field-appears-for-all-recipients
fix: date field appears for all recipients
2023-04-28 18:20:52 +02:00
82139f6b2d Merge branch 'main' into fix/DOC-214-date-field-appears-for-all-recipients 2023-04-25 11:51:23 +02:00
270c82759c Merge pull request #137 from zahid47/issue-131-redirect-to-dashboard-if-logged-in
Redirect to /dashboard if auth user tries to access /login or /signup
2023-04-25 11:15:11 +10:00
01c7903efa Merge pull request #142 from raysubham/fix/keep-url-state-in-sync
feat: Keep the URL query params and UI state in sync when status filter changes
2023-04-25 10:48:30 +10:00
64b755d5ba Merge branch 'main' into fix/keep-url-state-in-sync 2023-04-25 10:48:07 +10:00
8788b64585 Merge pull request #143 from mikeriss/fix-typo
Fix: typos on Readme
2023-04-25 10:41:27 +10:00
c9547057f6 fixed addional typos
typos fixed
2023-04-24 19:59:56 +02:00
17e688c222 typo
changed machnine to machine
2023-04-24 19:51:05 +02:00
f5a42e694d Updated README.md typo
changed a typo from signging to signing
2023-04-24 19:48:34 +02:00
b2d09216c8 rename function 2023-04-24 23:13:38 +05:30
6d30a486ab added type for statusFilter 2023-04-24 19:37:41 +05:30
dc6217b14e feat(Documents Filter): Keep the URL and UI state in sync when status filter changes 2023-04-24 19:16:56 +05:30
a6171ec4f3 Merge branch 'main' into fix/DOC-214-date-field-appears-for-all-recipients 2023-04-23 10:36:17 +10:00
d0f962598c Merge branch 'main' into feat/DOC-210-sign-dialog-broken-on-second-opening 2023-04-21 15:49:40 +02:00
81fd9ff749 fix: date field appears for all recipients
Updates the signing endpoint to only apply changes to the Date field for the current signer. This is made possible through the addition of the `signedAt` column within the database.

Resolves the issue with one signer filling the date for all recipients and also ensures that the date of signing on a document won't always be todays date after each recipient has signed.
2023-04-21 23:43:54 +10:00
4dcb0a684d fix: debounce display of signing canvas
Debounces the display of the signing canvas to avoid situtations where the canvas renders to 2px due to rendering while a transition is being performed.
2023-04-21 23:18:36 +10:00
ab96990d43 render PR env debug 2023-04-21 13:29:51 +02:00
ad5b2bcf82 fix: pr env condition 2023-04-21 12:59:53 +02:00
6f18be6b5b add render preview env support 2023-04-21 12:42:31 +02:00
8039871ab1 Merge pull request #130 from Mythie/fix/can-add-signature-space-for-empty-recipients
fix: disable selection for draft recipients
2023-04-20 17:26:01 +02:00
4b9840d7e0 Merge branch 'main' into fix/can-add-signature-space-for-empty-recipients 2023-04-20 17:25:39 +02:00
544a16caff Merge pull request #135 from Mythie/fix/signing-email-breaks-on-small-decices
fix: signing email breaks on small devices
2023-04-20 17:19:21 +02:00
989d036e54 Merge branch 'main' into fix/signing-email-breaks-on-small-decices 2023-04-20 17:14:00 +02:00
894f8720b8 Merge pull request #134 from SauravL3010/bugfix-#71/invalid-email-hint
Toast error for invalid email
2023-04-19 23:58:13 +10:00
70ea3ceaf3 fix: improve types 2023-04-19 23:56:39 +10:00
80d26adf9c add toast error for invalid email 2023-04-19 23:56:39 +10:00
b4e21f97e3 Merge pull request #133 from dephraiim/docker-container-name
Use `documenso` as container name for local development
2023-04-19 23:32:00 +10:00
95c3be9a77 chore: optimise depedency tree 2023-04-19 23:30:14 +10:00
52f554a636 Merge pull request #136 from dephraiim/doc-223
Remove Input Placeholders
2023-04-19 22:55:26 +10:00
849885b5b3 fix: redirect to /dashboard if auth user tries to access /login or /signup 2023-04-19 13:11:02 +06:00
bcc2530484 Declutter Textarea by removing placeholders 2023-04-16 23:45:57 +00:00
d863f89232 fix: signing email breaks on small devices
Currently the signing email displays poorly on small devices with the line wrapping
causing the button to look broken.

Resolve this by using whitespace no-wrap.
2023-04-17 07:01:41 +10:00
84e3d29589 Use documenso as container name for local development 2023-04-16 18:29:40 +00:00
ba3ffe68ea fix: disable selection for draft recipients 2023-04-16 23:02:50 +10:00
5c58b32d92 Merge branch 'main' of https://github.com/documenso/documenso 2023-04-15 20:35:36 +02:00
f10bafd998 cleanup 2023-04-15 20:35:33 +02:00
2cf8896e46 Merge branch 'main' of https://github.com/documenso/documenso 2023-04-15 20:33:38 +02:00
e873af3ec9 cleanup 2023-04-15 20:31:38 +02:00
06501bde60 cleanup 2023-04-15 20:31:24 +02:00
0dcab27e65 fix: openshift build does not allow private repos 2023-04-15 20:26:35 +02:00
ff2334ab55 fix: openshift build does not allow private repos
https://github.com/documenso/documenso/issues/79
2023-04-15 20:18:30 +02:00
63bd044723 feat: npm run d for ultra quick start 2023-04-15 20:04:28 +02:00
b111874d7c fix: redirect users sessions not found in databae 2023-04-15 19:54:04 +02:00
21149f82ba Merge pull request #61 from Mythie/feat/docker-environment
feat: add docker support and docker-compose quickstart
2023-04-15 19:44:33 +02:00
cb77a40fd9 fix: update postgres port 2023-04-13 23:43:42 +10:00
7aa7485388 fix: migrate dx.sh to package scripts 2023-04-13 22:52:54 +10:00
984084dd3b Merge branch 'main' into feat/docker-environment 2023-04-13 14:50:36 +02:00
421327432a added migration for doc-208 (allow document delete with sigantures) 2023-04-11 16:19:18 +02:00
134e366c27 Merge pull request #45 from SauravL3010/fix-#41-db-migration-Signature_recipientId_fkey
Fix-#41: Change referential action for Signature_recipientId_fkey
2023-04-11 15:50:22 +02:00
c79592cd0a Merge branch 'main' into fix-#41-db-migration-Signature_recipientId_fkey 2023-04-11 15:34:32 +02:00
f7cc44f138 Merge pull request #63 from dephraiim/doc-205
Disable the edit and add signer button for completed documents
2023-04-11 15:33:25 +02:00
60ff4fc992 Merge pull request #64 from dephraiim/doc-213
Send email notification to signers on document signing completion
2023-04-11 15:12:54 +02:00
e4e44b7f22 Replace fragment with null 2023-04-10 01:34:20 +00:00
6034e7a21e Send email notification to signers on document signing completion with signed document 2023-04-09 13:15:44 +00:00
2a34cc26c6 Replace empty string with fragments 2023-04-09 12:39:18 +00:00
6ea38efd9d chore: tidy script 2023-04-09 22:36:28 +10:00
0ce66a7957 Redirect breadcrump link on completed to avoid editing 2023-04-09 12:34:26 +00:00
49cb50ed6e feat: add down flag for stopping environment 2023-04-09 22:33:14 +10:00
065efabb39 Change wording on completed signers page 2023-04-09 12:29:31 +00:00
e86d4cc719 Disable the edit and add signer button for completed documents 2023-04-09 12:26:48 +00:00
5dd3713475 feat: add docker support and docker-compose quickstart
Add support for production container builds using the provided `Dockerfile` and `build.sh` script. This can later be used with actions to automatically publish to the provided docker registry.

Additionally, support an accelerated developer quickstart using `docker-compose`. Developers can now run the `dx` npm command to quickly spin up a database and mail server.
2023-04-08 23:20:42 +10:00
30c1c76dd7 Merge pull request #44 from SauravL3010/fix-recipient-selector
small fix for recipient-selector
2023-04-07 10:52:49 +02:00
22e191e98c Merge pull request #38 from Mythie/fix/improve-text-insertion-accuracy
fix: improve text insertion accuracy
2023-04-07 10:46:23 +02:00
5db54d3b8c Cange referential action for Signature_recipientId_fkey 2023-04-06 21:26:26 -04:00
593c317bf1 small fix for recipient-selector
ListBox options must be unique
2023-04-06 14:09:08 -04:00
ee4ca018d8 fix: improve text insertion accuracy
Previous inserted text would appear a little off center from where the user had selected which could cause some frustration.

We improve upon this by updating the code responsible for centering the text to behave in a more accurate manner. From what I can tell it looks to be quite solid but could do with more rigorous testing with shorter and longer inputs.

You can see the improved accuracy in action here:

https://www.loom.com/share/1095fee7605c4790b8b30f573a04f0f0
2023-04-06 23:34:53 +10:00
e3db462587 Merge pull request #34 from dephraiim/prettier-config
[new] Add `prettier`
2023-04-06 15:03:39 +02:00
739d29d753 Merge branch 'main' into prettier-config 2023-04-06 15:02:17 +02:00
964e749039 Update prettier styling 2023-04-04 22:10:30 +00:00
84b57d715c Apply prettier config to all files 2023-04-04 22:02:32 +00:00
85f2b5e84a Add prettier config 2023-04-04 22:00:01 +00:00
185 changed files with 8377 additions and 6708 deletions

38
.dockerignore Normal file
View File

@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
**/node_modules
**/.pnp
**.pnp.js
# testing
**/coverage
# next.js
**/.next/
**/out/
# production
**/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env
.env.example

View File

@ -1,8 +1,11 @@
# Database
# Option 1: You can use the provided remote test database, courtesy of the documenso team: postgres://documenso_test_user:GnmLG14u12sd9zHsd4vVWwP40WneFJMo@dpg-cf2hljh4reb5o45oqpq0-a.oregon-postgres.render.com/documenso_test_e2i3
# Option 2: Set up a local Postgres SQL instance (RECOMMENDED)
# ⚠ WARNING: The test database can be resetted or taken offline at any point.
# ⚠ WARNING: Please be aware that nothing written to the test databae is private.
# Option 3: Use the provided dx setup (RECOMMENDED)
# => postgres://documenso:password@127.0.0.1:54320/documenso
#
# ⚠ WARNING: The test database can be reset or taken offline at any point.
# ⚠ WARNING: Please be aware that nothing written to the test database is private.
DATABASE_URL=''
# URL
@ -13,6 +16,11 @@ NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
NEXTAUTH_SECRET='lorem ipsum sit dolor random string for encryption this could literally be anything'
NEXTAUTH_URL='http://localhost:3000'
# SIGNING
CERT_FILE_PATH=
CERT_PASSPHRASE=
CERT_FILE_ENCODING=
# MAIL (NODEMAILER)
# SENDGRID
# Get a Sendgrid Api key here: https://signup.sendgrid.com
@ -20,6 +28,12 @@ SENDGRID_API_KEY=''
# SMTP
# Set SMTP credentials to use SMTP instead of the Sendgrid API.
# If you're using the dx setup you can use the following values:
#
# SMTP_MAIL_HOST='127.0.0.1'
# SMTP_MAIL_PORT='2500'
# SMTP_MAIL_USER='documenso'
# SMTP_MAIL_PASSWORD='documenso'
SMTP_MAIL_HOST=''
SMTP_MAIL_PORT=''
SMTP_MAIL_USER=''
@ -28,6 +42,13 @@ SMTP_MAIL_PASSWORD=''
# Sender for signing requests and completion mails.
MAIL_FROM='documenso@localhost.com'
# STRIPE
STRIPE_API_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
#FEATURE FLAGS
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
ALLOW_SIGNUP=true
NEXT_PUBLIC_ALLOW_SIGNUP=true
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=false

3
.gitignore vendored
View File

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

3
.gitmodules vendored
View File

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

12
.vscode/settings.json vendored
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.

176
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,14 +74,18 @@ The current project goal is to <b>[release a production ready version](https://g
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
## Tools
## Contact us
Contact us if you are interested in our Enterprise plan for large organizations that need extra flexibility and control.
<a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
# Tech
Documenso is built using awesome open source tech including:
- [Typescript](https://www.typescriptlang.org/)
- [Javascript (when neccessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [Javascript (when necessary)](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
- [NextJS (JS Fullstack Framework)](https://nextjs.org/)
- [Postgres SQL (Database)](https://www.postgresql.org/)
- [Prisma (ORM - Object-relational mapping)](https://www.prisma.io/)
@ -86,7 +93,7 @@ Documenso is built using awesome open source tech including:
- [Node SignPDF (Digital Signature)](https://github.com/vbuch/node-signpdf)
- [React-PDF for viewing PDFs](https://github.com/wojtekmaj/react-pdf)
- [PDF-Lib for PDF manipulation](https://github.com/Hopding/pdf-lib)
- Check out /packages.json and /apps/web/package.json for more
- Check out `/package.json` and `/apps/web/package.json` for more
- Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned.
# Getting Started
@ -96,63 +103,150 @@ Documenso is built using awesome open source tech including:
To run Documenso locally you need
- [Node.js (Version: >=18.x)](https://nodejs.org/en/download/)
- Node Package Manger NPM - included in Node.js
- Node Package Manager NPM - included in Node.js
- [PostgreSQL (local or remote)](https://www.postgresql.org/download/)
## Developer Quickstart
> **Note**: This is a quickstart for developers. It assumes that you have both [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/) installed on your machine.
Want to get up and running quickly? Follow these steps:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh
git clone https://github.com/documenso/documenso
```
- Set up your `.env` file using the recommendations in the `.env.example` file.
- Run `npm run dx` in the root directory
- This will spin up a postgres database and inbucket mail server in docker containers.
- Run `npm run dev` in the root directory
- Want it even faster? Just use
```sh
npm run d
```
That's it! You should now be able to access the app at http://localhost:3000
Incoming mail will be available at http://localhost:9000
Your database will also be available on port `54320`. You can connect to it using your favorite database client.
## Developer Setup
Follow these steps to setup documenso on you local machnine:
Follow these steps to setup documenso on you local machine:
- [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device.
```sh
git clone https://github.com/documenso/documenso
```
- Run <code>npm i</code> in root directory
- Rename <code>.env.example</code> to <code>.env</code>
- Run `npm i` in root directory
- Rename `.env.example` to `.env`
- Set DATABASE_URL value in .env file
- You can use the provided test database url (may be wiped at any point)
- Or setup a local postgres sql instance (recommened)
- Create the database scheme by running <code>db-migrate:dev</code>
- Or setup a local postgres sql instance (recommended)
- Create the database scheme by running `db-migrate:dev`
- Setup your mail provider
- Set <code>SENDGRID_API_KEY</code> value in .env file
- Set `SENDGRID_API_KEY` value in .env file
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the <code>SMTP\_\* varibles</code> in your .env
- Run <code>npm run dev</code> root directory to start
- Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the `SMTP
\_
* variables` in your .env
- Run `npm run dev` root directory to start
- Register a new user at http://localhost:3000/signup
---
- Optional: Seed the database using <code>npm run db-seed</code> to create a test user and document
- Optional: Upload and sign <code>apps\web\ressources\example.pdf</code> manually to test your setup
- Optional: Seed the database using `npm run db-seed` to create a test user and document
- Optional: Upload and sign `apps/web/resources/example.pdf` manually to test your setup
- Optional: Create your own signing certificate
- A demo certificate is provided in /app/web/ressources/certificate.p12
- To generate you own using these steps and a linux Terminal or Windows Linux Subsystem see **Create your own signging certificate**.
- A demo certificate is provided in `/app/web/resources/certificate.p12`
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](#creating-your-own-signing-certificate)**.
## Updating
- If you pull the newest version from main, using <code>git pull</code>, it may be neccessary to regenerate your database client
- You can do this by running the generate command in /packages/prisma:
- If you pull the newest version from main, using `git pull`, it may be necessary to regenerate your database client
- You can do this by running the generate command in `/packages/prisma`:
```sh
npx prisma generate
```
- This is not neccessary on first clone
- This is not necessary on first clone.
# Creating your own signging certificate
# Creating your own signing certificate
For the digital signature of you documents you need a signign certificate in .p12 formate (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:\
<code>openssl genrsa -out private.key 2048</code>
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:
`openssl genrsa -out private.key 2048`
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
`openssl req -new -x509 -key private.key -out certificate.crt -days 365`
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:\
<code>openssl req -new -x509 -key private.key -out certificate.crt -days 365</code> \
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this: \
<code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code>
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this:
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
5. Place the certificate <code>/apps/web/ressource/certificate.p12</code>
5. Place the certificate `/apps/web/resources/certificate.p12`
# Deploying - Coming Soon™
# Docker
- Docker support
- One-Click-Deploy on Render.com Deploy
> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
Want to create a production ready docker image? Follow these steps:
- Run `./docker/build.sh` in the root directory.
- Publish the image to your docker registry of choice.
# Deployment
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
## Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/DjrRRX)
## Render
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso)
# Troubleshooting
## I'm not receiving any emails when using the developer quickstart
When using the developer quickstart an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing email locally for you to view.
The Web UI can be found at http://localhost:9000 while the SMTP port will be on localhost:2500.
## Support IPv6
In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the NextJS start command
For local docker run
```bash
docker run -it documenso:latest npm run start -- -H ::
```
For k8s or docker-compose
```yaml
containers:
- name: documenso
image: documenso:latest
imagePullPolicy: IfNotPresent
command:
- npm
args:
- run
- start
- --
- -H
- "::"
```

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

@ -1,8 +1,9 @@
import React, { useState } from "react";
import Draggable from "react-draggable";
import Logo from "../logo";
import { IconButton } from "@documenso/ui";
import Logo from "../logo";
import { XCircleIcon } from "@heroicons/react/20/solid";
import Draggable from "react-draggable";
const stc = require("string-to-color");
type FieldPropsType = {
@ -51,21 +52,19 @@ export default function EditableField(props: FieldPropsType) {
onMouseDown={(e: any) => {
e.preventDefault();
e.stopPropagation();
}}
>
}}>
{/* width: 192 height 96 */}
<div
hidden={props.hidden}
ref={nodeRef}
className="cursor-move opacity-80 p-2 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none"
className="absolute top-0 left-0 m-auto h-16 w-48 cursor-move select-none flex-row-reverse p-2 text-center text-lg font-bold opacity-80"
style={{
background: stc(props.field.Recipient.email),
}}
>
<div className="m-auto overflow-hidden flex-row-reverse text-lg font-bold text-center">
}}>
<div className="m-auto flex-row-reverse overflow-hidden text-center text-lg font-bold">
{field.type}
{field.type === "SIGNATURE" ? (
<div className="text-xs text-center">
<div className="text-center text-xs">
{`${props.field.Recipient?.name} <${props.field.Recipient?.email}>`}
</div>
) : (
@ -79,8 +78,7 @@ export default function EditableField(props: FieldPropsType) {
icon={XCircleIcon}
onClick={(event: any) => {
props.onDelete(props.field.id);
}}
></IconButton>
}}></IconButton>
</strong>
</div>
</Draggable>

View File

@ -1,7 +1,8 @@
import { useEffect, useState } from "react";
import { RadioGroup } from "@headlessui/react";
import { classNames } from "@documenso/lib";
import { RadioGroup } from "@headlessui/react";
import { FieldType } from "@prisma/client";
const stc = require("string-to-color");
export default function FieldTypeSelector(props: any) {
@ -24,8 +25,7 @@ export default function FieldTypeSelector(props: any) {
value={selectedFieldType}
onChange={(e: any) => {
setSelectedFieldType(e);
}}
>
}}>
<div className="space-y-4">
{fieldTypes.map((fieldType) => (
<RadioGroup.Option
@ -37,30 +37,23 @@ export default function FieldTypeSelector(props: any) {
className={({ checked, active }) =>
classNames(
checked ? "border-neon border-2" : "border-transparent",
"hover:bg-slate-100 select-none relative block cursor-pointer rounded-lg border bg-white px-3 py-2 focus:outline-none sm:flex sm:justify-between"
"relative block cursor-pointer select-none rounded-lg border bg-white px-3 py-2 hover:bg-slate-100 focus:outline-none sm:flex sm:justify-between"
)
}
>
}>
{({ active, checked }) => (
<>
<span className="flex items-center">
<span className="flex flex-col text-sm">
<RadioGroup.Label
as="span"
className="font-medium text-gray-900"
>
<RadioGroup.Label as="span" className="font-medium text-gray-900">
<span
className="inline-block h-4 w-4 flex-shrink-0 rounded-full mr-3 align-middle"
className="mr-3 inline-block h-4 w-4 flex-shrink-0 rounded-full align-middle"
style={{
background: stc(props.selectedRecipient?.email),
}}
/>
<span className="align-middle">
{" "}
{
fieldTypes.filter((e) => e.id === fieldType.id)[0]
.name
}
{fieldTypes.filter((e) => e.id === fieldType.id)[0].name}
</span>
</RadioGroup.Label>
</span>

View File

@ -1,13 +1,14 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import { useState } from "react";
import { createOrUpdateField, deleteField } from "@documenso/lib/api";
import { createField } from "@documenso/features/editor";
import RecipientSelector from "./recipient-selector";
import FieldTypeSelector from "./field-type-selector";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useRouter } from "next/router";
import { createField } from "@documenso/features/editor";
import { createOrUpdateField, deleteField } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import FieldTypeSelector from "./field-type-selector";
import RecipientSelector from "./recipient-selector";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
const stc = require("string-to-color");
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
@ -20,8 +21,7 @@ export default function PDFEditor(props: any) {
const [selectedRecipient, setSelectedRecipient]: any = useState();
const [selectedFieldType, setSelectedFieldType] = useState();
const noRecipients =
props?.document.Recipient.length === 0 ||
props?.document.Recipient.every((e: any) => !e.email);
props?.document.Recipient.length === 0 || props?.document.Recipient.every((e: any) => !e.email);
function onPositionChangedHandler(position: any, id: any) {
if (!position) return;
@ -30,7 +30,7 @@ export default function PDFEditor(props: any) {
movedField.positionY = position.y.toFixed(0);
createOrUpdateField(props.document, movedField);
// no instant redraw neccessary, postion information for saving or later rerender is enough
// no instant redraw necessary, position information for saving or later rerender is enough
// setFields(newFields);
}
@ -53,26 +53,16 @@ export default function PDFEditor(props: any) {
<div hidden={!noRecipients} className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-yellow-400"
aria-hidden="true"
/>
<InformationCircleIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-yellow-700">
This document does not have any recipients. Add recipients to
create fields.
This document does not have any recipients. Add recipients to create fields.
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<Link
href={
NEXT_PUBLIC_WEBAPP_URL +
"/documents/" +
props.document.id +
"/recipients"
}
className="whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600"
>
href={NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients"}
className="whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600">
Add Recipients
<span aria-hidden="true"> &rarr;</span>
</Link>
@ -98,12 +88,10 @@ export default function PDFEditor(props: any) {
}}
onMouseDown={(e: any, page: number) => {
if (e.button === 0) addField(e, page);
}}
></PDFViewer>
}}></PDFViewer>
<div
hidden={noRecipients}
className="fixed left-0 top-1/3 max-w-xs border border-slate-300 bg-white py-4 pr-5 rounded-md"
>
className="fixed left-0 top-1/3 max-w-xs rounded-md border border-slate-300 bg-white py-4 pr-5">
<RecipientSelector
recipients={props?.document?.Recipient}
onChange={setSelectedRecipient}
@ -123,12 +111,7 @@ export default function PDFEditor(props: any) {
if (!selectedFieldType) return;
if (noRecipients) return;
const signatureField = createField(
e,
page,
selectedRecipient,
selectedFieldType
);
const signatureField = createField(e, page, selectedRecipient, selectedFieldType);
createOrUpdateField(props?.document, signatureField).then((res) => {
setFields((prevState) => [...prevState, res]);

View File

@ -1,21 +1,14 @@
import Logo from "../logo";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import SignatureDialog from "./signature-dialog";
import { useEffect, useState } from "react";
import { Button } from "@documenso/ui";
import {
CheckBadgeIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import { FieldType } from "@prisma/client";
import {
createOrUpdateField,
deleteField,
signDocument,
} from "@documenso/lib/api";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { createField } from "@documenso/features/editor";
import { createOrUpdateField, deleteField, signDocument } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import Logo from "../logo";
import SignatureDialog from "./signature-dialog";
import { CheckBadgeIcon, InformationCircleIcon } from "@heroicons/react/24/outline";
import { FieldType } from "@prisma/client";
const PDFViewer = dynamic(() => import("./pdf-viewer"), {
ssr: false,
@ -27,9 +20,7 @@ export default function PDFSigner(props: any) {
const [signingDone, setSigningDone] = useState(false);
const [localSignatures, setLocalSignatures] = useState<any[]>([]);
const [fields, setFields] = useState<any[]>(props.fields);
const signatureFields = fields.filter(
(field) => field.type === FieldType.SIGNATURE
);
const signatureFields = fields.filter((field) => field.type === FieldType.SIGNATURE);
const [dialogField, setDialogField] = useState<any>();
useEffect(() => {
@ -79,34 +70,32 @@ export default function PDFSigner(props: any) {
<SignatureDialog open={open} setOpen={setOpen} onClose={onDialogClose} />
<div className="bg-neon p-4">
<div className="flex">
<div className="flex-shrink-0">
<Logo className="h-12 w-12 -mt-2.5"></Logo>
<div className="flex-shrink-0 flex gap-x-2 items-center">
<Logo className="h-8 w-8 text-black" />
<h2 className="text-2xl font-semibold">Documenso</h2>
</div>
<div className="ml-3 flex-1 md:flex md:justify-between text-center justify-start items-center">
<div className="mx-3 flex-1 items-center justify-start text-center md:flex md:justify-between">
<p className="text-lg text-slate-700">
{props.document.User.name
? `${props.document.User.name} (${props.document.User.email})`
: props.document.User.email}{" "}
would like you to sign this document.
</p>
<p className="mt-3 text-sm md:mt-0 md:ml-6">
<p className="mt-3 text-sm md:mt-0 md:ml-6 text-right md:text-inherit">
<Button
disabled={!signingDone}
color="secondary"
icon={CheckBadgeIcon}
className="float-right"
onClick={() => {
signDocument(
props.document,
localSignatures,
`${router.query.token}`
).then(() => {
router.push(
`/documents/${props.document.id}/signed?token=${router.query.token}`
);
});
}}
>
signDocument(props.document, localSignatures, `${router.query.token}`).then(
() => {
router.push(
`/documents/${props.document.id}/signed?token=${router.query.token}`
);
}
);
}}>
Done
</Button>
</p>
@ -117,15 +106,11 @@ export default function PDFSigner(props: any) {
<div className="bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon
className="h-5 w-5 text-yellow-400"
aria-hidden="true"
/>
<InformationCircleIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm text-yellow-700">
You can sign this document anywhere you like, but maybe look for
a signature line.
You can sign this document anywhere you like, but maybe look for a signature line.
</p>
</div>
</div>
@ -144,12 +129,10 @@ export default function PDFSigner(props: any) {
pdfUrl={`${NEXT_PUBLIC_WEBAPP_URL}/api/documents/${router.query.id}?token=${router.query.token}`}
onClick={onClick}
onMouseDown={function onMouseDown(e: any, page: number) {
if (signatureFields.length === 0)
addFreeSignature(e, page, props.recipient);
if (signatureFields.length === 0) addFreeSignature(e, page, props.recipient);
}}
onMouseUp={() => {}}
onDelete={onDeleteHandler}
></PDFViewer>
onDelete={onDeleteHandler}></PDFViewer>
</>
);
@ -166,18 +149,9 @@ export default function PDFSigner(props: any) {
}
function addFreeSignature(e: any, page: number, recipient: any): any {
const freeSignatureField = createField(
e,
page,
recipient,
FieldType.FREE_SIGNATURE
);
const freeSignatureField = createField(e, page, recipient, FieldType.FREE_SIGNATURE);
createOrUpdateField(
props.document,
freeSignatureField,
recipient.token
).then((res) => {
createOrUpdateField(props.document, freeSignatureField, recipient.token).then((res) => {
setFields((prevState) => [...prevState, res]);
setDialogField(res);
setOpen(true);

View File

@ -1,9 +1,9 @@
import { Fragment, useState } from "react";
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
import EditableField from "./editable-field";
import SignableField from "./signable-field";
import short from "short-uuid";
import { FieldType } from "@prisma/client";
import { Document, Page } from "react-pdf/dist/esm/entry.webpack5";
import short from "short-uuid";
export default function PDFViewer(props) {
const [numPages, setNumPages] = useState(null);
@ -33,16 +33,14 @@ export default function PDFViewer(props) {
<div
hidden={loading}
onMouseUp={props.onMouseUp}
style={{ height: numPages * pageHeight + 1000 }}
>
<div className="max-w-xs mt-6"></div>
style={{ height: numPages * pageHeight + 1000 }}>
<div className="mt-6 max-w-xs"></div>
<Document
file={props.pdfUrl}
onLoadSuccess={onDocumentLoadSuccess}
options={options}
renderMode="canvas"
className="absolute w-auto mx-auto left-0 right-0"
>
className="absolute left-0 right-0 mx-auto w-auto">
{Array.from({ length: numPages }, (_, index) => (
<Fragment key={short.generate().toString()}>
<div
@ -57,8 +55,7 @@ export default function PDFViewer(props) {
position: "relative",
...props.style,
}}
className="mx-auto w-fit"
>
className="mx-auto w-fit">
<Page
className="mt-5"
key={`page_${index + 1}`}
@ -69,8 +66,7 @@ export default function PDFViewer(props) {
if (e.height) setPageHeight(e.height);
setLoading(false);
}}
onRenderError={() => setLoading(false)}
></Page>
onRenderError={() => setLoading(false)}></Page>
{props?.fields
.filter((field) => field.page === index)
.map((field) =>
@ -80,8 +76,7 @@ export default function PDFViewer(props) {
key={field.id}
field={field}
className="absolute"
onDelete={onDeleteHandler}
></SignableField>
onDelete={onDeleteHandler}></SignableField>
) : (
<EditableField
hidden={
@ -93,8 +88,7 @@ export default function PDFViewer(props) {
field={field}
className="absolute"
onPositionChanged={onPositionChangedHandler}
onDelete={onDeleteHandler}
></EditableField>
onDelete={onDeleteHandler}></EditableField>
)
)}
</div>

View File

@ -1,13 +1,12 @@
import { Fragment, useEffect, useState } from "react";
import { classNames } from "@documenso/lib";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline";
import { classNames } from "@documenso/lib";
const stc = require("string-to-color");
export default function RecipientSelector(props: any) {
const [selectedRecipient, setSelectedRecipient]: any = useState(
props?.recipients[0]
);
const [selectedRecipient, setSelectedRecipient]: any = useState(props?.recipients[0]);
useEffect(() => {
props.onChange(selectedRecipient);
@ -18,11 +17,10 @@ export default function RecipientSelector(props: any) {
value={selectedRecipient}
onChange={(e: any) => {
setSelectedRecipient(e);
}}
>
}}>
{({ open }) => (
<div className="relative mt-1 mb-2">
<Listbox.Button className="select-none relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:border-neon focus:outline-none focus:ring-1 focus:ring-neon sm:text-sm">
<Listbox.Button className="focus:border-neon focus:ring-neon relative w-full cursor-default select-none rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-1 sm:text-sm">
<span className="flex items-center">
<span
className="inline-block h-4 w-4 flex-shrink-0 rounded-full"
@ -33,10 +31,7 @@ export default function RecipientSelector(props: any) {
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>
@ -45,20 +40,19 @@ export default function RecipientSelector(props: any) {
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
leaveTo="opacity-0">
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{props?.recipients.map((recipient: any) => (
<Listbox.Option
key={recipient?.id}
disabled={!recipient?.email}
className={({ active }) =>
classNames(
active ? "text-white bg-neon-dark" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9"
active ? "bg-neon-dark text-white" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9 aria-disabled:opacity-50 aria-disabled:cursor-not-allowed"
)
}
value={recipient}
>
value={recipient}>
{({ selected, active }) => (
<>
<div className="flex items-center">
@ -72,9 +66,8 @@ export default function RecipientSelector(props: any) {
className={classNames(
selected ? "font-semibold" : "font-normal",
"ml-3 block truncate"
)}
>
{`${selectedRecipient?.name} <${selectedRecipient?.email}>`}
)}>
{`${recipient?.name} <${recipient?.email || 'unknown'}>`}
</span>
</div>
@ -83,9 +76,8 @@ export default function RecipientSelector(props: any) {
className={classNames(
active ? "text-white" : "text-neon-dark",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
)}>
<CheckIcon className="h-5 w-5" strokeWidth={3} aria-hidden="true" />
</span>
) : null}
</>

View File

@ -1,8 +1,9 @@
import React, { useState } from "react";
import Draggable from "react-draggable";
import { classNames } from "@documenso/lib";
import { IconButton } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/20/solid";
import { classNames } from "@documenso/lib";
import Draggable from "react-draggable";
const stc = require("string-to-color");
type FieldPropsType = {
@ -37,31 +38,26 @@ export default function SignableField(props: FieldPropsType) {
onMouseDown={(e: any) => {
// e.preventDefault();
e.stopPropagation();
}}
>
}}>
<div
onClick={(e: any) => {
if (!field?.signature) props.onClick(props.field);
}}
ref={nodeRef}
className={classNames(
"opacity-80 m-auto w-48 h-16 flex-row-reverse text-lg font-bold text-center absolute top-0 left-0 select-none",
field.type === "SIGNATURE"
? "cursor-pointer hover:brightness-50"
: "cursor-not-allowed"
"absolute top-0 left-0 m-auto h-16 w-48 select-none flex-row-reverse text-center text-lg font-bold opacity-80",
field.type === "SIGNATURE" ? "cursor-pointer hover:brightness-50" : "cursor-not-allowed"
)}
style={{
background: stc(props.field.Recipient.email),
}}
>
<div hidden={field?.signature} className="font-medium my-4">
}}>
<div hidden={field?.signature} className="my-4 font-medium">
{field.type === "SIGNATURE" ? "SIGN HERE" : ""}
{field.type === "DATE" ? <small>Date (filled on sign)</small> : ""}
</div>
<div
hidden={!field?.signature}
className="font-qwigley text-5xl m-auto w-auto flex-row-reverse font-medium text-center"
>
className="font-qwigley m-auto w-auto flex-row-reverse text-center text-5xl font-medium">
{field?.signature?.type === "type" ? (
<div className="my-4">{field?.signature.typedSignature}</div>
) : (
@ -69,7 +65,7 @@ export default function SignableField(props: FieldPropsType) {
)}
{field?.signature?.type === "draw" ? (
<img className="w-48 h-16" src={field?.signature?.signatureImage} />
<img className="h-16 w-48" src={field?.signature?.signatureImage} />
) : (
""
)}

View File

@ -1,14 +1,11 @@
import { Fragment, useEffect, useState } from "react";
import { classNames } from "@documenso/lib";
import { localStorage } from "@documenso/lib";
import { Button, IconButton } from "@documenso/ui";
import { Dialog, Transition } from "@headlessui/react";
import {
LanguageIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { Fragment, useEffect, useState } from "react";
import { LanguageIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
import SignatureCanvas from "react-signature-canvas";
import { localStorage } from "@documenso/lib";
import { useDebouncedValue } from "../../hooks/use-debounced-value";
const tabs = [
{ name: "Type", icon: LanguageIcon, current: true },
@ -19,6 +16,9 @@ export default function SignatureDialog(props: any) {
const [currentTab, setCurrentTab] = useState(tabs[0]);
const [typedSignature, setTypedSignature] = useState("");
const [signatureEmpty, setSignatureEmpty] = useState(true);
// This is a workaround to prevent the canvas from being rendered when the dialog is closed
// we also need the debounce to avoid rendering while transitions are occurring.
const showCanvas = useDebouncedValue<boolean>(props.open, 1);
let signCanvasRef: any | undefined;
useEffect(() => {
@ -34,8 +34,7 @@ export default function SignatureDialog(props: any) {
onClose={() => {
props.setOpen(false);
setCurrent(tabs[0]);
}}
>
}}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@ -43,8 +42,7 @@ export default function SignatureDialog(props: any) {
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
leaveTo="opacity-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
@ -57,11 +55,10 @@ export default function SignatureDialog(props: any) {
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="min-h-[350px] relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<Dialog.Panel className="relative min-h-[350px] transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<div className="">
<div className="border-b border-gray-200 mb-3">
<div className="mb-3 border-b border-gray-200">
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<a
@ -72,11 +69,10 @@ export default function SignatureDialog(props: any) {
className={classNames(
tab.current
? "border-neon text-neon"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
"group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm cursor-pointer"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"group inline-flex cursor-pointer items-center border-b-2 py-4 px-1 text-sm font-medium"
)}
aria-current={tab.current ? "page" : undefined}
>
aria-current={tab.current ? "page" : undefined}>
<tab.icon
className={classNames(
tab.current
@ -93,7 +89,7 @@ export default function SignatureDialog(props: any) {
</div>
{isCurrentTab("Type") ? (
<div>
<div className="my-8 border-b border-gray-300 mb-3">
<div className="my-7 mb-3 border-b border-gray-300">
<input
value={typedSignature}
onChange={(e) => {
@ -101,36 +97,31 @@ export default function SignatureDialog(props: any) {
}}
className={classNames(
typedSignature ? "font-qwigley text-4xl" : "",
"leading-none h-10 align-bottom mt-14 text-center block w-full focus:border-neon focus:ring-neon text-2xl"
"focus:border-neon focus:ring-neon mt-14 block h-10 w-full text-center align-bottom text-2xl leading-none"
)}
placeholder="Kindly type your name"
/>
</div>
<div className="float-right">
<div className="flex flex-row-reverse items-center gap-x-3">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}
>
}}>
Cancel
</Button>
<Button
className="ml-3"
disabled={!typedSignature}
onClick={() => {
localStorage.setItem(
"typedSignature",
typedSignature
);
localStorage.setItem("typedSignature", typedSignature);
props.onClose({
type: "type",
typedSignature: typedSignature,
});
}}
>
}}>
Sign
</Button>
</div>
@ -139,52 +130,55 @@ export default function SignatureDialog(props: any) {
""
)}
{isCurrentTab("Draw") ? (
<div className="">
<SignatureCanvas
ref={(ref) => {
signCanvasRef = ref;
}}
canvasProps={{
className:
"sigCanvas border-b b-2 border-slate w-full h-full mb-3",
}}
clearOnResize={true}
onEnd={() => {
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
<IconButton
className="block float-left"
icon={TrashIcon}
onClick={() => {
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
></IconButton>
<div className="mt-10 float-right">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
<div className="" key={props.open ? "closed" : "open"}>
{showCanvas && (
<SignatureCanvas
ref={(ref) => {
signCanvasRef = ref;
}}
>
Cancel
</Button>
<Button
className="ml-3"
onClick={() => {
props.onClose({
type: "draw",
signatureImage:
signCanvasRef.toDataURL("image/png"),
});
canvasProps={{
className: "sigCanvas border-b b-2 border-slate w-full h-full mb-3",
}}
disabled={signatureEmpty}
>
Sign
</Button>
clearOnResize={true}
onEnd={() => {
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
)}
<div className="flex items-center justify-between">
<IconButton
className="block"
icon={TrashIcon}
onClick={() => {
signCanvasRef?.clear();
setSignatureEmpty(signCanvasRef?.isEmpty());
}}
/>
<div className="flex flex-row-reverse items-center gap-x-3">
<Button
color="secondary"
onClick={() => {
props.onClose();
props.setOpen(false);
setCurrent(tabs[0]);
}}>
Cancel
</Button>
<Button
className="ml-3"
onClick={() => {
props.onClose({
type: "draw",
signatureImage: signCanvasRef.toDataURL("image/png"),
});
}}
disabled={signatureEmpty}>
Sign
</Button>
</div>
</div>
</div>
) : (
@ -200,11 +194,11 @@ export default function SignatureDialog(props: any) {
</>
);
function isCurrentTab(tabName: string): boolean {
function isCurrentTab(tabName: string): boolean {
return currentTab.name === tabName;
}
function setCurrent(t: any) {
function setCurrent(t: any) {
tabs.forEach((tab) => {
tab.current = tab.name === t.name;
});

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,9 +1,13 @@
import { useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { useSubscription } from "@documenso/lib/stripe";
import { BillingWarning } from "./billing-warning";
import Navigation from "./navigation";
import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
import { SubscriptionStatus } from "@prisma/client";
import { useSession } from "next-auth/react";
function useRedirectToLoginIfUnauthenticated() {
const { data: session, status } = useSession();
@ -31,11 +35,16 @@ function useRedirectToLoginIfUnauthenticated() {
export default function Layout({ children }: any) {
useRedirectToLoginIfUnauthenticated();
const { subscription } = useSubscription();
return (
<>
<div className="min-h-full">
<Navigation></Navigation>
<Navigation />
<main>
<BillingWarning />
<div className="mx-auto max-w-7xl sm:px-6 lg:px-8">{children}</div>
</main>
</div>

View File

@ -1,14 +1,13 @@
import { LockClosedIcon } from "@heroicons/react/20/solid";
import Link from "next/link";
import { FormProvider, useForm } from "react-hook-form";
import Logo from "./logo";
import { signIn } from "next-auth/react";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "react-hot-toast";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import Logo from "./logo";
import { LockClosedIcon } from "@heroicons/react/20/solid";
import { signIn } from "next-auth/react";
import { FormProvider, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
interface LoginValues {
email: string;
@ -22,10 +21,7 @@ export default function Login(props: any) {
const methods = useForm<LoginValues>();
const { register, formState } = methods;
const [errorMessage, setErrorMessage] = useState<string | null>(null);
let callbackUrl =
typeof router.query?.callbackUrl === "string"
? router.query.callbackUrl
: "";
let callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "";
// If not absolute URL, make it absolute
if (!/^https?:\/\//.test(callbackUrl)) {
@ -73,16 +69,13 @@ export default function Login(props: any) {
<div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
<div>
<Logo className="mx-auto h-10 w-auto"></Logo>
<Logo className="mx-auto h-20 w-auto text-black"></Logo>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
Sign in to your account
</h2>
</div>
<FormProvider {...methods}>
<form
className="mt-8 space-y-6"
onSubmit={methods.handleSubmit(onSubmit)}
>
<form className="mt-8 space-y-6" onSubmit={methods.handleSubmit(onSubmit)}>
<input type="hidden" name="remember" defaultValue="true" />
<div className="-space-y-px rounded-md shadow-sm">
<div>
@ -96,7 +89,7 @@ export default function Login(props: any) {
type="email"
autoComplete="email"
required
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Email"
/>
</div>
@ -111,27 +104,28 @@ export default function Login(props: any) {
type="password"
autoComplete="current-password"
required
className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Password"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<a href="#" className="font-medium text-neon hover:text-neon">
<Link
href="/forgot-password"
className="hover:text-neon-700 font-medium text-gray-500">
Forgot your password?
</a>
</Link>
</div>
</div>
<div>
<Button
type="submit"
disabled={formState.isSubmitting}
className="group relative flex w-full"
>
className="group relative flex w-full">
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<LockClosedIcon
className="h-5 w-5 text-neon-dark group-hover:text-neon disabled:group-hover:bg-gray-600 disabled:disabled:bg-gray-600"
className="text-neon-700 group-hover:text-neon-dark-700 h-5 w-5 duration-200 disabled:disabled:bg-gray-600 disabled:group-hover:bg-gray-600"
aria-hidden="true"
/>
</span>
@ -140,10 +134,7 @@ export default function Login(props: any) {
</div>
<div>
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center"></div>
@ -154,8 +145,7 @@ export default function Login(props: any) {
Are you new here?{" "}
<Link
href="/signup"
className="font-medium text-neon hover:text-neon"
>
className="hover:text-neon-700 font-medium text-gray-500 duration-200">
Create a new Account
</Link>
</p>
@ -164,9 +154,8 @@ export default function Login(props: any) {
Like Documenso{" "}
<Link
href="https://documenso.com"
className="font-medium text-neon hover:text-neon"
>
Hosted Documenso will be availible soon
className="text-neon hover:text-neon font-medium">
Hosted Documenso is here!
</Link>
</p>
)}

View File

@ -1,28 +1,80 @@
import { classNames } from "@documenso/lib";
import Link from "next/link";
import { classNames } from "@documenso/lib";
export default function Logo(props: any) {
return (
<>
<Link href="/dashboard">
<svg
className="w-12"
viewBox="0 0 88.6758041381836 32.18000030517578"
{...props}
>
<rect
width="88.6758041381836"
height="32.18000030517578"
fill="transparent"
></rect>
<g transform="matrix(1,0,0,1,-25.98720359802246,-66.41200256347656)">
<path
d="M99.004,68.26l-.845-1.848-23.226,2.299c-1.437,.142-2.876,.089-4.3-.156-2.949-.51-6.32-1.717-9.073-2.099-1.477-.206-2.934,.448-3.789,1.67-1.174,1.678-2.308,3.888-3.501,5.622-1.518,2.207-3.032,4.418-4.531,6.638-1.693,2.499-3.365,5.013-5.061,7.51-.103,.153-.333,.221-.503,.329-.079-.279-.306-.636-.206-.824,1.006-1.928,1.845-4,3.165-5.694,1.908-2.449,2.914-5.445,5.007-7.745,.342-.777,.845-1.466,1.151-2.244,.915-2.341-1.295-5.305-2.935-5.305h-.613c-1.196,0-2.526,.357-4.092,1.716-.986,.856-2.391,2.432-2.97,3.326-3.424,5.286-7.177,10.382-10.15,15.932-.365,.682-1.719,3.722-2.013,3.606-.214-.077-.159-.458-.041-.823,1.312-4.065,4.163-9.851,5.843-13.777,.41-.962,.635-1.516-.305-2.361-1.016-.913-2.669,.084-3.084,1.052-1.784,4.174-2.631,8.08-4.038,12.396-.466,1.43-.916,2.865-1.386,4.294-.273,.831-.548,1.661-.836,2.488-.112,.321-.226,.642-.345,.962-.032,.082-.064,.165-.095,.248-.006,.011-.003,.002-.009,.017-.005,.008-.003,.015-.006,.023-.011,.026-.021,.05-.03,.076-.024,.067-.012,.044,.009-.002-.691,1.577,.417,3.002,2.091,3.002,.808,0,1.552-1.431,1.943-2.055,1.224-1.957,3.28-5.125,4.36-7.163,.894-1.69,2.078-3.14,3.209-4.615,1.857-2.42,3.065-5.242,5.025-7.575,.33-.394,.694-.772,1.093-1.093,.121-.098,.473-.05,.618,.062,.124,.098,.2,.432,.127,.577-.397,.788-.863,1.542-1.278,2.322-1.481,2.79-2.953,5.582-4.425,8.377-1.16,2.204-2.659,5.055-3.551,6.755-.438,.833-.176,1.857,.604,2.382,0,0-.383,2.028,1.908,2.028,1.896,0,2.711-1.145,3.053-1.624,.348-.486,.748-.951,1.202-1.334,.151-.13,.563-.018,.821,.079,.076,.029,.085,.406,.03,.582-.398,1.272-.323,2.283,1.879,2.283,.784,0,2.218-1.283,2.904-2.213,.794-1.075,.731-1.1,1.415-2.244,1.678-2.821,3.132-5.055,4.751-7.91,1.083-1.91,2.175-3.854,3.294-5.516,.685-1.015,1.446-1.252,2.188-1.252s.409,.686,.336,1.025c-.071,.341-.677,1.545-2.531,4.35-1.115,1.687-2.244,3.367-3.308,5.084-1.075,1.736-2.141,3.48-3.205,5.225-.88,1.445,.162,3.467,1.854,3.467h2.765c2.653,0,5.302-.397,7.916-.851l7.41-1.287c1.421-.245,2.868-.298,4.303-.158l23.161,2.296,.845-1.849c4.61-9.858,15.211-11.322,15.66-11.38v-5.717c-.448-.058-11.05-1.522-15.66-11.381Zm-40.143,6.165c-.164,.465-.491,.922-.86,1.255-.918,.828-1.451,1.842-1.911,2.977-.512,1.269-1.829,2.119-1.966,3.65-.027,.3-.627,.577-1.006,.797-.109,.062-.357-.114-.524-.174,.015-.229-.024-.401,.039-.515,.848-1.495,1.678-3.002,2.584-4.462,.775-1.254,1.642-2.453,2.469-3.677,.085-.126,.158-.273,.27-.365,.457-.368,.93-.719,1.399-1.075,.497,.721-.312,1.071-.494,1.589Zm30.047,12.011c1.736,0,3.162-1.137,3.686-2.693h10.547c-3.02,1.916-6.1,4.684-8.402,8.716l-21.902-2.17v-15.584l21.902-2.167c2.302,4.032,5.382,6.802,8.402,8.714h-10.547c-.524-1.554-1.951-2.691-3.686-2.691-2.172,0-3.938,1.763-3.938,3.938s1.766,3.938,3.938,3.938Z"
className={classNames(props.dark ? "fill-white" : "fill-brown")}
></path>
</g>
</svg>
</Link>
<svg viewBox="0 0 64 64" {...props}>
<rect width="88.6758041381836" height="32.18000030517578" fill="transparent"></rect>
<path
d="M27.07 9.25832C26.333 9.92796 25.5176 10.7145 24.5857 11.6341C23.9957 12.0973 23.2682 12.3587 22.5117 12.3733L19.3896 12.4333L20.2992 11.5237C25.815 6.0079 28.5729 3.25 32 3.25C35.4271 3.25 38.185 6.00789 43.7008 11.5237L44.6087 12.4317L41.5937 12.3749C40.7437 12.3588 39.9292 12.0311 39.3051 11.4539L37.4972 9.78198C37.3255 9.6212 37.1581 9.46631 36.9946 9.31712L36.897 9.22687L36.8953 9.22687C36.2778 8.667 35.7153 8.18958 35.1851 7.78508C33.6538 6.6167 32.7624 6.35263 32 6.35263C31.2376 6.35263 30.3462 6.6167 28.8149 7.78508C28.2783 8.19451 27.7085 8.67864 27.0821 9.24737L27.0814 9.24737L27.07 9.25832Z"
fill="currentColor"
/>
<path
d="M54.6826 27.0051C54.5337 26.8419 54.3791 26.6748 54.2187 26.5035L52.5459 24.6946C51.9691 24.0709 51.6413 23.2571 51.6249 22.4077L51.5667 19.3896L52.4763 20.2992C57.9921 25.815 60.75 28.5729 60.75 32C60.75 35.4271 57.9921 38.185 52.4763 43.7008L51.5667 44.6104L51.6249 41.5923C51.6413 40.7429 51.9691 39.9291 52.5459 39.3054L54.2185 37.4968C54.379 37.3253 54.5337 37.1581 54.6827 36.9948L54.7731 36.897V36.8953C55.333 36.2778 55.8104 35.7153 56.2149 35.1851C57.3833 33.6538 57.6474 32.7624 57.6474 32C57.6474 31.2376 57.3833 30.3462 56.2149 28.8149C55.8104 28.2847 55.333 27.7222 54.7731 27.1047V27.103L54.6826 27.0051Z"
fill="currentColor"
/>
<path
d="M36.9601 54.7143C37.1446 54.5464 37.334 54.3711 37.5289 54.1883L39.3054 52.5457C39.9294 51.9687 40.7435 51.6411 41.5932 51.6249L44.6096 51.5675L43.7008 52.4763C38.185 57.9921 35.4271 60.75 32 60.75C28.5729 60.75 25.815 57.9921 20.2992 52.4763L19.3896 51.5667L22.4141 51.6248C23.2599 51.641 24.0705 51.9659 24.6934 52.5383L25.9131 53.6592C27.0267 54.726 27.9626 55.5647 28.8149 56.2149C30.3462 57.3833 31.2376 57.6474 32 57.6474C32.7624 57.6474 33.6538 57.3833 35.1851 56.2149C35.7217 55.8055 36.2915 55.3214 36.9179 54.7526H36.9187L36.9601 54.7143Z"
fill="currentColor"
/>
<path
d="M9.26202 36.9341C9.44675 37.1373 9.64036 37.3465 9.8432 37.5625L11.4547 39.3051C12.0317 39.929 12.3594 40.7431 12.3756 41.5927L12.4333 44.6104L11.5237 43.7008C6.0079 38.185 3.25 35.4271 3.25 32C3.25 28.5729 6.00789 25.815 11.5237 20.2992L12.4325 19.3904L12.3754 22.4067C12.3593 23.2567 12.0314 24.0711 11.4541 24.6952L9.79271 26.4913C9.62762 26.6675 9.46871 26.8392 9.3158 27.0069L9.22687 27.103L9.22687 27.1047C8.66699 27.7222 8.18958 28.2847 7.78508 28.8149C6.6167 30.3462 6.35263 31.2376 6.35263 32C6.35263 32.7624 6.6167 33.6538 7.78508 35.1851C8.1946 35.7219 8.67887 36.2918 9.24777 36.9184L9.24777 36.9187L9.26202 36.9341Z"
fill="currentColor"
/>
<path
d="M9.24777 27.0804L11.4541 24.6952C11.9658 24.1421 12.2815 23.4395 12.3579 22.6951C12.367 21.4688 12.387 20.3991 12.4313 19.4536L12.4337 19.3242L12.4377 19.3202C12.4785 18.5034 12.5382 17.7805 12.6257 17.1297C12.8823 15.2207 13.3259 14.4037 13.865 13.8646C14.4041 13.3255 15.2211 12.882 17.1301 12.6253C17.7929 12.5362 18.5306 12.4759 19.3661 12.4351L19.3675 12.4337L19.4131 12.4329C20.3923 12.3861 21.5054 12.3657 22.7886 12.3569C23.5626 12.2798 24.2914 11.9441 24.8545 11.3998L27.0813 9.24742H25.7951C17.9946 9.24742 14.0944 9.24742 11.6711 11.6707C9.24777 14.094 9.24777 17.9943 9.24777 25.7948V27.0804Z"
fill="currentColor"
/>
<path
d="M9.24777 36.9187V38.2053C9.24777 46.0058 9.24777 49.9061 11.6711 52.3294C14.0944 54.7527 17.9946 54.7527 25.7951 54.7527H38.2057C46.0062 54.7527 49.9064 54.7527 52.3297 52.3294C54.753 49.9061 54.753 46.0058 54.753 38.2053V36.9187L52.5459 39.3054C52.0356 39.8571 51.7203 40.5577 51.643 41.3C51.6337 42.5529 51.613 43.6424 51.5668 44.603L51.5663 44.6325L51.5654 44.6334C51.5246 45.4693 51.4643 46.2073 51.3752 46.8704C51.1185 48.7794 50.6749 49.5964 50.1358 50.1355C49.5967 50.6746 48.7797 51.1181 46.8707 51.3748C46.2197 51.4623 45.4965 51.522 44.6793 51.5628L44.6758 51.5663L44.5626 51.5684C43.6127 51.6132 42.5373 51.6334 41.3032 51.6426C40.5597 51.7193 39.858 52.0347 39.3054 52.5457L36.9187 54.7526L27.103 54.7526L24.6934 52.5383C24.1424 52.032 23.4445 51.7193 22.7052 51.6426C21.4558 51.6334 20.3688 51.6129 19.4101 51.5671L19.3675 51.5663L19.3662 51.565C18.5307 51.5242 17.7929 51.4639 17.1301 51.3748C15.2211 51.1181 14.4041 50.6746 13.865 50.1355C13.3259 49.5964 12.8823 48.7794 12.6257 46.8704C12.5365 46.2075 12.4763 45.4698 12.4355 44.6342L12.4337 44.6325L12.4326 44.5753C12.3874 43.6221 12.367 42.5422 12.3579 41.3022C12.281 40.559 11.9655 39.8575 11.4547 39.3051L9.24777 36.9187Z"
fill="currentColor"
/>
<path
d="M51.643 22.7C51.7203 23.4423 52.0356 24.1428 52.5459 24.6946L54.753 27.0813V25.7948C54.753 17.9943 54.753 14.094 52.3297 11.6707C49.9064 9.24742 46.0062 9.24742 38.2057 9.24742H36.9192L39.3051 11.4539C39.8586 11.9658 40.5618 12.2815 41.3067 12.3575C42.5257 12.3666 43.5898 12.3865 44.531 12.4302L44.7192 12.4337L44.725 12.4396C45.5235 12.4803 46.2319 12.5394 46.8707 12.6253C48.7797 12.882 49.5967 13.3255 50.1358 13.8646C50.6749 14.4037 51.1185 15.2207 51.3752 17.1297C51.4643 17.7928 51.5246 18.5307 51.5654 19.3666L51.5663 19.3675L51.5668 19.3971C51.613 20.3577 51.6337 21.4471 51.643 22.7Z"
fill="currentColor"
/>
<path
d="M29.6453 18.2543L27.5526 20.0304C27.1792 20.3474 26.7112 20.5317 26.2219 20.5545L22.7195 20.7177L24.5458 18.8913C28.071 15.3661 29.8336 13.6035 32.0239 13.6035C34.2142 13.6035 35.9768 15.3661 39.502 18.8913L41.3172 20.7065L37.7657 20.5526C37.2678 20.531 36.7917 20.3422 36.4143 20.0167L34.8345 18.6538C34.2799 18.1319 33.8096 17.7194 33.3805 17.392C32.5014 16.7212 32.168 16.706 32.0239 16.7059C31.8799 16.7059 31.5465 16.7212 30.6674 17.392C30.6533 17.4027 30.6393 17.4135 30.6252 17.4243L30.6232 17.4243L30.6024 17.4419C30.3079 17.6703 29.9934 17.9385 29.6453 18.2543Z"
fill="currentColor"
/>
<path
d="M46.4935 30.4715C45.8957 29.7157 45.0376 28.8234 43.7954 27.5741C43.5923 27.2496 43.4753 26.8756 43.4596 26.4879L43.306 22.6953L45.1106 24.4999C48.6358 28.0251 50.3985 29.7878 50.3985 31.978C50.3985 34.1683 48.6358 35.9309 45.1106 39.4561L43.2963 41.2705L43.4711 37.6457C43.4954 37.142 43.6908 36.6617 44.025 36.284L45.352 34.7845C45.7709 34.3392 46.1192 33.9484 46.4095 33.5895L46.573 33.4048V33.3829C46.5854 33.3667 46.5978 33.3506 46.61 33.3346C47.2808 32.4555 47.296 32.1221 47.296 31.978C47.296 31.834 47.2808 31.5006 46.61 30.6215C46.5978 30.6054 46.5854 30.5894 46.573 30.5732V30.555L46.4935 30.4715Z"
fill="currentColor"
/>
<path
d="M17.4826 30.5696L19.902 27.9386C20.2447 27.566 20.4494 27.0873 20.4821 26.5821L20.616 24.5168C20.6363 23.8629 20.6699 23.3105 20.7254 22.829L20.7349 22.6825L20.7445 22.673C20.7466 22.6564 20.7488 22.6398 20.751 22.6234C20.8983 21.5275 21.1233 21.2809 21.2251 21.1791C21.327 21.0772 21.5736 20.8523 22.6695 20.7049C23.0213 20.6576 23.4118 20.6238 23.8544 20.5996L26.8259 20.3064C27.3027 20.2594 27.7513 20.0591 28.1046 19.7357L30.6159 17.4365H28.0583C23.0729 17.4365 20.5802 17.4365 19.0314 18.9853C17.712 20.3048 17.5166 22.3093 17.4877 25.9566C17.4826 26.5905 17.4826 27.274 17.4826 28.0122L17.4826 30.5632V30.5696Z"
fill="currentColor"
/>
<path
d="M17.4826 33.3865V33.3957L17.4826 35.9439C17.4826 36.6821 17.4826 37.3656 17.4877 37.9995C17.5166 41.6468 17.7119 43.6514 19.0314 44.9708C20.3509 46.2903 22.3554 46.4856 26.0028 46.5146C26.6366 46.5196 27.3201 46.5196 28.0583 46.5196H30.6059H33.4384H35.99C36.728 46.5196 37.4114 46.5196 38.0451 46.5146C41.6927 46.4856 43.6974 46.2903 45.0169 44.9708C46.5657 43.422 46.5657 40.9293 46.5657 35.9439V33.3787L43.9873 36.1746C43.7295 36.4542 43.5498 36.7933 43.462 37.158C43.457 38.9702 43.4324 40.2302 43.3142 41.2003L43.313 41.252L43.3071 41.2579C43.3039 41.283 43.3006 41.308 43.2973 41.3327C43.15 42.4286 42.925 42.6752 42.8231 42.7771C42.7213 42.8789 42.4747 43.1039 41.3788 43.2512C41.3541 43.2545 41.3291 43.2578 41.304 43.261L41.2979 43.2671L41.2111 43.2724C40.6594 43.3378 40.0141 43.3737 39.2282 43.3934L37.4602 43.5013C36.9569 43.5321 36.4791 43.7335 36.1058 44.0725L33.4107 46.5196H30.5938L27.8997 44.0008C27.5451 43.6693 27.0925 43.4643 26.612 43.4152C26.4039 43.4144 26.2033 43.4132 26.0098 43.4117C24.5576 43.3999 23.5048 43.3635 22.6695 43.2512C21.5736 43.1039 21.327 42.8789 21.2251 42.7771C21.1233 42.6752 20.8983 42.4286 20.751 41.3327C20.7488 41.3163 20.7466 41.2998 20.7445 41.2832L20.7349 41.2737L20.734 41.2529L20.7304 41.1702C20.6321 40.3446 20.6002 39.3097 20.5899 37.9068L20.5744 37.5474C20.5703 37.451 20.5598 37.3554 20.5434 37.2613C20.4714 36.8491 20.2836 36.4635 19.9993 36.1511L17.4826 33.3865Z"
fill="currentColor"
/>
<path
d="M43.4361 24.6464L43.5814 26.617C43.6181 27.1141 43.8212 27.5842 44.158 27.9516L46.5657 30.5773V28.0122C46.5657 23.0268 46.5657 20.5341 45.0169 18.9853C43.4681 17.4365 40.9754 17.4365 35.99 17.4365H33.4104L35.9941 19.7902C36.3562 20.1201 36.8173 20.3206 37.3055 20.3606L40.3018 20.6057C40.5881 20.6227 40.8522 20.6441 41.098 20.6709L41.3195 20.689L41.3288 20.6983C41.3455 20.7005 41.3622 20.7027 41.3788 20.7049C42.4747 20.8523 42.7213 21.0772 42.8231 21.1791C42.925 21.2809 43.15 21.5275 43.2973 22.6234C43.3724 23.1822 43.4136 23.8384 43.4361 24.6464Z"
fill="currentColor"
/>
<path
d="M20.7437 41.2626L20.734 41.2529L20.7349 41.2737L20.7445 41.2832L20.7437 41.2626Z"
fill="currentColor"
/>
<path
d="M22.7418 43.2607L26.0098 43.4117C24.5992 43.4003 23.5654 43.3656 22.7418 43.2607Z"
fill="currentColor"
/>
<path
d="M20.6049 37.695C20.5987 37.5479 20.578 37.4027 20.5434 37.2613C20.5598 37.3554 20.5703 37.451 20.5744 37.5474L20.5899 37.9068C20.6002 39.3097 20.6321 40.3446 20.7304 41.1702L20.734 41.2529L20.7437 41.2626L20.6049 37.695Z"
fill="currentColor"
/>
<path
d="M13.6494 31.978C13.6494 33.8441 14.9288 35.3997 17.4877 37.9995C17.4826 37.3656 17.4826 36.6821 17.4826 35.9439L17.4826 33.3957L17.4772 33.3895L17.4772 33.3858C17.464 33.3687 17.4508 33.3516 17.4379 33.3346C16.7671 32.4555 16.7518 32.1221 16.7518 31.978C16.7518 31.834 16.7671 31.5006 17.4379 30.6215C17.4526 30.6021 17.4675 30.5827 17.4826 30.5632L17.4826 28.0122C17.4826 27.274 17.4826 26.5905 17.4877 25.9566C14.9288 28.5563 13.6494 30.112 13.6494 31.978Z"
fill="currentColor"
/>
<path
d="M30.6674 46.5641C30.6549 46.5546 30.6425 46.5451 30.63 46.5354H30.6232L30.6059 46.5196H28.0583C27.3201 46.5196 26.6366 46.5196 26.0028 46.5146C28.6023 49.0732 30.1579 50.3526 32.0239 50.3526C33.8899 50.3526 35.4455 49.0732 38.0451 46.5146C37.4114 46.5196 36.728 46.5196 35.99 46.5196H33.4384C33.419 46.5346 33.3997 46.5494 33.3805 46.5641C32.5014 47.2348 32.168 47.2501 32.0239 47.2501C31.8799 47.2501 31.5465 47.2348 30.6674 46.5641Z"
fill="currentColor"
/>
</svg>
</>
);
}

View File

@ -1,23 +1,22 @@
import { Fragment, useEffect, useState } from "react";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { signOut, useSession } from "next-auth/react";
import avatarFromInitials from "avatar-from-initials";
import { toast } from "react-hot-toast";
import { getUser } from "@documenso/lib/api";
import Logo from "./logo";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import {
ArrowRightOnRectangleIcon,
Bars3Icon,
BellIcon,
XMarkIcon,
UserCircleIcon,
ArrowRightOnRectangleIcon,
DocumentTextIcon,
ChartBarIcon,
DocumentTextIcon,
UserCircleIcon,
WrenchIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import Logo from "./logo";
import { getUser } from "@documenso/lib/api";
import avatarFromInitials from "avatar-from-initials";
import { signOut, useSession } from "next-auth/react";
import { toast } from "react-hot-toast";
const navigation = [
{
@ -113,9 +112,12 @@ export default function TopNavigation() {
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 justify-between">
<div className="flex">
<div className="flex flex-shrink-0 items-center">
<Logo></Logo>
</div>
<Link
href="/dashboard"
className="flex flex-shrink-0 items-center gap-x-2 self-center overflow-hidden">
<Logo className="h-8 w-8 text-black" />
</Link>
<div className="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8">
{navigation.map((item) => (
<Link
@ -125,14 +127,12 @@ export default function TopNavigation() {
item.current
? "border-neon text-brown"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
"inline-flex items-center border-b-2 px-1 pt-1 text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
aria-current={item.current ? "page" : undefined}>
<item.icon
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
aria-hidden="true"
></item.icon>
className="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
aria-hidden="true"></item.icon>
{item.name}
</Link>
))}
@ -142,8 +142,7 @@ export default function TopNavigation() {
onClick={() => {
document?.getElementById("mb")?.click();
}}
className="hidden sm:ml-6 sm:flex sm:items-center hover:bg-gray-200 px-3 cursor-pointer"
>
className="hidden cursor-pointer px-3 hover:bg-gray-200 sm:ml-6 sm:flex sm:items-center">
<span className="text-sm">
<p className="font-bold">{user?.name || ""}</p>
<p>{user?.email}</p>
@ -152,8 +151,7 @@ export default function TopNavigation() {
<div>
<Menu.Button
id="mb"
className="flex max-w-xs items-center rounded-full bg-white text-sm"
>
className="flex max-w-xs items-center rounded-full bg-white text-sm">
<span className="sr-only">Open user menu</span>
<div
key={user?.email}
@ -170,8 +168,7 @@ export default function TopNavigation() {
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
leaveTo="transform opacity-0 scale-95">
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
{userNavigation.map((item) => (
<Menu.Item key={item.name}>
@ -182,12 +179,10 @@ export default function TopNavigation() {
className={classNames(
active ? "bg-gray-100" : "",
"block px-4 py-2 text-sm text-gray-700"
)}
>
)}>
<item.icon
className="flex-shrink-0 -ml-1 mr-3 h-6 w-6 inline"
aria-hidden="true"
></item.icon>
className="-ml-1 mr-3 inline h-6 w-6 flex-shrink-0"
aria-hidden="true"></item.icon>
{item.name}
</Link>
)}
@ -219,15 +214,14 @@ export default function TopNavigation() {
href={item.href}
className={classNames(
item.current
? "bg-teal-50 border-teal-500 text-teal-700"
: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800",
"block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
? "border-teal-500 bg-teal-50 text-teal-700"
: "border-transparent text-gray-600 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-800",
"block border-l-4 py-2 pl-3 pr-4 text-base font-medium"
)}
aria-current={item.current ? "page" : undefined}
onClick={() => {
close();
}}
>
}}>
{item.name}
</Link>
))}
@ -259,8 +253,7 @@ export default function TopNavigation() {
: item.click
}
href={item.href}
className="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800"
>
className="block px-4 py-2 text-base font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-800">
{item.name}
</Link>
))}

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

@ -1,12 +1,15 @@
import { ChangeEvent, useEffect, useState } from "react";
import { KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import Link from "next/link";
import Head from "next/head";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { updateUser } from "@documenso/features";
import { Button } from "@documenso/ui";
import { getUser } from "@documenso/lib/api";
import { fetchPortalSession, isSubscriptionsEnabled, useSubscription } from "@documenso/lib/stripe";
import { Button } from "@documenso/ui";
import { BillingPlans } from "./billing-plans";
import { CreditCardIcon, KeyIcon, UserCircleIcon } from "@heroicons/react/24/outline";
import { SubscriptionStatus } from "@prisma/client";
import { useSession } from "next-auth/react";
const subNavigation = [
{
@ -23,17 +26,26 @@ const subNavigation = [
},
];
if (process.env.NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS === "true") {
subNavigation.push({
name: "Billing",
href: "/settings/billing",
icon: CreditCardIcon,
current: false,
});
}
function classNames(...classes: any) {
return classes.filter(Boolean).join(" ");
}
export default function Setttings() {
const session = useSession();
const { subscription, hasSubscription } = useSubscription();
const [user, setUser] = useState({
email: "",
name: "",
});
useEffect(() => {
getUser().then((res: any) => {
res.json().then((j: any) => {
@ -48,6 +60,7 @@ export default function Setttings() {
});
const [savingTimeout, setSavingTimeout] = useState<any>();
const [password, setPassword] = useState("");
function handleNameChange(e: ChangeEvent<HTMLInputElement>): void {
let u = { ...user };
u.name = e.target.value;
@ -74,15 +87,12 @@ export default function Setttings() {
</Head>
<header className="py-10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold leading-tight tracking-tight text-brown">
Settings
</h1>
<h1 className="text-brown text-3xl font-bold leading-tight tracking-tight">Settings</h1>
</div>
</header>
<div
className="mx-auto max-w-screen-xl px-4 pb-6 sm:px-6 lg:px-8 lg:pb-16"
hidden={!user.email}
>
hidden={!user.email}>
<div className="overflow-hidden rounded-lg bg-white shadow">
<div className="divide-y divide-gray-200 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<aside className="py-6 lg:col-span-3">
@ -93,18 +103,17 @@ export default function Setttings() {
href={item.href}
className={classNames(
item.current
? "bg-teal-50 border-neon-dark text-teal-700 hover:bg-teal-50 hover:text-teal-700"
? "border-neon-dark bg-teal-50 text-teal-700 hover:bg-teal-50 hover:text-teal-700"
: "border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900",
"group border-l-4 px-3 py-2 flex items-center text-sm font-medium"
"group flex items-center border-l-4 px-3 py-2 text-sm font-medium"
)}
aria-current={item.current ? "page" : undefined}
>
aria-current={item.current ? "page" : undefined}>
<item.icon
className={classNames(
item.current
? "text-teal-500 group-hover:text-teal-500"
: "text-gray-400 group-hover:text-gray-500",
"flex-shrink-0 -ml-1 mr-3 h-6 w-6"
"-ml-1 mr-3 h-6 w-6 flex-shrink-0"
)}
aria-hidden="true"
/>
@ -115,20 +124,14 @@ export default function Setttings() {
</aside>
<form
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9"
action="#"
method="POST"
hidden={
subNavigation.filter((e) => e.current)[0]?.name !==
subNavigation[0].name
}
>
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation[0].name}>
{/* Profile section */}
<div className="py-6 px-4 sm:p-6 lg:pb-8">
<div>
<h2 className="text-lg font-medium leading-6 text-gray-900">
Profile
</h2>
<h2 className="text-lg font-medium leading-6 text-gray-900">Profile</h2>
<p className="mt-1 text-sm text-gray-500">
Let people know who they are dealing with builds trust.
</p>
@ -136,10 +139,7 @@ export default function Setttings() {
<div className="my-6 grid grid-cols-12 gap-6">
<div className="col-span-12 sm:col-span-6">
<label
htmlFor="first-name"
className="block text-sm font-medium text-gray-700"
>
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
Full Name
</label>
<input
@ -150,14 +150,11 @@ export default function Setttings() {
onChange={(e) => handleNameChange(e)}
onKeyDown={handleKeyPress}
autoComplete="given-name"
className="mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:outline-none sm:text-sm"
/>
</div>
<div className="col-span-12 sm:col-span-6">
<label
htmlFor="first-name"
className="block text-sm font-medium text-gray-700"
>
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
@ -167,36 +164,117 @@ export default function Setttings() {
name="first-name"
id="first-name"
autoComplete="given-name"
className="mt-1 block w-full rounded-md border disabled:bg-neutral-100 border-gray-300 py-2 px-3 shadow-sm focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:outline-none disabled:bg-neutral-100 sm:text-sm"
/>
</div>
</div>
<Button onClick={() => updateUser(user)}>Save</Button>
</div>
</form>
<div
hidden={
subNavigation.filter((e) => e.current)[0]?.name !==
subNavigation[1].name
}
className="divide-y divide-gray-200 lg:col-span-9 min-h-[251px]"
>
hidden={subNavigation.filter((e) => e.current)[0]?.name !== subNavigation[1].name}
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9">
{/* Passwords section */}
<div className="py-6 px-4 sm:p-6 lg:pb-8">
<div>
<h2 className="text-lg font-medium leading-6 text-gray-900">
Password
</h2>
<p className="mt-1 text-sm text-gray-500">
Forgot your passwort? Email <b>hi@documenso.com</b> to reset
it.
</p>
<h2 className="text-lg font-medium leading-6 text-gray-900">Update Password</h2>
<div className="my-6 grid grid-cols-12 gap-6">
<div className="col-span-12 sm:col-span-6">
<label
htmlFor="first-name"
className="block text-sm font-medium text-gray-700">
New Password
</label>
<input
type="password"
name="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="focus:border-neon focus:ring-neon mt-1 block w-full rounded-md border border-gray-300 py-2 px-3 shadow-sm focus:outline-none sm:text-sm"
/>
</div>
</div>
<Button
disabled={password.length < 6}
onClick={() => updateUser({ ...user, password })}>
Save
</Button>
</div>
</div>
</div>
<div
hidden={
!subNavigation.at(2) ||
subNavigation.find((e) => e.current)?.name !== subNavigation.at(2)?.name
}
className="min-h-[251px] divide-y divide-gray-200 lg:col-span-9">
{/* Billing section */}
<div className="py-6 px-4 sm:p-6 lg:pb-8">
<div>
<h2 className="text-lg font-medium leading-6 text-gray-900">Billing</h2>
{!isSubscriptionsEnabled() && (
<p className="mt-2 text-sm text-gray-500">
Subscriptions are not enabled on this instance, you have nothing to do here.
</p>
)}
{isSubscriptionsEnabled() && (
<>
<p className="mt-1 text-sm text-gray-500">
Your subscription is currently{" "}
<strong>
{subscription?.status &&
subscription?.status !== SubscriptionStatus.INACTIVE
? "Active"
: "Inactive"}
</strong>
.
</p>
{subscription?.status === SubscriptionStatus.PAST_DUE && (
<p className="mt-1 text-sm text-red-500">
Your subscription is past due. Please update your payment details to
continue using the service without interruption.
</p>
)}
<div className="mt-8">
<div className="grid grid-cols-1 lg:grid-cols-2">
<BillingPlans />
</div>
{subscription && (
<Button
onClick={() => {
if (isSubscriptionsEnabled() && subscription?.customerId) {
fetchPortalSession({
id: subscription.customerId,
}).then((res) => {
if (res.success) {
window.location.href = res.url;
}
});
}
}}>
Manage my subscription
</Button>
)}
</div>
</>
)}
</div>
</div>
</div>
</div>
</div>
</div>
<div className="mt-10 max-w-[1100px]" hidden={!!user.email}>
<div className="ph-item">
<div className="ph-col-12">

View File

@ -1,9 +1,9 @@
import Link from "next/link";
import { signup } from "@documenso/lib/api";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { Button } from "@documenso/ui";
import { XCircleIcon } from "@heroicons/react/24/outline";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
@ -107,8 +107,7 @@ export default function Signup(props: { source: string }) {
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="rgb(17 24 39 / var(--tw-text-opacity))"
className="w-8 h-8 inline mb-1"
>
className="mb-1 inline h-8 w-8">
<path
strokeLinecap="round"
strokeLinejoin="round"
@ -130,8 +129,7 @@ export default function Signup(props: { source: string }) {
form.clearErrors();
trigger();
}}
className="mt-8 space-y-6"
>
className="mt-8 space-y-6">
<input type="hidden" name="remember" defaultValue="true" />
<div className="-space-y-px rounded-md shadow-sm">
<div>
@ -145,7 +143,7 @@ export default function Signup(props: { source: string }) {
type="email"
autoComplete="email"
required
className="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Email"
/>
</div>
@ -157,8 +155,7 @@ export default function Signup(props: { source: string }) {
{...register("password", {
minLength: {
value: 7,
message:
"Your password has to be at least 7 characters long.",
message: "Your password has to be at least 7 characters long.",
},
})}
id="password"
@ -166,7 +163,7 @@ export default function Signup(props: { source: string }) {
type="password"
autoComplete="current-password"
required
className="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-neon focus:outline-none focus:ring-neon sm:text-sm"
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Password"
/>
</div>
@ -177,16 +174,12 @@ export default function Signup(props: { source: string }) {
onClick={() => {
form.clearErrors();
}}
className="sgroup relative flex w-full"
>
className="sgroup relative flex w-full">
Create Account
</Button>
<div className="pt-2">
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center"></div>
@ -194,10 +187,7 @@ export default function Signup(props: { source: string }) {
</div>
<p className="mt-2 text-center text-sm text-gray-600">
Already have an account?{" "}
<Link
href="/login"
className="font-medium text-neon hover:text-neon"
>
<Link href="/login" className="text-gray-500 hover:text-neon-700 font-medium">
Sign In
</Link>
</p>

View File

@ -0,0 +1,18 @@
import { useEffect, useState } from "react";
export function useDebouncedValue<T>(value: T, delay: number) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}

View File

@ -1,24 +1,18 @@
/** @type {import('next').NextConfig} */
require("dotenv").config({ path: "../../.env" });
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: false,
transpilePackages: [
"@documenso/prisma",
"@documenso/lib",
"@documenso/ui",
"@documenso/pdf",
"@documenso/features",
"@documenso/signing",
"react-signature-canvas",
],
};
const withTM = require("next-transpile-modules")([
"@documenso/prisma",
"@documenso/lib",
"@documenso/ui",
"@documenso/pdf",
"@documenso/features",
"@documenso/signing",
"react-signature-canvas",
]);
const plugins = [];
plugins.push(withTM);
const moduleExports = () =>
plugins.reduce((acc, next) => next(acc), nextConfig);
module.exports = moduleExports;
module.exports = nextConfig;

View File

@ -5,38 +5,29 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"start": "next start -p $PORT",
"lint": "next lint",
"db-studio": "prisma db studio"
"db-studio": "prisma db studio",
"stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook"
},
"dependencies": {
"@documenso/lib": "*",
"@documenso/pdf": "*",
"@documenso/prisma": "*",
"@documenso/ui": "*",
"@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13",
"@pdf-lib/fontkit": "^1.1.1",
"@tailwindcss/forms": "^0.5.3",
"@types/bcryptjs": "^2.4.2",
"@types/filesystem": "^0.0.32",
"@types/react-dom": "18.0.9",
"avatar-from-initials": "^1.0.3",
"base64-arraybuffer": "^1.0.2",
"bcryptjs": "^2.4.3",
"dotenv": "^16.0.3",
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"file-loader": "^6.2.0",
"formidable": "^3.2.5",
"install": "^0.13.0",
"next": "13.0.3",
"next-auth": ">=4.20.1",
"next-transpile-modules": "^10.0.0",
"next": "13.2.4",
"next-auth": "^4.22.0",
"node-forge": "^1.3.1",
"node-signpdf": "^1.5.0",
"nodemailer": "^6.9.0",
"nodemailer-sendgrid": "^1.0.3",
"npm": "^9.1.3",
"pdf-lib": "^1.17.1",
"placeholder-loading": "^0.6.0",
"react": "18.2.0",
@ -46,20 +37,29 @@
"react-pdf": "^6.2.2",
"react-resizable": "^3.0.4",
"react-tooltip": "^5.7.2",
"sass": "^1.57.1",
"short-uuid": "^4.2.2",
"string-to-color": "^2.2.2",
"typescript": "4.8.4"
"string-to-color": "^2.2.2"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@types/bcryptjs": "^2.4.2",
"@types/filesystem": "^0.0.32",
"@types/formidable": "^2.0.5",
"@types/node": "^18.11.18",
"@types/nodemailer": "^6.4.7",
"@types/nodemailer-sendgrid": "^1.0.0",
"@types/react-dom": "18.0.9",
"@types/react-pdf": "^6.2.0",
"@types/react-resizable": "^3.0.3",
"autoprefixer": "^10.4.13",
"dotenv": "^16.0.3",
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"file-loader": "^6.2.0",
"postcss": "^8.4.19",
"tailwindcss": "^3.2.4"
"sass": "^1.57.1",
"stripe-cli": "^0.1.0",
"tailwindcss": "^3.2.4",
"typescript": "4.8.4"
}
}

View File

@ -1,31 +1,31 @@
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
import { Button } from "@documenso/ui";
import Logo from "../components/logo";
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
import Link from "next/link";
export default function Custom404() {
return (
<>
<main className="relative min-h-full bg-gray-100 isolate">
<div className="absolute top-10 left-10">
<Logo className="w-10 md:w-20" />
</div>
<main className="relative isolate min-h-full bg-gray-100">
<Link href="/" className="absolute top-10 left-10 flex gap-x-2 items-center">
<Logo className="w-10 text-black" />
<h2 className="text-2xl font-semibold">Documenso</h2>
</Link>
<div className="px-6 py-48 mx-auto text-center max-w-7xl sm:py-40 lg:px-8">
<p className="text-base font-semibold leading-8 text-brown">404</p>
<h1 className="mt-4 text-3xl font-bold tracking-tight text-brown sm:text-5xl">
<div className="mx-auto max-w-7xl px-6 py-48 text-center sm:py-40 lg:px-8">
<p className="text-brown text-base font-semibold leading-8">404</p>
<h1 className="text-brown mt-4 text-3xl font-bold tracking-tight sm:text-5xl">
Page not found
</h1>
<p className="mt-4 text-base text-gray-700 sm:mt-6">
Sorry, we couldnt find the page youre looking for.
</p>
<div className="flex justify-center mt-10">
<div className="mt-10 flex justify-center">
<Button
color="secondary"
href="/"
icon={ArrowSmallLeftIcon}
className="text-base font-semibold leading-7 text-brown"
>
className="text-brown text-base font-semibold leading-7">
Back to home
</Button>
</div>

View File

@ -1,27 +1,27 @@
import Logo from "../components/logo";
import Link from "next/link";
import { Button } from "@documenso/ui";
import Logo from "../components/logo";
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
export default function Custom500() {
return (
<>
<div className="relative flex flex-col items-center justify-center min-h-full text-white bg-black">
<div className="absolute top-10 left-10">
<Logo dark className="w-10 md:w-20" />
</div>
<div className="relative flex min-h-full flex-col items-center justify-center bg-black text-white">
<Link href="/" className="absolute top-10 left-10 flex items-center gap-x-2 invert">
<Logo className="w-10 text-black" />
<h2 className="text-2xl font-semibold text-black">Documenso</h2>
</Link>
<div className="px-4 py-10 mt-20 max-w-7xl">
<div className="mt-20 max-w-7xl px-4 py-10">
<p className="inline-flex items-center text-3xl font-bold sm:text-5xl">
500
<span className="relative px-3 font-thin sm:text-6xl -top-1.5">
|
</span>{" "}
<span className="text-base font-semibold align-middle sm:text-2xl">
<span className="relative -top-1.5 px-3 font-thin sm:text-6xl">|</span>{" "}
<span className="align-middle text-base font-semibold sm:text-2xl">
Something went wrong.
</span>
</p>
<div className="flex justify-center mt-10">
<div className="mt-10 flex justify-center">
<Button color="secondary" href="/" icon={ArrowSmallLeftIcon}>
Back to home
</Button>

View File

@ -1,13 +1,30 @@
import "../styles/tailwind.css";
import { ReactElement, ReactNode } from "react";
import { NextPage } from "next";
import type { AppProps } from "next/app";
import { Montserrat, Qwigley } from "next/font/google";
import { SubscriptionProvider } from "@documenso/lib/stripe/providers/subscription-provider";
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
import "../../../node_modules/react-resizable/css/styles.css";
import "react-tooltip/dist/react-tooltip.css";
import { ReactElement, ReactNode } from "react";
import type { AppProps } from "next/app";
import { NextPage } from "next";
import "../styles/tailwind.css";
import { SessionProvider } from "next-auth/react";
export { coloredConsole } from "@documenso/lib";
import { Toaster } from "react-hot-toast";
import "react-tooltip/dist/react-tooltip.css";
export { coloredConsole } from "@documenso/lib";
const montserrat = Montserrat({
subsets: ["latin"],
weight: ["400", "700"],
display: "swap",
variable: "--font-sans",
});
const qwigley = Qwigley({
subsets: ["latin"],
weight: ["400"],
display: "swap",
variable: "--font-qwigley",
});
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
@ -19,13 +36,17 @@ type AppPropsWithLayout = AppProps & {
export default function App({
Component,
pageProps: { session, ...pageProps },
pageProps: { session, initialSubscription, ...pageProps },
}: AppPropsWithLayout) {
const getLayout = Component.getLayout || ((page: any) => page);
return (
<SessionProvider session={session}>
<Toaster position="top-center"></Toaster>
{getLayout(<Component {...pageProps} />)}
<SubscriptionProvider initialSubscription={initialSubscription}>
<main className={`${montserrat.variable} h-full font-sans`}>
<Toaster position="top-center" />
{getLayout(<Component {...pageProps} />)}
</main>
</SubscriptionProvider>
</SessionProvider>
);
}

View File

@ -1,21 +0,0 @@
import { Head, Html, Main, NextScript } from "next/document";
import Script from "next/script";
export default function Document(props) {
let pageProps = props.__NEXT_DATA__?.props?.pageProps;
return (
<Html
className="h-full bg-gray-100 scroll-smooth font-normal antialiased"
lang="en"
>
<Head>
<meta name="color-scheme"></meta>
</Head>
<body className="flex h-full flex-col">
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@ -0,0 +1,19 @@
import { Head, Html, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html className="h-full scroll-smooth bg-gray-100 font-normal antialiased" lang="en">
<Head>
<meta name="color-scheme"></meta>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
</Head>
<body className="flex h-full flex-col">
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@ -1,9 +1,9 @@
import NextAuth, { Session } from "next-auth";
import GitHubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";
import { ErrorCode } from "@documenso/lib/auth";
import prisma from "@documenso/prisma";
import { verifyPassword } from "@documenso/lib/auth";
import prisma from "@documenso/prisma";
import NextAuth, { Session } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GitHubProvider from "next-auth/providers/github";
export default NextAuth({
secret: process.env.AUTH_SECRET,
@ -27,8 +27,7 @@ export default NextAuth({
password: {
label: "Password",
type: "password",
placeholder:
"Select a password. Here is some inspiration: https://xkcd.com/936/",
placeholder: "Select a password. Here is some inspiration: https://xkcd.com/936/",
},
},
async authorize(credentials: any) {
@ -57,10 +56,7 @@ export default NextAuth({
throw new Error(ErrorCode.UserMissingPassword);
}
const isCorrectPassword = await verifyPassword(
credentials.password,
user.password
);
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
if (!isCorrectPassword) {
throw new Error(ErrorCode.IncorrectPassword);

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

@ -1,21 +1,20 @@
import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@documenso/prisma";
import { hashPassword } from "@documenso/lib/auth";
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { IdentityProvider } from "@prisma/client";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { email, password, source } = req.body;
const cleanEmail = email.toLowerCase();
if (!cleanEmail || !cleanEmail.includes("@")) {
res.status(422).json({ message: "Invalid email" });
if (!cleanEmail || !/.+@.+/.test(cleanEmail)) {
res.status(400).json({ message: "Invalid email" });
return;
}
if (!password || password.trim().length < 7) {
return res.status(422).json({
return res.status(400).json({
message: "Password should be at least 7 characters long.",
});
}

View File

@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { Document as PrismaDocument } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
import { Document as PrismaDocument } from "@prisma/client";
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { id: documentId } = req.query;
@ -46,8 +42,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
document = await getDocument(+documentId, req, res);
}
if (!document)
res.status(404).end(`No document with id ${documentId} found.`);
if (!document) res.status(404).end(`No document with id ${documentId} found.`);
const signaturesCount = await prisma.signature.count({
where: {
@ -61,18 +56,13 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
// No need to add a signature, if no one signed yet.
if (signaturesCount > 0) {
signedDocumentAsBase64 = await addDigitalSignature(
document?.document || ""
);
signedDocumentAsBase64 = await addDigitalSignature(document?.document || "");
}
const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64");
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Length", buffer.length);
res.setHeader(
"Content-Disposition",
`attachment; filename=${document?.title}`
);
res.setHeader("Content-Disposition", `attachment; filename=${document?.title}`);
return res.status(200).send(buffer);
}

View File

@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import short from "short-uuid";
import { Document as PrismaDocument, FieldType } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { FieldType, Document as PrismaDocument } from "@prisma/client";
import short from "short-uuid";
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res);

View File

@ -1,12 +1,8 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { Document as PrismaDocument, FieldType } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { FieldType, Document as PrismaDocument } from "@prisma/client";
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res);
@ -61,18 +57,14 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
});
if (!recipient || recipient?.documentId !== +documentId)
return res
.status(401)
.send("Recipient does not have access to this document.");
return res.status(401).send("Recipient does not have access to this document.");
}
if (user) {
const document: PrismaDocument = await getDocument(+documentId, req, res);
// todo entity ownerships checks
if (document.userId !== user.id) {
return res
.status(401)
.send("User does not have access to this document.");
return res.status(401).send("User does not have access to this document.");
}
}

View File

@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import short from "short-uuid";
import { Document as PrismaDocument } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { Document as PrismaDocument } from "@prisma/client";
import short from "short-uuid";
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res);

View File

@ -1,13 +1,9 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import short from "short-uuid";
import { Document as PrismaDocument } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { Document as PrismaDocument } from "@prisma/client";
import short from "short-uuid";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res);

View File

@ -1,63 +1,70 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { sendSigningRequest } from "@documenso/lib/mail";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { Document as PrismaDocument, SendStatus } from "@prisma/client";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res);
const { id: documentId } = req.query;
const { resendTo: resendTo = [] } = req.body;
try {
const user = await getUserFromToken(req, res);
const { id: documentId } = req.query;
const { resendTo: resendTo = [] } = req.body;
if (!user) return;
if (!user) {
return res.status(401).send("Unauthorized");
}
if (!documentId) {
res.status(400).send("Missing parameter documentId.");
return;
}
if (!documentId) {
return res.status(400).send("Missing parameter documentId.");
}
const document: PrismaDocument = await getDocument(+documentId, req, res);
const document: PrismaDocument = await getDocument(+documentId, req, res);
if (!document)
res.status(404).end(`No document with id ${documentId} found.`);
if (!document) {
res.status(404).end(`No document with id ${documentId} found.`);
}
let recipientCondition: any = {
documentId: +documentId,
sendStatus: SendStatus.NOT_SENT,
};
if (resendTo.length) {
recipientCondition = {
let recipientCondition: any = {
documentId: +documentId,
id: { in: resendTo },
sendStatus: SendStatus.NOT_SENT,
};
}
const recipients = await prisma.recipient.findMany({
where: {
...recipientCondition,
},
});
if (resendTo.length) {
recipientCondition = {
documentId: +documentId,
id: { in: resendTo },
};
}
if (!recipients.length) return res.status(200).send(recipients.length);
let sentRequests = 0;
recipients.forEach(async (recipient) => {
await sendSigningRequest(recipient, document, user).catch((err) => {
console.log(err);
return res.status(502).end("Coud not send request for signing.");
const recipients = await prisma.recipient.findMany({
where: {
...recipientCondition,
},
});
sentRequests++;
if (!recipients.length) {
return res.status(200).send(recipients.length);
}
let sentRequests = 0;
await Promise.all(
recipients.map(async (recipient) => {
await sendSigningRequest(recipient, document, user);
sentRequests++;
})
);
if (sentRequests === recipients.length) {
return res.status(200).send(recipients.length);
}
});
return res.status(502).end("Could not send request for signing.");
} catch (err) {
return res.status(502).end("Could not send request for signing.");
}
}
export default defaultHandler({

View File

@ -1,11 +1,11 @@
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { SigningStatus, DocumentStatus } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument, FieldType } from "@prisma/client";
import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
import { sendSigningDoneMail } from "@documenso/lib/mail";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import { insertImageInPDF, insertTextInPDF } from "@documenso/pdf";
import prisma from "@documenso/prisma";
import { DocumentStatus, SigningStatus } from "@prisma/client";
import { FieldType, Document as PrismaDocument } from "@prisma/client";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { token: recipientToken } = req.query;
@ -44,7 +44,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
for (const signature of signaturesFromBody) {
if (!signature.signatureImage && !signature.typedSignature) {
documentWithInserts = document.document;
throw new Error("Cant't save invalid signature.");
throw new Error("Can't save invalid signature.");
}
await saveSignature(signature);
@ -63,6 +63,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
data: {
signingStatus: SigningStatus.SIGNED,
signedAt: new Date(),
},
});
@ -73,13 +74,24 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
});
const signedRecipients = await prisma.recipient.findMany({
where: {
documentId: recipient.documentId,
signingStatus: SigningStatus.SIGNED,
},
});
// Don't check for inserted, because currently no "sign again" scenarios exist and
// this is probably the expected behaviour in unclean states.
const nonSignatureFields = await prisma.field.findMany({
where: {
documentId: document.id,
type: { in: [FieldType.DATE, FieldType.TEXT] },
recipientId: { in: signedRecipients.map((r) => r.id) },
},
include: {
Recipient: true,
}
});
// Insert fields other than signatures
@ -91,7 +103,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
month: "long",
day: "numeric",
year: "numeric",
}).format(new Date())
}).format(field.Recipient?.signedAt ?? new Date())
: field.customText || "",
field.positionX,
field.positionY,
@ -115,10 +127,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
},
data: {
document: documentWithInserts,
status:
unsignedRecipients.length > 0
? DocumentStatus.PENDING
: DocumentStatus.COMPLETED,
status: unsignedRecipients.length > 0 ? DocumentStatus.PENDING : DocumentStatus.COMPLETED,
},
});
@ -129,8 +138,11 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
});
document.document = documentWithInserts;
if (documentOwner)
await sendSigningDoneMail(recipient, document, documentOwner);
if (documentOwner) await sendSigningDoneMail(document, documentOwner);
for (const signer of signedRecipients) {
await sendSigningDoneMail(document, signer);
}
}
return res.status(200).end();
@ -139,9 +151,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
if (signedField?.Signature?.signatureImageAsBase64) {
documentWithInserts = await insertImageInPDF(
documentWithInserts,
signedField.Signature
? signedField.Signature?.signatureImageAsBase64
: "",
signedField.Signature ? signedField.Signature?.signatureImageAsBase64 : "",
signedField.positionX,
signedField.positionY,
signedField.page
@ -169,12 +179,8 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
create: {
recipientId: recipient.id,
fieldId: signature.fieldId,
signatureImageAsBase64: signature.signatureImage
? signature.signatureImage
: null,
typedSignature: signature.typedSignature
? signature.typedSignature
: null,
signatureImageAsBase64: signature.signatureImage ? signature.signatureImage : null,
typedSignature: signature.typedSignature ? signature.typedSignature : null,
},
});
}

View File

@ -1,9 +1,10 @@
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next";
import { getUserFromToken } from "@documenso/lib/server";
import formidable from "formidable";
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import { getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import formidable from "formidable";
import { isSubscribedServer } from "@documenso/lib/stripe";
export const config = {
api: {
@ -15,7 +16,17 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const form = formidable();
const user = await getUserFromToken(req, res);
if (!user) return;
if (!user) {
return res.status(401).end();
};
const isSubscribed = await isSubscribedServer(req);
if (!isSubscribed) {
throw new Error("User is not subscribed.");
}
form.parse(req, async (err, fields, files) => {
if (err) throw err;

View File

@ -1,5 +1,4 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";

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,8 +1,9 @@
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import { NextApiRequest, NextApiResponse } from "next";
import { Document as PrismaDocument } from "@prisma/client";
import { getDocument } from "@documenso/lib/query";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
import { Document as PrismaDocument } from "@prisma/client";
// todo remove before launch
@ -12,10 +13,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const signedDocument = await addDigitalSignature(document.document);
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Length", signedDocument.length);
res.setHeader(
"Content-Disposition",
`attachment; filename=${document.title}`
);
res.setHeader("Content-Disposition", `attachment; filename=${document.title}`);
return res.status(200).send(signedDocument);
}

View File

@ -1,11 +1,7 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import type { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@documenso/lib/auth";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { method, body } = req;
@ -29,6 +25,13 @@ async function patchHandler(req: NextApiRequest, res: NextApiResponse) {
if (!user) return;
const updatedUser = req.body;
let password: string | undefined = undefined;
if (typeof updatedUser.password === "string" && updatedUser.password.length >= 6) {
password = await hashPassword(updatedUser.password);
}
await prisma.user
.update({
where: {
@ -36,6 +39,7 @@ async function patchHandler(req: NextApiRequest, res: NextApiResponse) {
},
data: {
name: updatedUser.name,
password,
},
})
.then(() => {

View File

@ -1,11 +1,6 @@
import {
defaultHandler,
defaultResponder,
getUserFromToken,
} from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import type { NextApiRequest, NextApiResponse } from "next";
import { defaultHandler, defaultResponder, getUserFromToken } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res);

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

@ -1,7 +1,10 @@
import { ChangeEvent, ReactElement } from "react";
import Head from "next/head";
import { ReactElement } from "react";
import Layout from "../components/layout";
import Link from "next/link";
import { uploadDocument } from "@documenso/features";
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
import { getUserFromToken } from "@documenso/lib/server";
import Layout from "../components/layout";
import type { NextPageWithLayout } from "./_app";
import {
CheckBadgeIcon,
@ -9,23 +12,23 @@ import {
ExclamationTriangleIcon,
UsersIcon,
} from "@heroicons/react/24/outline";
import { uploadDocument } from "@documenso/features";
import {
DocumentStatus,
Document as PrismaDocument,
SendStatus,
SigningStatus,
Document as PrismaDocument,
} from "@prisma/client";
import { getUserFromToken } from "@documenso/lib/server";
import { getDocumentsForUserFromToken } from "@documenso/lib/query";
import { truncate } from "fs";
import { Tooltip as ReactTooltip } from "react-tooltip";
import { useSubscription } from "@documenso/lib/stripe";
type FormValues = {
document: File;
};
const DashboardPage: NextPageWithLayout = (props: any) => {
const { hasSubscription } = useSubscription();
const stats = [
{
name: "Draft",
@ -59,30 +62,30 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
Dashboard
</h1>
</header>
<dl className="grid gap-5 mt-8 md:grid-cols-3 ">
<dl className="mt-8 grid gap-5 md:grid-cols-3 ">
{stats.map((item) => (
<Link href={item.link} key={item.name}>
<div className="px-4 py-3 overflow-hidden bg-white rounded-lg shadow md:p-6 sm:py-5">
<dt className="text-sm font-medium text-gray-500 truncate ">
<div className="overflow-hidden rounded-lg bg-white px-4 py-3 shadow hover:shadow-lg duration-300 sm:py-5 md:p-6">
<dt className="truncate text-sm font-medium text-gray-700 ">
<item.icon
className="flex-shrink-0 inline w-5 h-5 mr-3 text-neon sm:w-6 sm:h-6"
aria-hidden="true"
></item.icon>
className="text-neon-600 mr-3 inline h-5 w-5 flex-shrink-0 sm:h-6 sm:w-6"
aria-hidden="true"></item.icon>
{item.name}
</dt>
<dd className="mt-1 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
<dd className="mt-3 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">
{getStat(item.name, props)}
</dd>
</div>
</Link>
))}
</dl>
<div className="mt-12">
<input
id="fileUploadHelper"
type="file"
accept="application/pdf"
onChange={(event: any) => {
onChange={(event: ChangeEvent) => {
uploadDocument(event);
}}
hidden
@ -90,27 +93,27 @@ const DashboardPage: NextPageWithLayout = (props: any) => {
</div>
<div
onClick={() => {
document?.getElementById("fileUploadHelper")?.click();
if (hasSubscription) {
document?.getElementById("fileUploadHelper")?.click();
}
}}
className="relative block w-full p-12 text-center border-2 border-gray-300 border-dashed rounded-lg cursor-pointer hover:border-neon focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
aria-disabled={!hasSubscription}
className="group hover:border-neon-600 duration-200 relative block w-full cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-12 text-center focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 aria-disabled:opacity-50 aria-disabled:pointer-events-none">
<svg
className="w-12 h-12 mx-auto text-gray-400"
className="mx-auto h-12 w-12 text-gray-400 group-hover:text-gray-700 duration-200"
stroke="currentColor"
fill="none"
viewBox="0 00 20 25"
aria-hidden="true"
>
aria-hidden="true">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/>
</svg>
<span
id="add_document"
className="mt-2 block text-sm font-medium text-neon"
>
<span id="add_document" className="text-gray-500 group-hover:text-neon-700 mt-2 block text-sm font-medium duration-200">
Add a new PDF document.
</span>
</div>
@ -147,9 +150,7 @@ export async function getServerSideProps(context: any) {
const documents: any[] = await getDocumentsForUserFromToken(context);
const drafts: PrismaDocument[] = documents.filter(
(d) => d.status === DocumentStatus.DRAFT
);
const drafts: PrismaDocument[] = documents.filter((d) => d.status === DocumentStatus.DRAFT);
const waiting: any[] = documents.filter(
(e) =>

View File

@ -1,7 +1,13 @@
import { ReactElement, useEffect, useState } from "react";
import { NextPageContext } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import { uploadDocument } from "@documenso/features";
import { deleteDocument, getDocuments } from "@documenso/lib/api";
import { useSubscription } from "@documenso/lib/stripe";
import { Button, IconButton, SelectBox } from "@documenso/ui";
import Layout from "../components/layout";
import type { NextPageWithLayout } from "./_app";
import Head from "next/head";
import {
ArrowDownTrayIcon,
CheckBadgeIcon,
@ -13,21 +19,23 @@ import {
PlusIcon,
TrashIcon,
} from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import { uploadDocument } from "@documenso/features";
import { DocumentStatus } from "@prisma/client";
import { Button, IconButton, SelectBox } from "@documenso/ui";
import { NextPageContext } from "next";
import { deleteDocument, getDocuments } from "@documenso/lib/api";
import { Tooltip as ReactTooltip } from "react-tooltip";
const DocumentsPage: NextPageWithLayout = (props: any) => {
const router = useRouter();
const { hasSubscription } = useSubscription();
const [documents, setDocuments]: any[] = useState([]);
const [filteredDocuments, setFilteredDocuments] = useState([]);
const [loading, setLoading] = useState(true);
const statusFilters = [
type statusFilterType = {
label: string;
value: DocumentStatus | "ALL";
};
const statusFilters: statusFilterType[] = [
{ label: "All", value: "ALL" },
{ label: "Draft", value: "DRAFT" },
{ label: "Waiting for others", value: "PENDING" },
@ -42,12 +50,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{ label: "Last 12 months", value: 366 },
];
const [selectedStatusFilter, setSelectedStatusFilter] = useState(
statusFilters[0]
);
const [selectedCreatedFilter, setSelectedCreatedFilter] = useState(
createdFilter[0]
);
const [selectedStatusFilter, setSelectedStatusFilter] = useState(statusFilters[0]);
const [selectedCreatedFilter, setSelectedCreatedFilter] = useState(createdFilter[0]);
const loadDocuments = async () => {
if (!documents.length) setLoading(true);
@ -62,9 +66,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
useEffect(() => {
loadDocuments().finally(() => {
setSelectedStatusFilter(
statusFilters.filter(
(status) => status.value === props.filter.toUpperCase()
)[0]
statusFilters.filter((status) => status.value === props.filter.toUpperCase())[0]
);
});
}, []);
@ -79,9 +81,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
function filterDocumentes(documents: []): any {
let filteredDocuments = documents.filter(
(d: any) =>
d.status === selectedStatusFilter.value ||
selectedStatusFilter.value === "ALL"
(d: any) => d.status === selectedStatusFilter.value || selectedStatusFilter.value === "ALL"
);
filteredDocuments = filteredDocuments.filter((document: any) =>
@ -91,6 +91,20 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
return filteredDocuments;
}
function handleStatusFilterChange(status: statusFilterType) {
router.replace(
{
pathname: router.pathname,
query: { filter: status.value },
},
undefined,
{
shallow: true, // Perform a shallow update, without reloading the page
}
);
setSelectedStatusFilter(status);
}
function wasXDaysAgoOrLess(documentDate: Date, lastXDays: number): boolean {
if (lastXDays < 0) return true;
@ -98,9 +112,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
const today: Date = new Date(); // Today's date
// Calculate the difference between the two dates in days
const diffInDays = Math.floor(
(today.getTime() - documentDate.getTime()) / millisecondsInDay
);
const diffInDays = Math.floor((today.getTime() - documentDate.getTime()) / millisecondsInDay);
console.log(diffInDays);
@ -114,7 +126,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<title>Documents | Documenso</title>
</Head>
<div className="px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center mt-10">
<div className="mt-10 sm:flex sm:items-center">
<div className="sm:flex-auto">
<header>
<h1 className="text-3xl font-bold leading-tight tracking-tight text-gray-900">
@ -125,36 +137,34 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<Button
icon={DocumentPlusIcon}
disabled={!hasSubscription}
onClick={() => {
document?.getElementById("fileUploadHelper")?.click();
}}
>
}}>
Add Document
</Button>
</div>
</div>
<div className="mt-3 mb-12">
<div className="w-fit block float-right ml-3 mt-7">
{filteredDocuments.length != 1
? filteredDocuments.length + " Documents"
: "1 Document"}
</div>
<div className="mt-3 mb-12 flex flex-wrap items-center justify-start gap-x-4 md:justify-end gap-y-4">
<SelectBox
className="w-1/4 block float-right"
className="block flex-1 md:flex-none md:w-1/4"
label="Status"
options={statusFilters}
value={selectedStatusFilter}
onChange={handleStatusFilterChange}
/>
<SelectBox
className="block flex-1 md:flex-none md:w-1/4"
label="Created"
options={createdFilter}
value={selectedCreatedFilter}
onChange={setSelectedCreatedFilter}
/>
<SelectBox
className="w-1/4 block float-right ml-3"
label="Status"
options={statusFilters}
value={selectedStatusFilter}
onChange={setSelectedStatusFilter}
/>
<div className="block w-fit pt-5">
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
</div>
</div>
<div className="mt-20 max-w-[1100px]" hidden={!loading}>
<div className="mt-8 max-w-[1100px]" hidden={!loading}>
<div className="ph-item">
<div className="ph-col-12">
<div className="ph-picture"></div>
@ -171,14 +181,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
</div>
</div>
</div>
<div
className="mt-28 flex flex-col"
hidden={!documents.length || loading}
>
<div className="mt-8 flex flex-col" hidden={!documents.length || loading}>
<div
className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8"
hidden={!documents.length || loading}
>
hidden={!documents.length || loading}>
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table className="min-w-full divide-y divide-gray-300">
@ -186,32 +192,25 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<tr>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Title
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Recipients
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Status
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Created
</th>
<th
scope="col"
className="relative py-3.5 pl-3 pr-4 sm:pr-6"
>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">Delete</span>
</th>
</tr>
@ -220,38 +219,30 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{filteredDocuments.map((document: any, index: number) => (
<tr
key={document.id}
className="hover:bg-gray-100 cursor-pointer"
onClick={(event) => showDocument(document.id)}
>
className="cursor-pointer hover:bg-gray-100"
onClick={(event) => showDocument(document.id)}>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{document.title || "#" + document.id}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<td className="inline-flex max-w-[250px] flex-wrap gap-x-2 gap-y-1 whitespace-nowrap py-3 text-sm text-gray-500">
{document.Recipient.map((item: any) => (
<div key={item.id}>
{item.sendStatus === "NOT_SENT" ? (
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
>
{item.name
? item.name + " <" + item.email + ">"
: item.email}
className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
{item.name ? item.name + " <" + item.email + ">" : item.email}
</span>
) : (
""
)}
{item.sendStatus === "SENT" &&
item.readStatus !== "OPENED" ? (
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
<span id="sent_icon">
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800"
>
<EnvelopeIcon className="inline h-5 mr-1"></EnvelopeIcon>
{item.name
? item.name + " <" + item.email + ">"
: item.email}
className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
<EnvelopeIcon className="mr-1 inline h-4"></EnvelopeIcon>
{item.name ? item.name + " <" + item.email + ">" : item.email}
</span>
</span>
) : (
@ -262,13 +253,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<span id="read_icon">
<span
id="sent_icon"
className="inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-green-800"
>
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
{item.name
? item.name + " <" + item.email + ">"
: item.email}
className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
<CheckIcon className="-mr-2 inline h-4"></CheckIcon>
<CheckIcon className="mr-1 inline h-4"></CheckIcon>
{item.name ? item.name + " <" + item.email + ">" : item.email}
</span>
</span>
) : (
@ -276,8 +264,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
)}
{item.signingStatus === "SIGNED" ? (
<span id="signed_icon">
<span className="inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>{" "}
<span className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>{" "}
{item.email}
</span>
</span>
@ -307,9 +295,8 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{formatDocumentStatus(document.status)}
<p>
<small hidden={document.Recipient.length === 0}>
{document.Recipient.filter(
(r: any) => r.signingStatus === "SIGNED"
).length || 0}
{document.Recipient.filter((r: any) => r.signingStatus === "SIGNED")
.length || 0}
/{document.Recipient.length || 0}
</small>
</p>
@ -327,6 +314,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
event.stopPropagation();
router.push("/documents/" + document.id);
}}
disabled={document.status === "COMPLETED"}
/>
<IconButton
icon={ArrowDownTrayIcon}
@ -342,30 +330,20 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
onClick={(event: any) => {
event.preventDefault();
event.stopPropagation();
if (
confirm(
"Are you sure you want to delete this document"
)
) {
if (confirm("Are you sure you want to delete this document")) {
const documentsWithoutIndex = [...documents];
const removedItem: any =
documentsWithoutIndex.splice(index, 1);
const removedItem: any = documentsWithoutIndex.splice(index, 1);
setDocuments(documentsWithoutIndex);
deleteDocument(document.id)
.catch((err) => {
documentsWithoutIndex.splice(
index,
0,
removedItem
);
documentsWithoutIndex.splice(index, 0, removedItem);
setDocuments(documentsWithoutIndex);
})
.then(() => {
loadDocuments();
});
}
}}
></IconButton>
}}></IconButton>
<span className="sr-only">, {document.name}</span>
</div>
</td>
@ -374,29 +352,21 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
</tbody>
</table>
</div>
<div
hidden={filteredDocuments.length > 0}
className="mx-auto w-fit mt-12 p-3"
>
<FunnelIcon className="w-5 inline mr-1 align-middle" /> Nothing
here. Maybe try a different filter.
<div hidden={filteredDocuments.length > 0} className="mx-auto mt-12 w-fit p-3">
<FunnelIcon className="mr-1 inline w-5 align-middle" /> Nothing here. Maybe try a
different filter.
</div>
</div>
</div>
</div>
</div>
<div
className="text-center mt-24"
id="empty"
hidden={documents.length > 0 || loading}
>
<div className="mt-24 text-center" id="empty" hidden={documents.length > 0 || loading}>
<svg
className="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
aria-hidden="true">
<path
strokeLinecap="round"
strokeLinejoin="round"
@ -411,10 +381,10 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
<div className="mt-6">
<Button
icon={PlusIcon}
disabled={!hasSubscription}
onClick={() => {
document?.getElementById("fileUploadHelper")?.click();
}}
>
}}>
Add Document
</Button>
<input

View File

@ -1,23 +1,21 @@
import { ReactElement } from "react";
import Layout from "../../../components/layout";
import { NextPageWithLayout } from "../../_app";
import Link from "next/link";
import { useRouter } from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
import { getUserFromToken } from "@documenso/lib/server";
import Link from "next/link";
import { DocumentStatus } from "@prisma/client";
import {
InformationCircleIcon,
PaperAirplaneIcon,
UsersIcon,
} from "@heroicons/react/24/outline";
import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument } from "@prisma/client";
import { Button, Breadcrumb } from "@documenso/ui";
import { getUserFromToken } from "@documenso/lib/server";
import { useSubscription } from "@documenso/lib/stripe";
import { Breadcrumb, Button } from "@documenso/ui";
import PDFEditor from "../../../components/editor/pdf-editor";
import Layout from "../../../components/layout";
import { NextPageWithLayout } from "../../_app";
import { InformationCircleIcon, PaperAirplaneIcon, UsersIcon } from "@heroicons/react/24/outline";
import { DocumentStatus } from "@prisma/client";
import { Document as PrismaDocument } from "@prisma/client";
const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
const router = useRouter();
const { hasSubscription } = useSubscription();
return (
<div className="mt-4">
@ -32,8 +30,7 @@ const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
},
{
title: props.document.title,
href:
NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
},
]}
/>
@ -67,21 +64,13 @@ const DocumentsDetailPage: NextPageWithLayout = (props: any) => {
<Button
icon={PaperAirplaneIcon}
className="ml-3"
href={
NEXT_PUBLIC_WEBAPP_URL +
"/documents/" +
props.document.id +
"/recipients"
}
href={NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients"}
onClick={() => {
if (
confirm(
`Send document out to ${props?.document?.Recipient?.length} recipients?`
)
confirm(`Send document out to ${props?.document?.Recipient?.length} recipients?`)
) {
}
}}
>
}}>
Prepare to Send
</Button>
</div>
@ -120,11 +109,7 @@ export async function getServerSideProps(context: any) {
const { id: documentId } = context.query;
try {
const document: PrismaDocument = await getDocument(
+documentId,
context.req,
context.res
);
const document: PrismaDocument = await getDocument(+documentId, context.req, context.res);
return {
props: {

View File

@ -1,32 +1,36 @@
import Head from "next/head";
import { ReactElement, useRef, useState } from "react";
import Head from "next/head";
import { NEXT_PUBLIC_WEBAPP_URL, classNames } from "@documenso/lib";
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
import { getDocument } from "@documenso/lib/query";
import { getUserFromToken } from "@documenso/lib/server";
import { Breadcrumb, Button, Dialog, IconButton, Tooltip } from "@documenso/ui";
import Layout from "../../../components/layout";
import { NextPageWithLayout } from "../../_app";
import { classNames, NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib";
import {
ArrowDownTrayIcon,
CheckBadgeIcon,
CheckIcon,
EnvelopeIcon,
PaperAirplaneIcon,
PencilSquareIcon,
TrashIcon,
UserPlusIcon,
EnvelopeIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { getUserFromToken } from "@documenso/lib/server";
import { getDocument } from "@documenso/lib/query";
import { Document as PrismaDocument, DocumentStatus } from "@prisma/client";
import { Breadcrumb, Button, Dialog, IconButton } from "@documenso/ui";
import { createOrUpdateRecipient, deleteRecipient, sendSigningRequests } from "@documenso/lib/api";
import { DocumentStatus, Document as PrismaDocument, Recipient } from "@prisma/client";
import { FormProvider, useFieldArray, useForm, useWatch } from "react-hook-form";
import { toast } from "react-hot-toast";
import { useSubscription } from "@documenso/lib/stripe";
export type FormValues = {
signers: { id: number; email: string; name: string }[];
signers: Array<Pick<Recipient, 'id' | 'email' | 'name' | 'sendStatus' | 'readStatus' | 'signingStatus'>>;
};
type FormSigner = FormValues["signers"][number];
const RecipientsPage: NextPageWithLayout = (props: any) => {
const { hasSubscription } = useSubscription();
const title: string = `"` + props?.document?.title + `"` + "Recipients | Documenso";
const breadcrumbItems = [
{
@ -35,7 +39,10 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
},
{
title: props.document.title,
href: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id,
href:
props.document.status !== DocumentStatus.COMPLETED
? NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id
: NEXT_PUBLIC_WEBAPP_URL + "/documents/" + props.document.id + "/recipients",
},
{
title: "Recipients",
@ -61,7 +68,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
});
const formValues = useWatch({ control, name: "signers" });
const cancelButtonRef = useRef(null);
const hasEmailError = (formValue: any): boolean => {
const hasEmailError = (formValue: FormSigner): boolean => {
const index = formValues.findIndex((e) => e.id === formValue.id);
return !!errors?.signers?.[index]?.email;
};
@ -71,80 +78,85 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
<Head>
<title>{title}</title>
</Head>
<div className="px-6 mt-10 sm:px-0">
<div className="mt-10 px-6 sm:px-0">
<div>
<Breadcrumb document={props.document} items={breadcrumbItems} />
</div>
<div className="mt-2 md:flex md:items-center md:justify-between">
<div className="flex-1 min-w-0">
<div className="min-w-0 flex-1">
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
{props.document.title}
</h2>
</div>
<div className="flex flex-shrink-0 mt-4 md:mt-0 md:ml-4">
<div className="mt-4 flex flex-shrink-0 md:mt-0 md:ml-4">
<Button
icon={ArrowDownTrayIcon}
color="secondary"
className="mr-2"
href={"/api/documents/" + props.document.id}
>
href={"/api/documents/" + props.document.id}>
Download
</Button>
<Button
icon={PencilSquareIcon}
disabled={props.document.status === DocumentStatus.COMPLETED}
color={props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"}
className="mr-2"
href={breadcrumbItems[1].href}
>
Edit Document
</Button>
<Button
className="min-w-[125px]"
color="primary"
icon={PaperAirplaneIcon}
onClick={() => {
setOpen(true);
}}
disabled={
(formValues.length || 0) === 0 ||
!formValues.some(
(r: any) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
) ||
loading
}
>
Send
</Button>
{props.document.status !== DocumentStatus.COMPLETED && (
<>
<Button
icon={PencilSquareIcon}
disabled={props.document.status === DocumentStatus.COMPLETED}
color={
props.document.status === DocumentStatus.COMPLETED ? "primary" : "secondary"
}
className="mr-2"
href={breadcrumbItems[1].href}>
Edit Document
</Button>
<Button
className="min-w-[125px]"
color="primary"
icon={PaperAirplaneIcon}
onClick={() => {
formValues.some((r) => r.email && hasEmailError(r))
? toast.error("Please enter a valid email address.", { id: "invalid email" })
: setOpen(true);
}}
disabled={
!hasSubscription ||
(formValues.length || 0) === 0 ||
!formValues.some(
(r) => r.email && !hasEmailError(r) && r.sendStatus === "NOT_SENT"
) ||
loading
}>
Send
</Button>
</>
)}
</div>
</div>
<div className="p-4 mt-10 overflow-hidden bg-white rounded-md shadow sm:p-6">
<div className="pb-3 border-b border-gray-200 sm:pb-5">
<div className="mt-10 overflow-hidden rounded-md bg-white p-4 shadow sm:p-6">
<div className="border-b border-gray-200 pb-3 sm:pb-5">
<h3 className="text-lg font-medium leading-6 text-gray-900 ">Signers</h3>
<p className="max-w-4xl mt-2 text-sm text-gray-500">
The people who will sign the document.
<p className="mt-2 max-w-4xl text-sm text-gray-500">
{props.document.status !== DocumentStatus.COMPLETED
? "The people who will sign the document."
: "The people who signed the document."}
</p>
</div>
<FormProvider {...form}>
<form
onChange={() => {
trigger();
}}
>
}}>
<ul role="list" className="divide-y divide-gray-200">
{fields.map((item: any, index: number) => (
{fields.map((item, index) => (
<li
key={index}
className="w-full px-2 py-3 border-0 hover:bg-green-50 group sm:py-4"
>
className="group w-full border-0 px-2 py-3 hover:bg-green-50 sm:py-4">
<div id="container" className="block w-full lg:flex lg:justify-between">
<div className="block space-y-2 md:space-x-2 md:space-y-0 md:flex">
<div className="block space-y-2 md:flex md:space-x-2 md:space-y-0">
<div
className={classNames(
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
"focus-within:border-neon focus-within:ring-neon rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:ring-1 md:w-[250px]",
item.sendStatus === "SENT" ? "bg-gray-100" : ""
)}
>
)}>
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
Email
</label>
@ -170,8 +182,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
documentId: props.document.id,
});
}}
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
placeholder="john.dorian@loremipsum.com"
className="block w-full border-0 bg-inherit p-0 text-gray-900 placeholder-gray-500 outline-none disabled:bg-neutral-100 sm:text-sm"
/>
{errors?.signers?.[index] ? (
<p className="mt-2 text-sm text-red-600" id="email-error">
@ -183,10 +194,9 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
</div>
<div
className={classNames(
"md:w-[250px] rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:border-neon focus-within:ring-1 focus-within:ring-neon",
"focus-within:border-neon focus-within:ring-neon rounded-md border border-gray-300 px-3 py-2 shadow-sm focus-within:ring-1 md:w-[250px]",
item.sendStatus === "SENT" ? "bg-gray-100" : ""
)}
>
)}>
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
Name (optional)
</label>
@ -209,121 +219,118 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
documentId: props.document.id,
});
}}
className="block w-full p-0 text-gray-900 placeholder-gray-500 disabled:bg-neutral-100 border-0 outline-none sm:text-sm bg-inherit"
placeholder="John Dorian"
className="block w-full border-0 bg-inherit p-0 text-gray-900 placeholder-gray-500 outline-none disabled:bg-neutral-100 sm:text-sm"
/>
</div>
</div>
<div className="flex items-center space-x-2 lg:ml-2">
<div className="flex mb-2 mr-2 lg:mr-0">
<div className="mb-2 mr-2 flex lg:mr-0">
<div key={item.id} className="space-x-2">
{item.sendStatus === "NOT_SENT" ? (
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
Not Sent
</span>
) : (
""
)}
) : null}
{item.sendStatus === "SENT" && item.readStatus !== "OPENED" ? (
<span id="sent_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800 "
>
<CheckIcon className="inline h-5 mr-1" /> Sent
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800 ">
<CheckIcon className="mr-1 inline h-5" /> Sent
</span>
</span>
) : (
""
)}
) : null}
{item.readStatus === "OPENED" && item.signingStatus === "NOT_SIGNED" ? (
<span id="read_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800"
>
<CheckIcon className="inline h-5 -mr-2"></CheckIcon>
<CheckIcon className="inline h-5 mr-1"></CheckIcon>
className="mt-3 inline-block flex-shrink-0 rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-gray-800">
<CheckIcon className="-mr-2 inline h-5"></CheckIcon>
<CheckIcon className="mr-1 inline h-5"></CheckIcon>
Seen
</span>
</span>
) : (
""
)}
) : null}
{item.signingStatus === "SIGNED" ? (
<span id="signed_icon">
<span
id="sent_icon"
className="inline-block mt-3 flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800"
>
<CheckBadgeIcon className="inline h-5 mr-1"></CheckBadgeIcon>
className="mt-3 inline-block flex-shrink-0 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>
Signed
</span>
</span>
) : (
""
)}
) : null}
</div>
</div>
<div className="flex mr-1">
<IconButton
icon={PaperAirplaneIcon}
disabled={
!item.id ||
item.sendStatus !== "SENT" ||
item.signingStatus === "SIGNED" ||
loading
}
color="secondary"
className="my-auto mr-4 h-9"
onClick={() => {
if (confirm("Resend this signing request?")) {
setLoading(true);
sendSigningRequests(props.document, [item.id]).finally(() => {
setLoading(false);
});
}
}}
>
Resend
</IconButton>
<IconButton
icon={TrashIcon}
disabled={!item.id || item.sendStatus === "SENT" || loading}
onClick={() => {
const removedItem = { ...fields }[index];
remove(index);
deleteRecipient(item)?.catch((err) => {
append(removedItem);
});
}}
className="group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</div>
{props.document.status !== DocumentStatus.COMPLETED && (
<div className="mr-1 flex">
<Tooltip label="Resend">
<IconButton
icon={PaperAirplaneIcon}
disabled={
!item.id ||
item.sendStatus !== "SENT" ||
item.signingStatus === "SIGNED" ||
loading
}
onClick={(event: any) => {
event.preventDefault();
event.stopPropagation();
if (confirm("Resend this signing request?")) {
setLoading(true);
sendSigningRequests(props.document, [item.id]).finally(() => {
setLoading(false);
});
}
}}
className="mx-1 group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</Tooltip>
<Tooltip label="Delete">
<IconButton
icon={TrashIcon}
disabled={!item.id || item.sendStatus === "SENT" || loading}
onClick={(event: any) => {
event.preventDefault();
event.stopPropagation();
if (confirm("Delete this signing request?")) {
const removedItem = { ...fields }[index];
remove(index);
deleteRecipient(item)?.catch((err) => {
append(removedItem);
});
}
}}
className="mx-1 group-hover:text-neon-dark group-hover:disabled:text-gray-400"
/>
</Tooltip>
</div>
)}
</div>
</div>
</li>
))}
</ul>
<Button
icon={UserPlusIcon}
className="mt-3"
onClick={() => {
createOrUpdateRecipient({
id: "",
email: "",
name: "",
documentId: props.document.id,
}).then((res) => {
append(res);
});
}}
>
Add Signer
</Button>
{props.document.status !== "COMPLETED" && (
<Button
icon={UserPlusIcon}
className="mt-3"
onClick={() => {
createOrUpdateRecipient({
id: "",
email: "",
name: "",
documentId: props.document.id,
}).then((res) => {
append(res);
});
}}>
Add Signer
</Button>
)}
</form>
</FormProvider>
</div>
@ -336,7 +343,7 @@ const RecipientsPage: NextPageWithLayout = (props: any) => {
open={open}
setLoading={setLoading}
setOpen={setOpen}
icon={<EnvelopeIcon className="w-6 h-6 text-green-600" aria-hidden="true" />}
icon={<EnvelopeIcon className="h-6 w-6 text-green-600" aria-hidden="true" />}
/>
</>
);

View File

@ -1,11 +1,11 @@
import prisma from "@documenso/prisma";
import Head from "next/head";
import { NextPageWithLayout } from "../../_app";
import { ReadStatus } from "@prisma/client";
import PDFSigner from "../../../components/editor/pdf-signer";
import Link from "next/link";
import prisma from "@documenso/prisma";
import PDFSigner from "../../../components/editor/pdf-signer";
import { NextPageWithLayout } from "../../_app";
import { ClockIcon } from "@heroicons/react/24/outline";
import { FieldType, DocumentStatus } from "@prisma/client";
import { ReadStatus } from "@prisma/client";
import { DocumentStatus, FieldType } from "@prisma/client";
const SignPage: NextPageWithLayout = (props: any) => {
return (
@ -14,36 +14,22 @@ const SignPage: NextPageWithLayout = (props: any) => {
<title>Sign | Documenso</title>
</Head>
{!props.expired ? (
<PDFSigner
document={props.document}
recipient={props.recipient}
fields={props.fields}
/>
<PDFSigner document={props.document} recipient={props.recipient} fields={props.fields} />
) : (
<>
<div className="mx-auto w-fit px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
<ClockIcon className="text-neon w-10 inline mr-1"></ClockIcon>
<h1 className="text-base font-medium text-neon inline align-middle">
Time flies.
</h1>
<p className="mt-2 text-4xl font-bold tracking-tight">
This signing link is expired.
</p>
<ClockIcon className="text-neon mr-1 inline w-10"></ClockIcon>
<h1 className="text-neon inline align-middle text-base font-medium">Time flies.</h1>
<p className="mt-2 text-4xl font-bold tracking-tight">This signing link is expired.</p>
<p className="mt-2 text-base text-gray-500">
Please ask{" "}
{props.document.User.name
? `${props.document.User.name}`
: `the sender`}{" "}
Please ask {props.document.User.name ? `${props.document.User.name}` : `the sender`}{" "}
to resend it.
</p>
<div className="mx-auto w-fit text-xl pt-20"></div>
<div className="mx-auto w-fit pt-20 text-xl"></div>
</div>
<div>
<div className="relative mx-96">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center"></div>
@ -51,10 +37,7 @@ const SignPage: NextPageWithLayout = (props: any) => {
</div>
<p className="mt-4 text-center text-sm text-gray-600">
Want to send of your own?{" "}
<Link
href="/signup?source=expired"
className="font-medium text-neon hover:text-neon"
>
<Link href="/signup?source=expired" className="text-neon hover:text-neon font-medium">
Create your own Account
</Link>
</p>
@ -76,7 +59,7 @@ export async function getServerSideProps(context: any) {
},
});
const recipient = await prisma.recipient.findFirstOrThrow({
const recipient = await prisma.recipient.findFirst({
where: {
token: recipientToken,
},
@ -85,12 +68,21 @@ export async function getServerSideProps(context: any) {
},
});
if (!recipient) {
return {
redirect: {
permanent: false,
destination: "/404",
},
};
}
// Document is already signed
if (recipient.Document.status === DocumentStatus.COMPLETED) {
return {
redirect: {
permanent: false,
destination: `/documents/${recipient.Document.id}/signed`,
destination: `/documents/${recipient.Document.id}/signed?token=${recipientToken}`,
},
};
}
@ -118,13 +110,9 @@ export async function getServerSideProps(context: any) {
return {
props: {
recipient: JSON.parse(JSON.stringify(recipient)),
document: JSON.parse(
JSON.stringify({ ...recipient.Document, document: "" })
),
document: JSON.parse(JSON.stringify({ ...recipient.Document, document: "" })),
fields: JSON.parse(JSON.stringify(unsignedFields)),
expired: recipient.expired
? new Date(recipient.expired) < new Date()
: false,
expired: recipient.expired ? new Date(recipient.expired) < new Date() : false,
},
};
}

View File

@ -1,10 +1,11 @@
import prisma from "@documenso/prisma";
import Head from "next/head";
import { NextPageWithLayout } from "../../_app";
import { ArrowDownTrayIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
import { Button, IconButton } from "@documenso/ui";
import Link from "next/link";
import { useRouter } from "next/router";
import prisma from "@documenso/prisma";
import { Button, IconButton } from "@documenso/ui";
import { NextPageWithLayout } from "../../_app";
import { ArrowDownTrayIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
import { truncate } from "@documenso/lib/helpers";
const Signed: NextPageWithLayout = (props: any) => {
const router = useRouter();
@ -18,29 +19,18 @@ const Signed: NextPageWithLayout = (props: any) => {
<title>Sign | Documenso</title>
</Head>
<div className="mx-auto w-fit px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
<CheckBadgeIcon className="text-neon w-10 inline mr-1"></CheckBadgeIcon>
<h1 className="text-base font-medium text-neon inline align-middle">
It's done!
</h1>
<CheckBadgeIcon className="text-neon mr-1 inline w-10"></CheckBadgeIcon>
<h1 className="text-neon inline align-middle text-base font-medium">It's done!</h1>
<p className="mt-2 text-4xl font-bold tracking-tight">
You signed "{props.document.title}"
You signed "{truncate(props.document.title)}"
</p>
<p
className="mt-2 text-base text-gray-500 max-w-sm"
hidden={allRecipientsSigned}
>
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={allRecipientsSigned}>
You will be notfied when all recipients have signed.
</p>
<p
className="mt-2 text-base text-gray-500 max-w-sm"
hidden={!allRecipientsSigned}
>
<p className="mt-2 max-w-sm text-base text-gray-500" hidden={!allRecipientsSigned}>
All recipients signed.
</p>
<div
className="mx-auto w-fit text-xl pt-20"
hidden={!allRecipientsSigned}
>
<div className="mx-auto w-fit pt-20 text-xl" hidden={!allRecipientsSigned}>
<Button
icon={ArrowDownTrayIcon}
color="secondary"
@ -48,23 +38,16 @@ const Signed: NextPageWithLayout = (props: any) => {
event.preventDefault();
event.stopPropagation();
router.push(
"/api/documents/" +
props.document.id +
"?token=" +
props.recipient.token
"/api/documents/" + props.document.id + "?token=" + props.recipient.token
);
}}
>
}}>
Download "{props.document.title}"
</Button>
</div>
</div>
<div>
<div className="relative mx-96">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center"></div>
@ -72,11 +55,8 @@ const Signed: NextPageWithLayout = (props: any) => {
</div>
<p className="mt-4 text-center text-sm text-gray-600">
Want to send slick signing links like this one?{" "}
<Link
href="https://documenso.com"
className="font-medium text-neon hover:text-neon"
>
Hosted Documenso is coming soon
<Link href="https://documenso.com" className="text-neon hover:text-neon font-medium">
Hosted Documenso is here!
</Link>
</p>
</>

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

@ -1,4 +1,5 @@
import Head from "next/head";
import { getUserFromToken } from "@documenso/lib/server";
import Login from "../components/login";
export default function LoginPage(props: any) {
@ -13,11 +14,21 @@ export default function LoginPage(props: any) {
}
export async function getServerSideProps(context: any) {
const ALLOW_SIGNUP = process.env.ALLOW_SIGNUP === "true";
const user = await getUserFromToken(context.req, context.res);
if (user)
return {
redirect: {
source: "/login",
destination: "/dashboard",
permanent: false,
},
};
const ALLOW_SIGNUP = process.env.NEXT_PUBLIC_ALLOW_SIGNUP === "true";
return {
props: {
ALLOW_SIGNUP: ALLOW_SIGNUP,
ALLOW_SIGNUP,
},
};
}

View File

@ -1,2 +1,3 @@
import SettingsPage from ".";
export default SettingsPage;

View File

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

View File

@ -1,2 +1,3 @@
import SettingsPage from ".";
export default SettingsPage;

View File

@ -1,2 +1,3 @@
import SettingsPage from ".";
export default SettingsPage;

View File

@ -1,5 +1,6 @@
import { NextPageContext } from "next";
import Head from "next/head";
import { getUserFromToken } from "@documenso/lib/server";
import Signup from "../components/signup";
export default function SignupPage(props: { source: string }) {
@ -14,7 +15,7 @@ export default function SignupPage(props: { source: string }) {
}
export async function getServerSideProps(context: any) {
if (process.env.ALLOW_SIGNUP !== "true")
if (process.env.NEXT_PUBLIC_ALLOW_SIGNUP !== "true")
return {
redirect: {
destination: "/login",
@ -22,6 +23,16 @@ export async function getServerSideProps(context: any) {
},
};
const user = await getUserFromToken(context.req, context.res);
if (user)
return {
redirect: {
source: "/signup",
destination: "/dashboard",
permanent: false,
},
};
const signupSource: string = context.query["source"];
return {
props: {

View File

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};

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,37 +6,3 @@
min-height: 100%;
}
html,
body,
:host {
font-family: montserrat;
}
@font-face {
font-family: "Qwigley";
src: url("/fonts/Qwigley-Regular.ttf");
}
/* latin */
@font-face {
font-family: "Montserrat";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/fonts/montserrat.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/* latin */
@font-face {
font-family: "Montserrat";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/fonts/montserrat.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}

View File

@ -12,13 +12,65 @@ module.exports = {
theme: {
extend: {
fontFamily: {
monteserrat: ["Monteserrat", "serif"],
qwigley: ["Qwigley", "serif"],
sans: ["var(--font-sans)", ...defaultTheme.fontFamily.sans],
qwigley: ["var(--font-qwigley)", "serif"],
},
colors: {
neon: "#37f095",
"neon-dark": "#2CC077",
brown: "#353434",
neon: {
DEFAULT: "#37F095",
50: "#E2FDF0",
100: "#CFFBE5",
200: "#A9F9D1",
300: "#83F6BD",
400: "#5DF3A9",
500: "#37F095",
600: "#11DE79",
700: "#0DAA5D",
800: "#097640",
900: "#054224",
950: "#032816",
},
"neon-dark": {
DEFAULT: "#2CC077",
50: "#B5EED2",
100: "#A5EAC8",
200: "#84E3B4",
300: "#62DBA0",
400: "#41D48B",
500: "#2CC077",
600: "#22925B",
700: "#17653E",
800: "#0D3722",
900: "#020906",
950: "#000000",
},
brown: {
DEFAULT: "#353434",
50: "#918F8F",
100: "#878585",
200: "#737171",
300: "#5E5C5C",
400: "#4A4848",
500: "#353434",
600: "#191818",
700: "#000000",
800: "#000000",
900: "#000000",
950: "#000000",
},
brand: {
DEFAULT: "#A2E771",
100: "#F4FCEE",
200: "#E8F9DC",
300: "#D1F3B9",
400: "#BBED96",
500: "#A2E771",
600: "#8DE151",
700: "#76DC2E",
800: "#63C021",
900: "#519D1B",
950: "#488C18",
},
},
borderRadius: {
"4xl": "2rem",

View File

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

50
docker/Dockerfile Normal file
View File

@ -0,0 +1,50 @@
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS production_deps
WORKDIR /app
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
# Copy our current monorepo
COPY . .
RUN npm ci --production
# Install dependencies only when needed
FROM base AS builder
WORKDIR /app
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
# Copy our current monorepo
COPY . .
RUN npm ci
RUN npm run build --workspaces
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=production_deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=production_deps --chown=nextjs:nodejs /app/package-lock.json ./package-lock.json
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/package.json ./package.json
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next ./.next
EXPOSE 3000
ENV PORT 3000
CMD ["npm", "run", "start"]

28
docker/build.sh Executable file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
command -v docker >/dev/null 2>&1 || {
echo "Docker is not running. Please start Docker and try again."
exit 1
}
command -v jq >/dev/null 2>&1 || {
echo "jq is not installed. Please install jq and try again."
exit 1
}
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
APP_VERSION="$(jq -r '.version' "$MONOREPO_ROOT/apps/web/package.json")"
GIT_SHA="$(git rev-parse HEAD)"
echo "Building docker image for monorepo at $MONOREPO_ROOT"
echo "App version: $APP_VERSION"
echo "Git SHA: $GIT_SHA"
docker build -f "$SCRIPT_DIR/Dockerfile" \
--progress=plain \
-t "documenso:latest" \
-t "documenso:$GIT_SHA" \
-t "documenso:$APP_VERSION" \
"$MONOREPO_ROOT"

12
docker/compose-entrypoint.sh Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
cd "$MONOREPO_ROOT"
npm ci
npm run db-migrate:dev
npm run dev

View File

@ -0,0 +1,18 @@
services:
database:
image: postgres:15
container_name: database
environment:
- POSTGRES_USER=documenso
- POSTGRES_PASSWORD=password
- POSTGRES_DB=documenso
ports:
- 54320:5432
inbucket:
image: inbucket/inbucket
container_name: mailserver
ports:
- 9000:9000
- 2500:2500
- 1100:1100

40
docker/compose.yml Normal file
View File

@ -0,0 +1,40 @@
services:
database:
image: postgres:15
environment:
- POSTGRES_USER=documenso
- POSTGRES_PASSWORD=password
- POSTGRES_DB=documenso
ports:
- 5432:5432
inbucket:
image: inbucket/inbucket
ports:
- 9000:9000
- 2500:2500
- 1100:1100
documenso:
image: node:18
working_dir: /app
command: ./docker/compose-entrypoint.sh
depends_on:
- database
- inbucket
environment:
- DATABASE_URL=postgres://documenso:password@database:5432/documenso
- NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
- NEXTAUTH_SECRET=my-super-secure-secret
- NEXTAUTH_URL=http://localhost:3000
- SENDGRID_API_KEY=
- SMTP_MAIL_HOST=inbucket
- SMTP_MAIL_PORT=2500
- SMTP_MAIL_USER=username
- SMTP_MAIL_PASSWORD=password
- MAIL_FROM=admin@example.com
- NEXT_PUBLIC_ALLOW_SIGNUP=true
ports:
- 3000:3000
volumes:
- ../:/app

9417
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,17 @@
"name": "documenso-monorepo",
"version": "0.0.0",
"scripts": {
"dev": "cd apps && cd web && next dev",
"build": "npm i && cd apps && cd web && npm i && next build",
"start": "cd apps && cd web && next start",
"dev": "turbo run dev --filter=web",
"build": "turbo run build --filter=web",
"start": "turbo run start --filter=web",
"db-migrate:dev": "prisma migrate dev",
"db-seed": "prisma db seed",
"db-studio": "prisma studio"
"db-studio": "prisma studio",
"docker:compose-up": "docker compose -p documenso -f ./docker/compose-without-app.yml up -d || docker-compose -p documenso -f ./docker/compose-without-app.yml up -d",
"docker:compose-down": "docker compose -p documenso -f ./docker/compose-without-app.yml down || docker-compose -p documenso -f ./docker/compose-without-app.yml down",
"stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook",
"dx": "npm install && run-s docker:compose-up db-migrate:dev",
"d": "npm install && run-s docker:compose-up db-migrate:dev && npm run db-seed && npm run dev"
},
"workspaces": [
"apps/*",
@ -21,27 +26,34 @@
"@documenso/prisma": "*",
"@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13",
"@tailwindcss/forms": "^0.5.3",
"@types/bcryptjs": "^2.4.2",
"@types/node": "18.11.9",
"@types/react-dom": "18.0.9",
"@types/react-signature-canvas": "^1.0.2",
"@hookform/resolvers": "^3.1.0",
"avatar-from-initials": "^1.0.3",
"bcryptjs": "^2.4.3",
"dotenv": "^16.0.3",
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"install": "^0.13.0",
"next": "13.0.3",
"next": "13.2.4",
"next-auth": ">=4.20.1",
"next-transpile-modules": "^10.0.0",
"npm": "^9.1.3",
"pdf-lib": "^1.17.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.41.5",
"react-hot-toast": "^2.4.0",
"react-signature-canvas": "^1.0.6",
"zod": "^3.21.4"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/bcryptjs": "^2.4.2",
"@types/node": "18.11.9",
"@types/react-dom": "18.0.9",
"@types/react-signature-canvas": "^1.0.2",
"dotenv": "^16.0.3",
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"next-transpile-modules": "^10.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.5",
"turbo": "^1.9.9",
"typescript": "4.8.4"
}
}

View File

@ -14,10 +14,8 @@ export const createField = (
if (newFieldX < 0) newFieldX = 0;
if (newFieldY < 0) newFieldY = 0;
if (newFieldX + fieldSize.width > rect.width)
newFieldX = rect.width - fieldSize.width;
if (newFieldY + fieldSize.height > rect.height)
newFieldY = rect.height - fieldSize.height;
if (newFieldX + fieldSize.width > rect.width) newFieldX = rect.width - fieldSize.width;
if (newFieldY + fieldSize.height > rect.height) newFieldY = rect.height - fieldSize.height;
const signatureField = {
id: -1,

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,9 +1,10 @@
import { ChangeEvent } from "react";
import router from "next/router";
import toast from "react-hot-toast";
import { NEXT_PUBLIC_WEBAPP_URL } from "../lib/constants";
import toast from "react-hot-toast";
export const uploadDocument = async (event: any) => {
if (event.target.files && event.target.files[0]) {
export const uploadDocument = async (event: ChangeEvent) => {
if (event.target instanceof HTMLInputElement && event.target?.files && event.target.files[0]) {
const body = new FormData();
const document = event.target.files[0];
const fileName: string = event.target.files[0].name;
@ -12,25 +13,31 @@ export const uploadDocument = async (event: any) => {
toast.error("Non-PDF documents are not supported yet.");
return;
}
body.append("document", document || "");
const response: any = await toast
.promise(
fetch("/api/documents", {
method: "POST",
body,
}),
{
loading: "Uploading document...",
success: `${fileName} uploaded successfully.`,
error: "Could not upload document :/",
await toast.promise(
fetch("/api/documents", {
method: "POST",
body,
}).then((response: Response) => {
if (!response.ok) {
throw new Error("Could not upload document");
}
)
.then((response: Response) => {
response.json().then((createdDocumentIdFromBody) => {
router.push(
`${NEXT_PUBLIC_WEBAPP_URL}/documents/${createdDocumentIdFromBody}/recipients`
);
});
});
}),
{
loading: "Uploading document...",
success: `${fileName} uploaded successfully.`,
error: "Could not upload document :/",
}
).catch((_err) => {
// Do nothing
});
}
};

View File

@ -6,16 +6,13 @@ export const deleteRecipient = (recipient: any) => {
}
return toast.promise(
fetch(
"/api/documents/" + recipient.documentId + "/recipients/" + recipient.id,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(recipient),
}
),
fetch("/api/documents/" + recipient.documentId + "/recipients/" + recipient.id, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(recipient),
}),
{
loading: "Deleting...",
success: "Deleted.",

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