Compare commits

..

79 Commits

Author SHA1 Message Date
Mythie
d3f82e1eb0 fix: code style updates and email wording changes 2023-06-17 11:44:34 +10:00
Ephraim Atta-Duncan
e3bc41934c Fixes from code review 2023-06-09 03:55:30 +00:00
Ephraim Atta-Duncan
13a840ff78 Password validation with zod 2023-06-07 12:33:33 +00:00
Ephraim Atta-Duncan
fe6561f596 Set reset token expiry to 24 hours 2023-06-07 11:02:50 +00:00
Ephraim Atta-Duncan
9cfbb1dec9 Avoid leaking that a user has an account 2023-06-07 10:59:20 +00:00
Ephraim Atta-Duncan
9dd8c2842c Match emails with regex 2023-06-07 10:44:07 +00:00
Ephraim Atta-Duncan
54a965e2b4 Remove unused props from components 2023-06-07 10:37:47 +00:00
Ephraim Atta-Duncan
7cc1ae2de0 Refactor forgot password and reset component 2023-06-07 10:33:05 +00:00
Ephraim Atta-Duncan
f08836216e Remove unused input fields 2023-06-07 10:12:05 +00:00
Ephraim Atta-Duncan
7184c47ac4 Rename component interfaces 2023-06-07 10:10:56 +00:00
Ephraim Atta-Duncan
79bd410687 Remove tokens on successful password reset 2023-06-05 17:15:41 +00:00
Ephraim Atta-Duncan
3a0648c85d Expire token after 1 hour 2023-06-05 16:54:12 +00:00
Ephraim Atta-Duncan
2b9a2ff250 Avoid user from setting the same old password 2023-06-05 16:36:16 +00:00
Ephraim Atta-Duncan
4136811e32 Avoid consecutive password reset requests 2023-06-05 16:01:01 +00:00
Ephraim Atta-Duncan
e9cee23c15 Error handling for invalid users 2023-06-05 15:52:00 +00:00
Ephraim Atta-Duncan
5d2349086d Send email on password reset complete 2023-06-05 15:33:27 +00:00
Ephraim Atta-Duncan
c47e01b2b8 Display sucessful password reset request 2023-06-05 14:59:50 +00:00
Ephraim Atta-Duncan
7c30ee0c3e Redirect to /login on password reset 2023-06-05 14:47:10 +00:00
Ephraim Atta-Duncan
6e2b05f835 Change password in database to new reset password 2023-06-05 14:36:20 +00:00
Ephraim Atta-Duncan
8dc9c9d72d Add reset password page 2023-06-05 14:17:45 +00:00
Ephraim Atta-Duncan
66b529a841 feat: send reset password email 2023-06-05 13:44:47 +00:00
Ephraim Atta-Duncan
8293b50195 Create reset password token for user 2023-06-05 13:05:25 +00:00
Ephraim Atta-Duncan
002b22b1a8 Add forgot password page 2023-06-05 12:53:51 +00:00
Ephraim Atta-Duncan
447bf0cb76 Add password reset to prisma schema 2023-06-05 12:23:52 +00:00
Lucas Smith
4e65ff3a47 Merge pull request #195 from PeerRich/patch-1
chore: fix readme Product Hunt Badges
2023-06-05 21:47:39 +10:00
Peer Richelsen
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
Lucas Smith
11c1b6841f Merge pull request #185 from ahiho/fix/ipv6
docs: update troubleshooting for IPv6
2023-06-03 00:44:04 +10:00
Thanh Vu
c41007e026 Revert "fix: support ipv6 for nextjs"
This reverts commit f9de6671e0aa29e25e872a80aa334d3319e3e522.
2023-06-02 18:04:52 +07:00
Thanh Vu
db99bf3674 Revert "fix: custom nextjs server"
This reverts commit 8f9a5f4ec7d834970a3e2b0778ce94218c997a8f.
2023-06-02 18:04:52 +07:00
Thanh Vu
3caa01ab53 Revert "fix: add custom nextjs server to docker"
This reverts commit 5dbe7b26286234db542921d9ded000c522c9a31e.
2023-06-02 18:04:52 +07:00
Thanh Vu
20b618c70f docs: update troubleshooting for IPv6 2023-06-02 18:04:52 +07:00
Thanh Vu
bbedd6d3de fix: add custom nextjs server to docker 2023-06-02 18:04:52 +07:00
Thanh Vu
054480500f fix: custom nextjs server 2023-06-02 18:04:52 +07:00
Thanh Vu
15b5f31a74 fix: support ipv6 for nextjs 2023-06-02 18:04:52 +07:00
Mythie
316fb49339 fix: disable subscriptions in example env 2023-06-02 19:03:59 +10:00
Lucas Smith
fc1b3be5ad Merge pull request #184 from The-Robin-Hood/bugfix/docker_script_update
docker script updated 🐳
2023-06-02 18:26:21 +10:00
Robinhood
20b51198b4 docker script updated 🐳 2023-06-01 22:24:58 +05:30
Lucas Smith
f80edf3f94 Merge pull request #181 from ahiho/fix/docker-image-typo
typo: documentso >> documenso
2023-06-02 00:06:28 +10:00
Lucas Smith
08faabc813 Merge pull request #182 from JonasPardon/patch-1
Fix typos in example env
2023-06-02 00:04:50 +10:00
Mythie
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
Jonas Pardon
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
Thanh Vu
dd4568b7fa typo: documentso >> documenso 2023-06-01 13:58:18 +07:00
Lucas Smith
893ab9bea5 Merge pull request #167 from dephraiim/fix/dashboard-logo
fix: dashboard logo
2023-05-30 23:29:57 +10:00
Lucas Smith
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
Ansari
0a5de18235 minor script fix 2023-05-30 15:43:58 +05:30
Mythie
b5e03359c1 fix: mark truncateTitle as optional 2023-05-30 20:11:23 +10:00
Lucas Smith
a266e4f9d4 Merge pull request #150 from The-Robin-Hood/bugfix/long_filename
Long filename fix 🗄
2023-05-30 20:04:19 +10:00
Lucas Smith
eccd9b5cd3 Merge branch 'main' into bugfix/long_filename 2023-05-30 20:03:19 +10:00
Lucas Smith
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
Lucas Smith
6048555e4a Merge branch 'main' into next 2023-05-30 19:55:31 +10:00
Lucas Smith
e33f31c483 Merge pull request #165 from doug-andrade/fix-typos
typo: /ressources to /resources
2023-05-30 19:54:32 +10:00
Lucas Smith
fe82e3c84f Merge pull request #171 from leerob/turbo
Add turborepo to monorepo.
2023-05-30 19:34:53 +10:00
Ansari
7684a49b7d Merge branch 'main' into bugfix/long_filename 2023-05-30 14:30:55 +05:30
Mythie
d8ad4b4b2b fix: add turbo dep and start command 2023-05-30 18:56:41 +10:00
Lucas Smith
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
Lucas Smith
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
Doug Andrade
307b0cc9d9 fixed height on login/signup pages 2023-05-30 02:36:43 -04:00
Doug Andrade
3e94491474 fixed next/font load on ALL pages and toast. 2023-05-30 02:17:34 -04:00
Doug Andrade
007fe44db8 loading fonts with next/font 2023-05-29 22:25:36 -04:00
Lee Robinson
1e6f65f92d Explicit deps 2023-05-29 19:46:24 -05:00
Piyush Maurya
82fbedf8e3 fix: deprecated type checks and imports from @prisma 2023-05-30 00:24:18 +05:30
Lee Robinson
2f3be1cfe5 Add turborepo to monorepo. 2023-05-29 10:38:24 -05:00
Mythie
8ecd5cf215 fix: respect truncate title prop 2023-05-29 18:47:54 +10:00
Lucas Smith
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
Ephraim Atta-Duncan
4c06b5e640 fix: logo only on dashboard 2023-05-29 08:01:12 +00:00
Doug Andrade
b477799d70 update: add new brand color as palette in tailwind config 2023-05-28 23:17:52 -04:00
Doug Andrade
b928993510 typo: /ressources >> /resources 2023-05-28 20:06:43 -04:00
Doug Andrade
ad4d844b4d remove: next-transpile-modules dependency 2023-05-28 19:53:44 -04:00
Doug Andrade
3444d7fd93 task: simplify next.config.js 2023-05-28 19:44:00 -04:00
Timur Ercan
3e220135be fix: readme format 2023-05-28 17:42:54 +02:00
Timur Ercan
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
Timur Ercan
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
Mythie
748f842115 fix: update prop typo, extract truncate method 2023-05-25 18:43:37 +10:00
Timur Ercan
4a2162478e Merge branch 'main' into bugfix/long_filename 2023-05-19 19:33:22 +02:00
Ansari
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
Ansari
8bf6594cf4 prop name changed 📛 2023-05-15 15:43:29 +05:30
Ansari
b6ff01ef86 Merge branch 'main' into bugfix/long_filename 2023-05-15 15:40:04 +05:30
Ansari
9b993c08f1 Truncate prop added 2023-05-15 15:39:37 +05:30
Ansari
051e681701 Long filename fix 🗄 2023-05-10 20:13:07 +05:30
43 changed files with 1006 additions and 372 deletions

View File

@@ -4,8 +4,8 @@
# Option 3: Use the provided dx setup (RECOMMENDED) # Option 3: Use the provided dx setup (RECOMMENDED)
# => postgres://documenso:password@127.0.0.1:54320/documenso # => postgres://documenso:password@127.0.0.1:54320/documenso
# #
# ⚠ WARNING: The test database can be resetted or taken offline at any point. # ⚠ WARNING: The test database can be reset or taken offline at any point.
# ⚠ WARNING: Please be aware that nothing written to the test databae is private. # ⚠ WARNING: Please be aware that nothing written to the test database is private.
DATABASE_URL='' DATABASE_URL=''
# URL # URL
@@ -51,4 +51,4 @@ NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
#FEATURE FLAGS #FEATURE FLAGS
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page. # Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
NEXT_PUBLIC_ALLOW_SIGNUP=true NEXT_PUBLIC_ALLOW_SIGNUP=true
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=true NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=false

3
.gitignore vendored
View File

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

View File

@@ -1,7 +1,4 @@
> <strong>We are launching TOMORROW on Product Hunt soon! Sign up to support the launch: </strong> <p align="center" style="margin-top: 120px">
> <center><a href="https://dub.sh/documenso-launch"><img src="https://img.shields.io/badge/Documenso%20on%20Product%20Hunt-Notify%20Me-orange" alt="Product Hunt"></a></center>
<p align="center" style="margin-top: 12px">
<a href="https://github.com/documenso/documenso.com"> <a href="https://github.com/documenso/documenso.com">
<img width="250px" src="https://github.com/documenso/documenso/assets/1309312/cd7823ec-4baa-40b9-be78-4acb3b1c73cb" alt="Documenso Logo"> <img width="250px" src="https://github.com/documenso/documenso/assets/1309312/cd7823ec-4baa-40b9-be78-4acb3b1c73cb" alt="Documenso Logo">
</a> </a>
@@ -59,6 +56,13 @@
Signing documents digitally is fast, easy and should be best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure. Signing documents digitally is fast, easy and should be best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure.
## 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 🎯 ## Community and Next Steps 🎯
The current project goal is to <b>[release a production ready version](https://github.com/documenso/documenso/milestone/1)</b> for self-hosting as soon as possible. If you want to help making that happen you can: The current project goal is to <b>[release a production ready version](https://github.com/documenso/documenso/milestone/1)</b> for self-hosting as soon as possible. If you want to help making that happen you can:
@@ -74,8 +78,6 @@ 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). - To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
# Tech # Tech
Documenso is built using awesome open source tech including: Documenso is built using awesome open source tech including:
@@ -119,7 +121,7 @@ Want to get up and running quickly? Follow these steps:
- This will spin up a postgres database and inbucket mail server in docker containers. - This will spin up a postgres database and inbucket mail server in docker containers.
- Run `npm run dev` in the root directory - Run `npm run dev` in the root directory
- Want it even faster? Just use - Want it even faster? Just use
```sh ```sh
npm run d npm run d
``` ```
@@ -153,10 +155,10 @@ Follow these steps to setup documenso on you local machine:
--- ---
- Optional: Seed the database using <code>npm run db-seed</code> to create a test user and document - 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: Upload and sign <code>apps/web/resources/example.pdf</code> manually to test your setup
- Optional: Create your own signing certificate - Optional: Create your own signing certificate
- A demo certificate is provided in `/app/web/ressources/certificate.p12` - A demo certificate is provided in `/app/web/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)**. - To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](#creating-your-own-signing-certificate)**.
## Updating ## Updating
@@ -181,7 +183,7 @@ For the digital signature of your documents you need a signing certificate in .p
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this: \ 3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this: \
<code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code> <code>openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt</code>
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**) 4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
5. Place the certificate <code>/apps/web/ressource/certificate.p12</code> 5. Place the certificate <code>/apps/web/resources/certificate.p12</code>
# Docker # Docker
@@ -196,3 +198,32 @@ Want to create a production ready docker image? Follow these steps:
- Docker support - Docker support
- One-Click-Deploy on Render.com Deploy - One-Click-Deploy on Render.com Deploy
# Troubleshooting
## 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

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

@@ -3,11 +3,11 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants"; import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { useSubscription } from "@documenso/lib/stripe"; import { useSubscription } from "@documenso/lib/stripe";
import { BillingWarning } from "./billing-warning";
import Navigation from "./navigation"; import Navigation from "./navigation";
import { PaperAirplaneIcon } from "@heroicons/react/24/outline"; import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
import { SubscriptionStatus } from "@prisma/client"; import { SubscriptionStatus } from "@prisma/client";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { BillingWarning } from "./billing-warning";
function useRedirectToLoginIfUnauthenticated() { function useRedirectToLoginIfUnauthenticated() {
const { data: session, status } = useSession(); const { data: session, status } = useSession();

View File

@@ -111,9 +111,11 @@ export default function Login(props: any) {
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-sm"> <div className="text-sm">
<a href="#" className="hover:text-neon-700 font-medium text-gray-500"> <Link
href="/forgot-password"
className="hover:text-neon-700 font-medium text-gray-500">
Forgot your password? Forgot your password?
</a> </Link>
</div> </div>
</div> </div>
<div> <div>

View File

@@ -116,7 +116,6 @@ export default function TopNavigation() {
href="/dashboard" href="/dashboard"
className="flex flex-shrink-0 items-center gap-x-2 self-center overflow-hidden"> className="flex flex-shrink-0 items-center gap-x-2 self-center overflow-hidden">
<Logo className="h-8 w-8" /> <Logo className="h-8 w-8" />
<h2 className="text-2xl font-semibold">Documenso</h2>
</Link> </Link>
<div className="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8"> <div className="hidden sm:-my-px sm:ml-6 sm:flex sm:space-x-8">

View File

@@ -0,0 +1,143 @@
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { Button } from "@documenso/ui";
import Logo from "./logo";
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import * as z from "zod";
const ZResetPasswordFormSchema = z
.object({
password: z.string().min(8, { message: "Password must be at least 8 characters" }),
confirmPassword: z.string().min(8, { message: "Password must be at least 8 characters" }),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Password don't match",
});
type TResetPasswordFormSchema = z.infer<typeof ZResetPasswordFormSchema>;
export default function ResetPassword() {
const router = useRouter();
const { token } = router.query;
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
} = useForm<TResetPasswordFormSchema>({
resolver: zodResolver(ZResetPasswordFormSchema),
});
const [resetSuccessful, setResetSuccessful] = useState(false);
const onSubmit = async ({ password }: TResetPasswordFormSchema) => {
const response = await toast.promise(
fetch(`/api/auth/reset-password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ password, token }),
}),
{
loading: "Resetting...",
success: `Reset password successful`,
error: "Could not reset password :/",
}
);
if (!response.ok) {
toast.dismiss();
const error = await response.json();
toast.error(error.message);
}
if (response.ok) {
setResetSuccessful(true);
setTimeout(() => {
router.push("/login");
}, 3000);
}
};
return (
<>
<div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-md space-y-8">
<div>
<Logo className="mx-auto h-20 w-auto"></Logo>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
Reset Password
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
{resetSuccessful ? "Your password has been reset." : "Please chose your new password"}
</p>
</div>
{!resetSuccessful && (
<form className="mt-8 space-y-6" onSubmit={handleSubmit(onSubmit)}>
<div className="-space-y-px rounded-md shadow-sm">
<div>
<label htmlFor="password" className="sr-only">
Password
</label>
<input
{...register("password", { required: "Password is required" })}
id="password"
name="password"
type="password"
autoComplete="current-password"
required
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="New password"
/>
</div>
<div>
<label htmlFor="confirmPassword" className="sr-only">
Password
</label>
<input
{...register("confirmPassword")}
id="confirmPassword"
name="confirmPassword"
type="password"
required
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
placeholder="Confirm new password"
/>
</div>
</div>
{errors && (
<span className="text-xs text-red-500">{errors.confirmPassword?.message}</span>
)}
<div>
<Button
type="submit"
disabled={isSubmitting}
className="group relative flex w-full">
Reset password
</Button>
</div>
</form>
)}
<div>
<Link href="/login">
<div className="relative mt-10 flex items-center justify-center gap-2 text-sm text-gray-500 hover:cursor-pointer hover:text-gray-900">
<ArrowLeftIcon className="h-4 w-4" />
Back to log in
</div>
</Link>
</div>
</div>
</div>
</>
);
}

View File

@@ -4,22 +4,15 @@ require("dotenv").config({ path: "../../.env" });
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
swcMinify: false, swcMinify: false,
transpilePackages: [
"@documenso/prisma",
"@documenso/lib",
"@documenso/ui",
"@documenso/pdf",
"@documenso/features",
"@documenso/signing",
"react-signature-canvas",
],
}; };
const transpileModules = require("next-transpile-modules")([ module.exports = nextConfig;
"@documenso/prisma",
"@documenso/lib",
"@documenso/ui",
"@documenso/pdf",
"@documenso/features",
"@documenso/signing",
"react-signature-canvas",
]);
const plugins = [
transpileModules
];
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
module.exports = moduleExports;

View File

@@ -56,11 +56,10 @@
"eslint": "8.27.0", "eslint": "8.27.0",
"eslint-config-next": "13.0.3", "eslint-config-next": "13.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"next-transpile-modules": "^10.0.0",
"postcss": "^8.4.19", "postcss": "^8.4.19",
"sass": "^1.57.1", "sass": "^1.57.1",
"stripe-cli": "^0.1.0", "stripe-cli": "^0.1.0",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.2.4",
"typescript": "4.8.4" "typescript": "4.8.4"
} }
} }

View File

@@ -1,6 +1,7 @@
import { ReactElement, ReactNode } from "react"; import { ReactElement, ReactNode } from "react";
import { NextPage } from "next"; import { NextPage } from "next";
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
import { Montserrat, Qwigley } from "next/font/google";
import { SubscriptionProvider } from "@documenso/lib/stripe/providers/subscription-provider"; import { SubscriptionProvider } from "@documenso/lib/stripe/providers/subscription-provider";
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss"; import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
import "../../../node_modules/react-resizable/css/styles.css"; import "../../../node_modules/react-resizable/css/styles.css";
@@ -11,6 +12,20 @@ import "react-tooltip/dist/react-tooltip.css";
export { coloredConsole } from "@documenso/lib"; 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> & { export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode; getLayout?: (page: ReactElement) => ReactNode;
}; };
@@ -27,8 +42,10 @@ export default function App({
return ( return (
<SessionProvider session={session}> <SessionProvider session={session}>
<SubscriptionProvider initialSubscription={initialSubscription}> <SubscriptionProvider initialSubscription={initialSubscription}>
<Toaster position="top-center" /> <main className={`${montserrat.variable} h-full font-sans`}>
{getLayout(<Component {...pageProps} />)} <Toaster position="top-center" />
{getLayout(<Component {...pageProps} />)}
</main>
</SubscriptionProvider> </SubscriptionProvider>
</SessionProvider> </SessionProvider>
); );

View File

@@ -0,0 +1,63 @@
import { NextApiRequest, NextApiResponse } from "next";
import { sendResetPassword } from "@documenso/lib/mail";
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
import crypto from "crypto";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { email } = req.body;
const cleanEmail = email.toLowerCase();
if (!cleanEmail || !/.+@.+/.test(cleanEmail)) {
res.status(400).json({ message: "Invalid email" });
return;
}
const user = await prisma.user.findFirst({
where: {
email: cleanEmail,
},
});
if (!user) {
return res.status(200).json({ message: "A password reset email has been sent." });
}
const existingToken = await prisma.passwordResetToken.findFirst({
where: {
userId: user.id,
createdAt: {
gte: new Date(Date.now() - 1000 * 60 * 60),
},
},
});
if (existingToken) {
return res.status(200).json({ message: "A password reset email has been sent." });
}
const token = crypto.randomBytes(64).toString("hex");
const expiry = new Date();
expiry.setHours(expiry.getHours() + 24); // Set expiry to one hour from now
let passwordResetToken;
try {
passwordResetToken = await prisma.passwordResetToken.create({
data: {
token,
expiry,
userId: user.id,
},
});
} catch (error) {
return res.status(500).json({ message: "Something went wrong" });
}
await sendResetPassword(user, passwordResetToken.token);
return res.status(200).json({ message: "A password reset email has been sent." });
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});

View File

@@ -0,0 +1,69 @@
import { NextApiRequest, NextApiResponse } from "next";
import { hashPassword, verifyPassword } from "@documenso/lib/auth";
import { sendResetPasswordSuccessMail } from "@documenso/lib/mail";
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { token, password } = req.body;
if (!token) {
res.status(400).json({ message: "Invalid token" });
return;
}
const foundToken = await prisma.passwordResetToken.findUnique({
where: {
token,
},
include: {
User: true,
},
});
if (!foundToken) {
return res.status(404).json({ message: "Invalid token." });
}
const now = new Date();
if (now > foundToken.expiry) {
return res.status(400).json({ message: "Token has expired" });
}
const isSamePassword = await verifyPassword(password, foundToken.User.password!);
if (isSamePassword) {
return res.status(400).json({ message: "New password must be different" });
}
const hashedPassword = await hashPassword(password);
const transaction = await prisma.$transaction([
prisma.user.update({
where: {
id: foundToken.userId,
},
data: {
password: hashedPassword,
},
}),
prisma.passwordResetToken.deleteMany({
where: {
userId: foundToken.userId,
},
}),
]);
if (!transaction) {
return res.status(500).json({ message: "Error resetting password." });
}
await sendResetPasswordSuccessMail(foundToken.User);
res.status(200).json({ message: "Password reset successful." });
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});

View File

@@ -8,13 +8,13 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { email, password, source } = req.body; const { email, password, source } = req.body;
const cleanEmail = email.toLowerCase(); const cleanEmail = email.toLowerCase();
if (!cleanEmail || !cleanEmail.includes("@")) { if (!cleanEmail || !/.+@.+/.test(cleanEmail)) {
res.status(422).json({ message: "Invalid email" }); res.status(400).json({ message: "Invalid email" });
return; return;
} }
if (!password || password.trim().length < 7) { 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.", message: "Password should be at least 7 characters long.",
}); });
} }

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

@@ -5,6 +5,7 @@ import prisma from "@documenso/prisma";
import { Button, IconButton } from "@documenso/ui"; import { Button, IconButton } from "@documenso/ui";
import { NextPageWithLayout } from "../../_app"; import { NextPageWithLayout } from "../../_app";
import { ArrowDownTrayIcon, CheckBadgeIcon } from "@heroicons/react/24/outline"; import { ArrowDownTrayIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
import { truncate } from "@documenso/lib/helpers";
const Signed: NextPageWithLayout = (props: any) => { const Signed: NextPageWithLayout = (props: any) => {
const router = useRouter(); const router = useRouter();
@@ -21,7 +22,7 @@ const Signed: NextPageWithLayout = (props: any) => {
<CheckBadgeIcon className="text-neon mr-1 inline w-10"></CheckBadgeIcon> <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> <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"> <p className="mt-2 text-4xl font-bold tracking-tight">
You signed "{props.document.title}" You signed "{truncate(props.document.title)}"
</p> </p>
<p className="mt-2 max-w-sm text-base text-gray-500" 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. You will be notfied when all recipients have signed.

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

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

View File

@@ -12,8 +12,8 @@ module.exports = {
theme: { theme: {
extend: { extend: {
fontFamily: { fontFamily: {
monteserrat: ["Monteserrat", "serif"], sans: ["var(--font-sans)", ...defaultTheme.fontFamily.sans],
qwigley: ["Qwigley", "serif"], qwigley: ["var(--font-qwigley)", "serif"],
}, },
colors: { colors: {
neon: { neon: {
@@ -58,6 +58,19 @@ module.exports = {
900: "#000000", 900: "#000000",
950: "#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: { borderRadius: {
"4xl": "2rem", "4xl": "2rem",

View File

@@ -22,7 +22,7 @@ echo "Git SHA: $GIT_SHA"
docker build -f "$SCRIPT_DIR/Dockerfile" \ docker build -f "$SCRIPT_DIR/Dockerfile" \
--progress=plain \ --progress=plain \
-t "documentso:latest" \ -t "documenso:latest" \
-t "documenso:$GIT_SHA" \ -t "documenso:$GIT_SHA" \
-t "documenso:$APP_VERSION" \ -t "documenso:$APP_VERSION" \
"$MONOREPO_ROOT" "$MONOREPO_ROOT"

438
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@documenso/prisma": "*", "@documenso/prisma": "*",
"@headlessui/react": "^1.7.4", "@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13", "@heroicons/react": "^2.0.13",
"@hookform/resolvers": "^3.1.0",
"avatar-from-initials": "^1.0.3", "avatar-from-initials": "^1.0.3",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"next": "13.2.4", "next": "13.2.4",
@@ -24,7 +25,8 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.41.5", "react-hook-form": "^7.41.5",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-signature-canvas": "^1.0.6" "react-signature-canvas": "^1.0.6",
"zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
@@ -40,6 +42,7 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.5", "prettier-plugin-tailwindcss": "^0.2.5",
"turbo": "^1.9.9",
"typescript": "4.8.4" "typescript": "4.8.4"
} }
}, },
@@ -92,7 +95,6 @@
"eslint": "8.27.0", "eslint": "8.27.0",
"eslint-config-next": "13.0.3", "eslint-config-next": "13.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"next-transpile-modules": "^10.0.0",
"postcss": "^8.4.19", "postcss": "^8.4.19",
"sass": "^1.57.1", "sass": "^1.57.1",
"stripe-cli": "^0.1.0", "stripe-cli": "^0.1.0",
@@ -525,6 +527,14 @@
"react": ">= 16" "react": ">= 16"
} }
}, },
"node_modules/@hookform/resolvers": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.1.0.tgz",
"integrity": "sha512-z0A8K+Nxq+f83Whm/ajlwE6VtQlp/yPHZnXw7XWVPIGm1Vx0QV8KThU3BpbBRfAZ7/dYqCKKBNnQh85BkmBKkA==",
"peerDependencies": {
"react-hook-form": "^7.0.0"
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.8", "version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
@@ -633,36 +643,6 @@
"glob": "7.1.7" "glob": "7.1.7"
} }
}, },
"node_modules/@next/swc-android-arm-eabi": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.4.tgz",
"integrity": "sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-android-arm64": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.2.4.tgz",
"integrity": "sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "13.2.4", "version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz",
@@ -678,156 +658,6 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@next/swc-darwin-x64": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.2.4.tgz",
"integrity": "sha512-a6LBuoYGcFOPGd4o8TPo7wmv5FnMr+Prz+vYHopEDuhDoMSHOnC+v+Ab4D7F0NMZkvQjEJQdJS3rqgFhlZmKlw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-freebsd-x64": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.4.tgz",
"integrity": "sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm-gnueabihf": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.4.tgz",
"integrity": "sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.2.4.tgz",
"integrity": "sha512-xzYZdAeq883MwXgcwc72hqo/F/dwUxCukpDOkx/j1HTq/J0wJthMGjinN9wH5bPR98Mfeh1MZJ91WWPnZOedOg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.2.4.tgz",
"integrity": "sha512-8rXr3WfmqSiYkb71qzuDP6I6R2T2tpkmf83elDN8z783N9nvTJf2E7eLx86wu2OJCi4T05nuxCsh4IOU3LQ5xw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.2.4.tgz",
"integrity": "sha512-Ngxh51zGSlYJ4EfpKG4LI6WfquulNdtmHg1yuOYlaAr33KyPJp4HeN/tivBnAHcZkoNy0hh/SbwDyCnz5PFJQQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.2.4.tgz",
"integrity": "sha512-gOvwIYoSxd+j14LOcvJr+ekd9fwYT1RyMAHOp7znA10+l40wkFiMONPLWiZuHxfRk+Dy7YdNdDh3ImumvL6VwA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.2.4.tgz",
"integrity": "sha512-q3NJzcfClgBm4HvdcnoEncmztxrA5GXqKeiZ/hADvC56pwNALt3ngDC6t6qr1YW9V/EPDxCYeaX4zYxHciW4Dw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.2.4.tgz",
"integrity": "sha512-/eZ5ncmHUYtD2fc6EUmAIZlAJnVT2YmxDsKs1Ourx0ttTtvtma/WKlMV5NoUsyOez0f9ExLyOpeCoz5aj+MPXw==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.2.4.tgz",
"integrity": "sha512-0MffFmyv7tBLlji01qc0IaPP/LVExzvj7/R5x1Jph1bTAIj4Vu81yFQWHHQAP6r4ff9Ukj1mBK6MDNVXm7Tcvw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -7665,6 +7495,102 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/turbo": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.1.tgz",
"integrity": "sha512-wq0YeSv6P/eEDXOL42jkMUr+T4z34dM8mdHu5u6C6OOAq8JuLJ72F/v4EVR1JmY8icyTkFz10ICLV0haUUYhbQ==",
"dev": true,
"hasInstallScript": true,
"bin": {
"turbo": "bin/turbo"
},
"optionalDependencies": {
"turbo-darwin-64": "1.10.1",
"turbo-darwin-arm64": "1.10.1",
"turbo-linux-64": "1.10.1",
"turbo-linux-arm64": "1.10.1",
"turbo-windows-64": "1.10.1",
"turbo-windows-arm64": "1.10.1"
}
},
"node_modules/turbo-darwin-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.10.1.tgz",
"integrity": "sha512-isLLoPuAOMNsYovOq9BhuQOZWQuU13zYsW988KkkaA4OJqOn7qwa9V/KBYCJL8uVQqtG+/Y42J37lO8RJjyXuA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/turbo-darwin-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.10.1.tgz",
"integrity": "sha512-x1nloPR10fLElNCv17BKr0kCx/O5gse/UXAcVscMZH2tvRUtXrdBmut62uw2YU3J9hli2fszYjUWXkulVpQvFA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/turbo-linux-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.10.1.tgz",
"integrity": "sha512-abV+ODCeOlz0503OZlHhPWdy3VwJZc1jObf1VQj7uQM+JqJ/kXbMyqJIMQVz+m7QJUFdferYPRxGhYT/NbYK7Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/turbo-linux-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.10.1.tgz",
"integrity": "sha512-zRC3nZbHQ63tofOmbuySzEn1ROISWTkemYYr1L98rpmT5aVa0kERlGiYcfDwZh3cBso/Ylg/wxexRAaPzcCJYQ==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/turbo-windows-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.10.1.tgz",
"integrity": "sha512-Irqz8IU+o7Q/5V44qatZBTunk+FQAOII1hZTsEU54ah62f9Y297K6/LSp+yncmVQOZlFVccXb6MDqcETExIQtA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/turbo-windows-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.10.1.tgz",
"integrity": "sha512-124IT15d2gyjC+NEn11pHOaVFvZDRHpxfF+LDUzV7YxfNIfV0mGkR3R/IyVXtQHOgqOdtQTbC4y411sm31+SEw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/tweetnacl": { "node_modules/tweetnacl": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@@ -8132,6 +8058,14 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/zod": {
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"packages/features": { "packages/features": {
"name": "@documenso/features", "name": "@documenso/features",
"version": "0.0.0" "version": "0.0.0"
@@ -8517,7 +8451,6 @@
"formidable": "^3.2.5", "formidable": "^3.2.5",
"next": "13.2.4", "next": "13.2.4",
"next-auth": "^4.22.0", "next-auth": "^4.22.0",
"next-transpile-modules": "^10.0.0",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"node-signpdf": "^1.5.0", "node-signpdf": "^1.5.0",
"nodemailer": "^6.9.0", "nodemailer": "^6.9.0",
@@ -8592,6 +8525,12 @@
"integrity": "sha512-x89rFxH3SRdYaA+JCXwfe+RkE1SFTo9GcOkZettHer71Y3T7V+ogKmfw5CjTazgS3d0ClJ7p1NA+SP7VQLQcLw==", "integrity": "sha512-x89rFxH3SRdYaA+JCXwfe+RkE1SFTo9GcOkZettHer71Y3T7V+ogKmfw5CjTazgS3d0ClJ7p1NA+SP7VQLQcLw==",
"requires": {} "requires": {}
}, },
"@hookform/resolvers": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.1.0.tgz",
"integrity": "sha512-z0A8K+Nxq+f83Whm/ajlwE6VtQlp/yPHZnXw7XWVPIGm1Vx0QV8KThU3BpbBRfAZ7/dYqCKKBNnQh85BkmBKkA==",
"requires": {}
},
"@humanwhocodes/config-array": { "@humanwhocodes/config-array": {
"version": "0.11.8", "version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
@@ -8681,84 +8620,12 @@
"glob": "7.1.7" "glob": "7.1.7"
} }
}, },
"@next/swc-android-arm-eabi": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.4.tgz",
"integrity": "sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==",
"optional": true
},
"@next/swc-android-arm64": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-13.2.4.tgz",
"integrity": "sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==",
"optional": true
},
"@next/swc-darwin-arm64": { "@next/swc-darwin-arm64": {
"version": "13.2.4", "version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz",
"integrity": "sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A==", "integrity": "sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A==",
"optional": true "optional": true
}, },
"@next/swc-darwin-x64": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.2.4.tgz",
"integrity": "sha512-a6LBuoYGcFOPGd4o8TPo7wmv5FnMr+Prz+vYHopEDuhDoMSHOnC+v+Ab4D7F0NMZkvQjEJQdJS3rqgFhlZmKlw==",
"optional": true
},
"@next/swc-freebsd-x64": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.4.tgz",
"integrity": "sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==",
"optional": true
},
"@next/swc-linux-arm-gnueabihf": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.4.tgz",
"integrity": "sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==",
"optional": true
},
"@next/swc-linux-arm64-gnu": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.2.4.tgz",
"integrity": "sha512-xzYZdAeq883MwXgcwc72hqo/F/dwUxCukpDOkx/j1HTq/J0wJthMGjinN9wH5bPR98Mfeh1MZJ91WWPnZOedOg==",
"optional": true
},
"@next/swc-linux-arm64-musl": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.2.4.tgz",
"integrity": "sha512-8rXr3WfmqSiYkb71qzuDP6I6R2T2tpkmf83elDN8z783N9nvTJf2E7eLx86wu2OJCi4T05nuxCsh4IOU3LQ5xw==",
"optional": true
},
"@next/swc-linux-x64-gnu": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.2.4.tgz",
"integrity": "sha512-Ngxh51zGSlYJ4EfpKG4LI6WfquulNdtmHg1yuOYlaAr33KyPJp4HeN/tivBnAHcZkoNy0hh/SbwDyCnz5PFJQQ==",
"optional": true
},
"@next/swc-linux-x64-musl": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.2.4.tgz",
"integrity": "sha512-gOvwIYoSxd+j14LOcvJr+ekd9fwYT1RyMAHOp7znA10+l40wkFiMONPLWiZuHxfRk+Dy7YdNdDh3ImumvL6VwA==",
"optional": true
},
"@next/swc-win32-arm64-msvc": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.2.4.tgz",
"integrity": "sha512-q3NJzcfClgBm4HvdcnoEncmztxrA5GXqKeiZ/hADvC56pwNALt3ngDC6t6qr1YW9V/EPDxCYeaX4zYxHciW4Dw==",
"optional": true
},
"@next/swc-win32-ia32-msvc": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.2.4.tgz",
"integrity": "sha512-/eZ5ncmHUYtD2fc6EUmAIZlAJnVT2YmxDsKs1Ourx0ttTtvtma/WKlMV5NoUsyOez0f9ExLyOpeCoz5aj+MPXw==",
"optional": true
},
"@next/swc-win32-x64-msvc": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.2.4.tgz",
"integrity": "sha512-0MffFmyv7tBLlji01qc0IaPP/LVExzvj7/R5x1Jph1bTAIj4Vu81yFQWHHQAP6r4ff9Ukj1mBK6MDNVXm7Tcvw==",
"optional": true
},
"@nodelib/fs.scandir": { "@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -13859,6 +13726,62 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"turbo": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.1.tgz",
"integrity": "sha512-wq0YeSv6P/eEDXOL42jkMUr+T4z34dM8mdHu5u6C6OOAq8JuLJ72F/v4EVR1JmY8icyTkFz10ICLV0haUUYhbQ==",
"dev": true,
"requires": {
"turbo-darwin-64": "1.10.1",
"turbo-darwin-arm64": "1.10.1",
"turbo-linux-64": "1.10.1",
"turbo-linux-arm64": "1.10.1",
"turbo-windows-64": "1.10.1",
"turbo-windows-arm64": "1.10.1"
}
},
"turbo-darwin-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.10.1.tgz",
"integrity": "sha512-isLLoPuAOMNsYovOq9BhuQOZWQuU13zYsW988KkkaA4OJqOn7qwa9V/KBYCJL8uVQqtG+/Y42J37lO8RJjyXuA==",
"dev": true,
"optional": true
},
"turbo-darwin-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.10.1.tgz",
"integrity": "sha512-x1nloPR10fLElNCv17BKr0kCx/O5gse/UXAcVscMZH2tvRUtXrdBmut62uw2YU3J9hli2fszYjUWXkulVpQvFA==",
"dev": true,
"optional": true
},
"turbo-linux-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.10.1.tgz",
"integrity": "sha512-abV+ODCeOlz0503OZlHhPWdy3VwJZc1jObf1VQj7uQM+JqJ/kXbMyqJIMQVz+m7QJUFdferYPRxGhYT/NbYK7Q==",
"dev": true,
"optional": true
},
"turbo-linux-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.10.1.tgz",
"integrity": "sha512-zRC3nZbHQ63tofOmbuySzEn1ROISWTkemYYr1L98rpmT5aVa0kERlGiYcfDwZh3cBso/Ylg/wxexRAaPzcCJYQ==",
"dev": true,
"optional": true
},
"turbo-windows-64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.10.1.tgz",
"integrity": "sha512-Irqz8IU+o7Q/5V44qatZBTunk+FQAOII1hZTsEU54ah62f9Y297K6/LSp+yncmVQOZlFVccXb6MDqcETExIQtA==",
"dev": true,
"optional": true
},
"turbo-windows-arm64": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.10.1.tgz",
"integrity": "sha512-124IT15d2gyjC+NEn11pHOaVFvZDRHpxfF+LDUzV7YxfNIfV0mGkR3R/IyVXtQHOgqOdtQTbC4y411sm31+SEw==",
"dev": true,
"optional": true
},
"tweetnacl": { "tweetnacl": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@@ -14198,6 +14121,11 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true "dev": true
},
"zod": {
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw=="
} }
} }
} }

View File

@@ -2,15 +2,14 @@
"name": "documenso-monorepo", "name": "documenso-monorepo",
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"dev": "npm run dev -w apps/web", "dev": "turbo run dev --filter=web",
"build": "npm i && cd apps && cd web && npm i && next build", "build": "turbo run build --filter=web",
"start": "cd apps && cd web && next start", "start": "turbo run start --filter=web",
"db-migrate:dev": "prisma migrate dev", "db-migrate:dev": "prisma migrate dev",
"db-seed": "prisma db seed", "db-seed": "prisma db seed",
"db-studio": "prisma studio", "db-studio": "prisma studio",
"docker:compose": "docker-compose -f ./docker/compose-without-app.yml", "docker:compose-up": "docker compose -f ./docker/compose-without-app.yml up -d || docker-compose -f ./docker/compose-without-app.yml up -d",
"docker:compose-up": "npm run docker:compose -- up -d", "docker:compose-down": "docker compose -f ./docker/compose-without-app.yml down || docker-compose -f ./docker/compose-without-app.yml down",
"docker:compose-down": "npm run docker:compose -- down",
"stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook", "stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook",
"dx": "npm install && run-s docker:compose-up db-migrate:dev", "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" "d": "npm install && run-s docker:compose-up db-migrate:dev && npm run db-seed && npm run dev"
@@ -27,6 +26,7 @@
"@documenso/prisma": "*", "@documenso/prisma": "*",
"@headlessui/react": "^1.7.4", "@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13", "@heroicons/react": "^2.0.13",
"@hookform/resolvers": "^3.1.0",
"avatar-from-initials": "^1.0.3", "avatar-from-initials": "^1.0.3",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"next": "13.2.4", "next": "13.2.4",
@@ -36,7 +36,8 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.41.5", "react-hook-form": "^7.41.5",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-signature-canvas": "^1.0.6" "react-signature-canvas": "^1.0.6",
"zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
@@ -52,6 +53,7 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.5", "prettier-plugin-tailwindcss": "^0.2.5",
"turbo": "^1.9.9",
"typescript": "4.8.4" "typescript": "4.8.4"
} }
} }

View File

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

View File

@@ -0,0 +1,13 @@
/**
* Truncates a title to a given max length substituting the middle with an ellipsis.
*/
export const truncate = (str: string, maxLength: number = 20) => {
if (str.length <= maxLength) {
return str;
}
const startLength = Math.ceil((maxLength - 3) / 2);
const endLength = Math.floor((maxLength - 3) / 2);
return `${str.slice(0, startLength)}...${str.slice(-endLength)}`;
};

View File

@@ -1,10 +1,9 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants"; import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { Document as PrismaDocument } from "@prisma/client";
export const baseEmailTemplate = (message: string, content: string) => { export const baseEmailTemplate = (message: string, content: string) => {
const html = ` const html = `
<div style="background-color: #eaeaea; padding: 2%;"> <div style="background-color: #eaeaea; padding: 2%;">
<div style="text-align:center; margin: auto; font-size: 14px; font-color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px"> <div style="text-align:center; margin: auto; font-size: 14px; color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
<img src="${NEXT_PUBLIC_WEBAPP_URL}/logo_h.png" alt="Documenso Logo" style="width: 180px; display: block; margin: auto; margin-bottom: 14px;"> <img src="${NEXT_PUBLIC_WEBAPP_URL}/logo_h.png" alt="Documenso Logo" style="width: 180px; display: block; margin: auto; margin-bottom: 14px;">
${message} ${message}
${content} ${content}

View File

@@ -2,3 +2,7 @@ export { signingRequestTemplate } from "./signingRequestTemplate";
export { signingCompleteTemplate } from "./signingCompleteTemplate"; export { signingCompleteTemplate } from "./signingCompleteTemplate";
export { sendSigningRequest as sendSigningRequest } from "./sendSigningRequest"; export { sendSigningRequest as sendSigningRequest } from "./sendSigningRequest";
export { sendSigningDoneMail } from "./sendSigningDoneMail"; export { sendSigningDoneMail } from "./sendSigningDoneMail";
export { resetPasswordTemplate } from "./resetPasswordTemplate";
export { sendResetPassword } from "./sendResetPassword";
export { resetPasswordSuccessTemplate } from "./resetPasswordSuccessTemplate";
export { sendResetPasswordSuccessMail } from "./sendResetPasswordSuccessMail";

View File

@@ -0,0 +1,51 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { User } from "@prisma/client";
export const resetPasswordSuccessTemplate = (user: User) => {
return `
<div style="background-color: #eaeaea; padding: 2%;">
<div
style="text-align:left; margin: auto; font-size: 14px; color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
<img src="${NEXT_PUBLIC_WEBAPP_URL}/logo_h.png" alt="Documenso Logo"
style="width: 180px; display: block; margin-bottom: 14px;" />
<h2 style="text-align: left; margin-top: 20px; font-size: 24px; font-weight: bold">Password updated!</h2>
<p style="margin-top: 15px">
Hi ${user.name ? user.name : user.email},
</p>
<p style="margin-top: 15px">
We've changed your password as you asked. You can now sign in with your new password.
</p>
<p style="margin-top: 15px">
Didn't request a password change? We are here to help you secure your account, just <a href="https://documenso.com">contact us</a>.
</p>
<p style="margin-top: 15px">
<p style="font-weight: bold">
The Documenso Team
</p>
</p>
<p style="text-align:left; margin-top: 30px">
<small>Want to send you own signing links?
<a href="https://documenso.com">Hosted Documenso is here!</a>.</small>
</p>
</div>
</div>
<div style="text-align: left; line-height: 18px; color: #666666; margin: 24px">
<div style="margin-top: 12px">
<b>Need help?</b>
<br>
Contact us at <a href="mailto:hi@documenso.com">hi@documenso.com</a>
</div>
<hr size="1" style="height: 1px; border: none; color: #D8D8D8; background-color: #D8D8D8">
<div style="text-align: center">
<small>Easy and beautiful document signing by Documenso.</small>
</div>
</div>
`;
};
export default resetPasswordSuccessTemplate;

View File

@@ -0,0 +1,46 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
export const resetPasswordTemplate = (ctaLink: string, ctaLabel: string) => {
const customContent = `
<h2 style="margin-top: 36px; font-size: 24px; font-weight: bold;">Forgot your password?</h2>
<p style="margin-top: 8px;">
That's okay, it happens! Click the button below to reset your password.
</p>
<p style="margin: 30px 0px; text-align: center">
<a href="${ctaLink}" style="background-color: #37f095; white-space: nowrap; color: white; border-color: transparent; border-width: 1px; border-radius: 0.375rem; font-size: 18px; padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; text-decoration: none; margin-top: 4px; margin-bottom: 4px;">
${ctaLabel}
</a>
</p>
<p style="margin-top: 20px;">
<small>Want to send you own signing links? <a href="https://documenso.com">Hosted Documenso is here!</a>.</small>
</p>`;
const html = `
<div style="background-color: #eaeaea; padding: 2%;">
<div
style="text-align:center; margin: auto; font-size: 14px; color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
<img src="${NEXT_PUBLIC_WEBAPP_URL}/logo_h.png" alt="Documenso Logo"
style="width: 180px; display: block; margin: auto; margin-bottom: 14px;" />
${customContent}
</div>
</div>
`;
const footer = `
<div style="text-align: left; line-height: 18px; color: #666666; margin: 24px">
<div style="margin-top: 12px">
<b>Need help?</b>
<br>
Contact us at <a href="mailto:hi@documenso.com">hi@documenso.com</a>
</div>
<hr size="1" style="height: 1px; border: none; color: #D8D8D8; background-color: #D8D8D8">
<div style="text-align: center">
<small>Easy and beautiful document signing by Documenso.</small>
</div>
</div>`;
return html + footer;
};
export default resetPasswordTemplate;

View File

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

View File

@@ -0,0 +1,14 @@
import { resetPasswordTemplate } from "@documenso/lib/mail";
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { sendMail } from "./sendMail";
import { User } from "@prisma/client";
export const sendResetPassword = async (user: User, token: string) => {
await sendMail(
user.email,
"Forgot password?",
resetPasswordTemplate(`${NEXT_PUBLIC_WEBAPP_URL}/auth/reset/${token}`, "Reset Your Password")
).catch((err) => {
throw err;
});
};

View File

@@ -0,0 +1,11 @@
import resetPasswordSuccessTemplate from "./resetPasswordSuccessTemplate";
import { sendMail } from "./sendMail";
import { User } from "@prisma/client";
export const sendResetPasswordSuccessMail = async (user: User) => {
await sendMail(user.email, "Password Reset Success!", resetPasswordSuccessTemplate(user)).catch(
(err) => {
throw err;
}
);
};

View File

@@ -1,6 +1,5 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants"; import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { baseEmailTemplate } from "./baseTemplate"; import { baseEmailTemplate } from "./baseTemplate";
import { Document as PrismaDocument } from "@prisma/client";
export const signingCompleteTemplate = (message: string) => { export const signingCompleteTemplate = (message: string) => {
const customContent = ` const customContent = `

View File

@@ -1,5 +1,5 @@
import { HttpError } from "@documenso/lib/server"; import { HttpError } from "@documenso/lib/server";
import { NotFoundError, PrismaClientKnownRequestError } from "@prisma/client/runtime"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
export function getServerErrorFromUnknown(cause: unknown): HttpError { export function getServerErrorFromUnknown(cause: unknown): HttpError {
// Error was manually thrown and does not need to be parsed. // Error was manually thrown and does not need to be parsed.
@@ -18,7 +18,7 @@ export function getServerErrorFromUnknown(cause: unknown): HttpError {
return new HttpError({ statusCode: 400, message: cause.message, cause }); return new HttpError({ statusCode: 400, message: cause.message, cause });
} }
if (cause instanceof NotFoundError) { if (cause instanceof PrismaClientKnownRequestError) {
return new HttpError({ statusCode: 404, message: cause.message, cause }); return new HttpError({ statusCode: 404, message: cause.message, cause });
} }

View File

@@ -1,23 +1,17 @@
import { NextApiRequest, NextApiResponse } from "next"; import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next";
import { NextRequest } from "next/server";
import prisma from "@documenso/prisma"; import prisma from "@documenso/prisma";
import { User as PrismaUser } from "@prisma/client"; import { User as PrismaUser } from "@prisma/client";
import { getToken } from "next-auth/jwt"; import { getToken } from "next-auth/jwt";
import { signOut } from "next-auth/react";
export async function getUserFromToken( export async function getUserFromToken(
req: NextApiRequest, req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
res: NextApiResponse res?: NextApiResponse // TODO: Remove this optional parameter
): Promise<PrismaUser | null> { ): Promise<PrismaUser | null> {
const token = await getToken({ req }); const token = await getToken({ req });
const tokenEmail = token?.email?.toString(); const tokenEmail = token?.email?.toString();
if (!token) { if (!token || !tokenEmail) {
if (res.status) res.status(401).send("No session token found for request.");
return null;
}
if (!tokenEmail) {
res.status(400).send("No email found in session token.");
return null; return null;
} }
@@ -26,7 +20,6 @@ export async function getUserFromToken(
}); });
if (!user) { if (!user) {
if (res && res.status) res.status(401).end();
return null; return null;
} }

View File

@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "PasswordResetToken" (
"id" SERIAL NOT NULL,
"token" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" INTEGER NOT NULL,
CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");
-- AddForeignKey
ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `expiry` to the `PasswordResetToken` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "PasswordResetToken" ADD COLUMN "expiry" TIMESTAMP(3) NOT NULL;

View File

@@ -13,17 +13,18 @@ enum IdentityProvider {
} }
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String? name String?
email String @unique email String @unique
emailVerified DateTime? emailVerified DateTime?
password String? password String?
source String? source String?
identityProvider IdentityProvider @default(DOCUMENSO) identityProvider IdentityProvider @default(DOCUMENSO)
accounts Account[] accounts Account[]
sessions Session[] sessions Session[]
Document Document[] Document Document[]
Subscription Subscription[] Subscription Subscription[]
PasswordResetToken PasswordResetToken[]
} }
enum SubscriptionStatus { enum SubscriptionStatus {
@@ -158,3 +159,12 @@ model Signature {
Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade) Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
Field Field @relation(fields: [fieldId], references: [id], onDelete: Restrict) Field Field @relation(fields: [fieldId], references: [id], onDelete: Restrict)
} }
model PasswordResetToken {
id Int @id @default(autoincrement())
token String @unique
createdAt DateTime @default(now())
expiry DateTime
userId Int
User User @relation(fields: [userId], references: [id])
}

View File

@@ -10,7 +10,7 @@ export const addDigitalSignature = async (documentAsBase64: string): Promise<str
const pdfBuffer = Buffer.from(documentAsBase64, "base64"); const pdfBuffer = Buffer.from(documentAsBase64, "base64");
const p12Buffer = Buffer.from( const p12Buffer = Buffer.from(
fs fs
.readFileSync(process.env.CERT_FILE_PATH || "ressources/certificate.p12") .readFileSync(process.env.CERT_FILE_PATH || "resources/certificate.p12")
.toString(process.env.CERT_FILE_ENCODING ? undefined : "binary"), .toString(process.env.CERT_FILE_ENCODING ? undefined : "binary"),
(process.env.CERT_FILE_ENCODING as BufferEncoding) || "binary" (process.env.CERT_FILE_ENCODING as BufferEncoding) || "binary"
); );

View File

@@ -1,6 +1,7 @@
import React from "react"; import React, { useMemo } from "react";
import { Fragment } from "react"; import { Fragment } from "react";
import { sendSigningRequests } from "@documenso/lib/api"; import { sendSigningRequests } from "@documenso/lib/api";
import { truncate } from "@documenso/lib/helpers";
import { Button } from "@documenso/ui"; import { Button } from "@documenso/ui";
import { Dialog as DialogComponent, Transition } from "@headlessui/react"; import { Dialog as DialogComponent, Transition } from "@headlessui/react";
import { Document as PrismaDocument } from "@prisma/client"; import { Document as PrismaDocument } from "@prisma/client";
@@ -19,6 +20,7 @@ type DialogProps = {
formValues: FormValue[]; formValues: FormValue[];
setLoading: (loading: boolean) => void; setLoading: (loading: boolean) => void;
icon: React.ReactNode; icon: React.ReactNode;
truncateTitle?: boolean;
}; };
export function Dialog({ export function Dialog({
@@ -29,11 +31,14 @@ export function Dialog({
formValues, formValues,
setLoading, setLoading,
icon, icon,
truncateTitle = true,
}: DialogProps) { }: DialogProps) {
const unsentEmailsLength = formValues.filter( const unsentEmailsLength = formValues.filter(
(s: any) => s.email && s.sendStatus != "SENT" (s: any) => s.email && s.sendStatus != "SENT"
).length; ).length;
const documentTitle = truncateTitle ? truncate(document.title) : document.title;
return ( return (
<Transition.Root show={open} as={Fragment}> <Transition.Root show={open} as={Fragment}>
<DialogComponent as="div" className="relative z-10" onClose={setOpen}> <DialogComponent as="div" className="relative z-10" onClose={setOpen}>
@@ -71,7 +76,7 @@ export function Dialog({
</DialogComponent.Title> </DialogComponent.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
{`"${document.title}" will be sent to ${unsentEmailsLength} recipients.`} {`"${documentTitle}" will be sent to ${unsentEmailsLength} recipients.`}
</p> </p>
</div> </div>
</div> </div>

38
turbo.json Normal file
View File

@@ -0,0 +1,38 @@
{
"$schema": "https://turbo.build/schema.json",
"globalEnv": [
"DATABASE_URL",
"NEXT_PUBLIC_WEBAPP_URL",
"NEXTAUTH_SECRET",
"NEXTAUTH_URL",
"CERT_FILE_PATH",
"CERT_PASSPHRASE",
"CERT_FILE_ENCODING",
"SENDGRID_API_KEY",
"SMTP_MAIL_HOST",
"SMTP_MAIL_PORT",
"SMTP_MAIL_USER",
"SMTP_MAIL_PASSWORD",
"MAIL_FROM",
"STRIPE_API_KEY",
"STRIPE_WEBHOOK_SECRET",
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID",
"NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID",
"NEXT_PUBLIC_ALLOW_SIGNUP",
"NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS"
],
"pipeline": {
"build": {
"outputs": [".next/**", "!.next/cache/**"]
},
"start": {
"dependsOn": ["build"],
"cache": false,
"persistent": true
},
"dev": {
"cache": false,
"persistent": true
}
}
}