Merge branch 'main' into logo
This commit is contained in:
@@ -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
3
.gitignore
vendored
@@ -36,3 +36,6 @@ yarn-error.log*
|
|||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.env
|
.env
|
||||||
.env.example
|
.env.example
|
||||||
|
|
||||||
|
# turborepo
|
||||||
|
.turbo
|
||||||
@@ -17,7 +17,7 @@ The development branch is <code>main</code>. All pull request should be made aga
|
|||||||
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||||
2. Create a new branch:
|
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
|
```sh
|
||||||
git checkout -b doc-999-my-feature-or-fix
|
git checkout -b doc-999-my-feature-or-fix
|
||||||
|
|||||||
57
README.md
57
README.md
@@ -1,10 +1,3 @@
|
|||||||
<div align="center" style="margin-top: 12px; margin-bottom: 3332px;">
|
|
||||||
<p>
|
|
||||||
We are LIVE on Product Hunt. Come say hi..
|
|
||||||
</p>
|
|
||||||
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=395047&theme=light" alt="Documenso - The Open Source DocuSign Alternative. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<p align="center" style="margin-top: 120px">
|
<p align="center" style="margin-top: 120px">
|
||||||
<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">
|
||||||
@@ -18,7 +11,7 @@
|
|||||||
<a href="https://documenso.com"><strong>Learn more »</strong></a>
|
<a href="https://documenso.com"><strong>Learn more »</strong></a>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<a href="https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w">Slack</a>
|
<a href="https://documen.so/slack">Slack</a>
|
||||||
·
|
·
|
||||||
<a href="https://documenso.com">Website</a>
|
<a href="https://documenso.com">Website</a>
|
||||||
·
|
·
|
||||||
@@ -29,7 +22,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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/slack"><img src="https://img.shields.io/badge/Slack-documenso.slack.com-%234A154B" alt="Join Documenso on Slack"></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/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/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>
|
<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>
|
||||||
@@ -63,13 +56,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.
|
Signing documents digitally is fast, easy and should be best practice for every document signed worldwide. This is technically quite easy today, but it also introduces a new party to every signature: The signing tool providers. While this is not a problem in itself, it should make us think about how we want these providers of trust to work. Documenso aims to be the world's most trusted document signing tool. This trust is built by empowering you to self-host Documenso and review how it works under the hood. Join us in creating the next generation of open trust infrastructure.
|
||||||
|
|
||||||
|
## Recognition
|
||||||
|
|
||||||
|
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=395047&theme=light&period=daily" alt="Documenso - The open source DocuSign alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
<a href="https://www.producthunt.com/posts/documenso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=395047&theme=light" alt="Documenso - The Open Source DocuSign Alternative. | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
## Community and Next Steps 🎯
|
## 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:
|
||||||
|
|
||||||
- Check out the first source code release in this repository and test it
|
- 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)
|
- 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
|
- ⭐ the repository to help us raise awareness
|
||||||
- Spread the word on Twitter, that Documenso is working towards a more open signing tool
|
- 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
|
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
|
||||||
@@ -78,8 +76,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:
|
||||||
@@ -123,7 +119,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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -157,10 +153,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
|
||||||
@@ -185,7 +181,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
|
||||||
|
|
||||||
@@ -200,3 +196,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
|
||||||
|
- "::"
|
||||||
|
```
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function PDFEditor(props: any) {
|
|||||||
movedField.positionY = position.y.toFixed(0);
|
movedField.positionY = position.y.toFixed(0);
|
||||||
createOrUpdateField(props.document, movedField);
|
createOrUpdateField(props.document, movedField);
|
||||||
|
|
||||||
// no instant redraw neccessary, postion information for saving or later rerender is enough
|
// no instant redraw neccessary, position information for saving or later rerender is enough
|
||||||
// setFields(newFields);
|
// setFields(newFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
115
apps/web/components/forgot-password.tsx
Normal file
115
apps/web/components/forgot-password.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@documenso/ui";
|
||||||
|
import Logo from "./logo";
|
||||||
|
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
|
interface ForgotPasswordForm {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ForgotPassword() {
|
||||||
|
const { register, formState, resetField, handleSubmit } = useForm<ForgotPasswordForm>();
|
||||||
|
const [resetSuccessful, setResetSuccessful] = useState(false);
|
||||||
|
|
||||||
|
const onSubmit = async (values: ForgotPasswordForm) => {
|
||||||
|
const response = await toast.promise(
|
||||||
|
fetch(`/api/auth/forgot-password`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(values),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: "Sending...",
|
||||||
|
success: "Reset link sent.",
|
||||||
|
error: "Could not send reset link :/",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
toast.dismiss();
|
||||||
|
|
||||||
|
if (response.status == 404) {
|
||||||
|
toast.error("Email address not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status == 400) {
|
||||||
|
toast.error("Password reset requested.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status == 500) {
|
||||||
|
toast.error("Something went wrong.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setResetSuccessful(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetField("email");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="w-full max-w-md space-y-8">
|
||||||
|
<div>
|
||||||
|
<Logo className="mx-auto h-20 w-auto"></Logo>
|
||||||
|
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
|
||||||
|
{resetSuccessful ? "Reset Password" : "Forgot Password?"}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-center text-sm text-gray-600">
|
||||||
|
{resetSuccessful
|
||||||
|
? "Please check your email for reset instructions."
|
||||||
|
: "No worries, we'll send you reset instructions."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{!resetSuccessful && (
|
||||||
|
<form className="mt-8 space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="-space-y-px rounded-md shadow-sm">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email-address" className="sr-only">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
{...register("email")}
|
||||||
|
id="email-address"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
required
|
||||||
|
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
|
||||||
|
placeholder="Email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={formState.isSubmitting}
|
||||||
|
className="group relative flex w-full">
|
||||||
|
Reset password
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<Link href="/login">
|
||||||
|
<div className="relative mt-10 flex items-center justify-center gap-2 text-sm text-gray-500 hover:cursor-pointer hover:text-gray-900">
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
Back to log in
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 text-black" />
|
<Logo className="h-8 w-8 text-black" />
|
||||||
<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">
|
||||||
|
|||||||
143
apps/web/components/reset-password.tsx
Normal file
143
apps/web/components/reset-password.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { Button } from "@documenso/ui";
|
||||||
|
import Logo from "./logo";
|
||||||
|
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import * as z from "zod";
|
||||||
|
|
||||||
|
const ZResetPasswordFormSchema = z
|
||||||
|
.object({
|
||||||
|
password: z.string().min(8, { message: "Password must be at least 8 characters" }),
|
||||||
|
confirmPassword: z.string().min(8, { message: "Password must be at least 8 characters" }),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
path: ["confirmPassword"],
|
||||||
|
message: "Password don't match",
|
||||||
|
});
|
||||||
|
|
||||||
|
type TResetPasswordFormSchema = z.infer<typeof ZResetPasswordFormSchema>;
|
||||||
|
|
||||||
|
export default function ResetPassword() {
|
||||||
|
const router = useRouter();
|
||||||
|
const { token } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
handleSubmit,
|
||||||
|
} = useForm<TResetPasswordFormSchema>({
|
||||||
|
resolver: zodResolver(ZResetPasswordFormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [resetSuccessful, setResetSuccessful] = useState(false);
|
||||||
|
|
||||||
|
const onSubmit = async ({ password }: TResetPasswordFormSchema) => {
|
||||||
|
const response = await toast.promise(
|
||||||
|
fetch(`/api/auth/reset-password`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ password, token }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: "Resetting...",
|
||||||
|
success: `Reset password successful`,
|
||||||
|
error: "Could not reset password :/",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
toast.dismiss();
|
||||||
|
const error = await response.json();
|
||||||
|
toast.error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setResetSuccessful(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push("/login");
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="w-full max-w-md space-y-8">
|
||||||
|
<div>
|
||||||
|
<Logo className="mx-auto h-20 w-auto"></Logo>
|
||||||
|
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
|
||||||
|
Reset Password
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-center text-sm text-gray-600">
|
||||||
|
{resetSuccessful ? "Your password has been reset." : "Please chose your new password"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{!resetSuccessful && (
|
||||||
|
<form className="mt-8 space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="-space-y-px rounded-md shadow-sm">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="sr-only">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
{...register("password", { required: "Password is required" })}
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
|
||||||
|
placeholder="New password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="confirmPassword" className="sr-only">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
{...register("confirmPassword")}
|
||||||
|
id="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
className="focus:border-neon focus:ring-neon relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:outline-none sm:text-sm"
|
||||||
|
placeholder="Confirm new password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{errors && (
|
||||||
|
<span className="text-xs text-red-500">{errors.confirmPassword?.message}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="group relative flex w-full">
|
||||||
|
Reset password
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Link href="/login">
|
||||||
|
<div className="relative mt-10 flex items-center justify-center gap-2 text-sm text-gray-500 hover:cursor-pointer hover:text-gray-900">
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
Back to log in
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,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;
|
|
||||||
|
|||||||
@@ -56,7 +56,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",
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
63
apps/web/pages/api/auth/forgot-password.ts
Normal file
63
apps/web/pages/api/auth/forgot-password.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { sendResetPassword } from "@documenso/lib/mail";
|
||||||
|
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { email } = req.body;
|
||||||
|
const cleanEmail = email.toLowerCase();
|
||||||
|
|
||||||
|
if (!cleanEmail || !/.+@.+/.test(cleanEmail)) {
|
||||||
|
res.status(400).json({ message: "Invalid email" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
email: cleanEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(200).json({ message: "A password reset email has been sent." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingToken = await prisma.passwordResetToken.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
createdAt: {
|
||||||
|
gte: new Date(Date.now() - 1000 * 60 * 60),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingToken) {
|
||||||
|
return res.status(200).json({ message: "A password reset email has been sent." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = crypto.randomBytes(64).toString("hex");
|
||||||
|
const expiry = new Date();
|
||||||
|
expiry.setHours(expiry.getHours() + 24); // Set expiry to one hour from now
|
||||||
|
|
||||||
|
let passwordResetToken;
|
||||||
|
try {
|
||||||
|
passwordResetToken = await prisma.passwordResetToken.create({
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
expiry,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({ message: "Something went wrong" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendResetPassword(user, passwordResetToken.token);
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "A password reset email has been sent." });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||||
|
});
|
||||||
69
apps/web/pages/api/auth/reset-password.ts
Normal file
69
apps/web/pages/api/auth/reset-password.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { hashPassword, verifyPassword } from "@documenso/lib/auth";
|
||||||
|
import { sendResetPasswordSuccessMail } from "@documenso/lib/mail";
|
||||||
|
import { defaultHandler, defaultResponder } from "@documenso/lib/server";
|
||||||
|
import prisma from "@documenso/prisma";
|
||||||
|
|
||||||
|
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { token, password } = req.body;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
res.status(400).json({ message: "Invalid token" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundToken = await prisma.passwordResetToken.findUnique({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
User: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!foundToken) {
|
||||||
|
return res.status(404).json({ message: "Invalid token." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
if (now > foundToken.expiry) {
|
||||||
|
return res.status(400).json({ message: "Token has expired" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSamePassword = await verifyPassword(password, foundToken.User.password!);
|
||||||
|
|
||||||
|
if (isSamePassword) {
|
||||||
|
return res.status(400).json({ message: "New password must be different" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await hashPassword(password);
|
||||||
|
|
||||||
|
const transaction = await prisma.$transaction([
|
||||||
|
prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: foundToken.userId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
password: hashedPassword,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.passwordResetToken.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: foundToken.userId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!transaction) {
|
||||||
|
return res.status(500).json({ message: "Error resetting password." });
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendResetPasswordSuccessMail(foundToken.User);
|
||||||
|
|
||||||
|
res.status(200).json({ message: "Password reset successful." });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||||
|
});
|
||||||
@@ -8,13 +8,13 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
const { email, password, source } = req.body;
|
const { 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.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,53 +6,62 @@ import prisma from "@documenso/prisma";
|
|||||||
import { Document as PrismaDocument, SendStatus } from "@prisma/client";
|
import { Document as PrismaDocument, SendStatus } from "@prisma/client";
|
||||||
|
|
||||||
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const user = await getUserFromToken(req, res);
|
try {
|
||||||
const { id: documentId } = req.query;
|
const user = await getUserFromToken(req, res);
|
||||||
const { resendTo: resendTo = [] } = req.body;
|
const { id: documentId } = req.query;
|
||||||
|
const { resendTo: resendTo = [] } = req.body;
|
||||||
|
|
||||||
if (!user) return;
|
if (!user) {
|
||||||
|
return res.status(401).send("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
if (!documentId) {
|
if (!documentId) {
|
||||||
res.status(400).send("Missing parameter documentId.");
|
return res.status(400).send("Missing parameter documentId.");
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 = {
|
let recipientCondition: any = {
|
||||||
documentId: +documentId,
|
|
||||||
sendStatus: SendStatus.NOT_SENT,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (resendTo.length) {
|
|
||||||
recipientCondition = {
|
|
||||||
documentId: +documentId,
|
documentId: +documentId,
|
||||||
id: { in: resendTo },
|
sendStatus: SendStatus.NOT_SENT,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const recipients = await prisma.recipient.findMany({
|
if (resendTo.length) {
|
||||||
where: {
|
recipientCondition = {
|
||||||
...recipientCondition,
|
documentId: +documentId,
|
||||||
},
|
id: { in: resendTo },
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!recipients.length) return res.status(200).send(recipients.length);
|
const recipients = await prisma.recipient.findMany({
|
||||||
|
where: {
|
||||||
let sentRequests = 0;
|
...recipientCondition,
|
||||||
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.");
|
if (!recipients.length) {
|
||||||
|
return res.status(200).send(recipients.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sentRequests = 0;
|
||||||
|
recipients.forEach(async (recipient) => {
|
||||||
|
await sendSigningRequest(recipient, document, user);
|
||||||
|
|
||||||
|
sentRequests++;
|
||||||
});
|
});
|
||||||
|
|
||||||
sentRequests++;
|
|
||||||
if (sentRequests === recipients.length) {
|
if (sentRequests === recipients.length) {
|
||||||
return res.status(200).send(recipients.length);
|
return res.status(200).send(recipients.length);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
return res.status(502).end("Coud not send request for signing.");
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(502).end("Coud not send request for signing.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultHandler({
|
export default defaultHandler({
|
||||||
|
|||||||
30
apps/web/pages/auth/reset/[token].tsx
Normal file
30
apps/web/pages/auth/reset/[token].tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Head from "next/head";
|
||||||
|
import { getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import ResetPassword from "../../../components/reset-password";
|
||||||
|
|
||||||
|
export default function ResetPasswordPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Reset Password | Documenso</title>
|
||||||
|
</Head>
|
||||||
|
<ResetPassword />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServerSideProps(context: any) {
|
||||||
|
const user = await getUserFromToken(context.req, context.res);
|
||||||
|
if (user)
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
source: "/login",
|
||||||
|
destination: "/dashboard",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
20
apps/web/pages/auth/reset/index.tsx
Normal file
20
apps/web/pages/auth/reset/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Logo from "../../../components/logo";
|
||||||
|
|
||||||
|
export default function ResetPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="w-full max-w-md space-y-8">
|
||||||
|
<div>
|
||||||
|
<Logo className="mx-auto h-20 w-auto"></Logo>
|
||||||
|
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
|
||||||
|
Reset Password
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-center text-sm text-gray-600">
|
||||||
|
The token you provided is invalid. Please try again.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import Head from "next/head";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { uploadDocument } from "@documenso/features";
|
import { uploadDocument } from "@documenso/features";
|
||||||
import { deleteDocument, getDocuments } from "@documenso/lib/api";
|
import { deleteDocument, getDocuments } from "@documenso/lib/api";
|
||||||
|
import { useSubscription } from "@documenso/lib/stripe";
|
||||||
import { Button, IconButton, SelectBox } from "@documenso/ui";
|
import { Button, IconButton, SelectBox } from "@documenso/ui";
|
||||||
import Layout from "../components/layout";
|
import Layout from "../components/layout";
|
||||||
import type { NextPageWithLayout } from "./_app";
|
import type { NextPageWithLayout } from "./_app";
|
||||||
@@ -20,7 +21,6 @@ import {
|
|||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import { DocumentStatus } from "@prisma/client";
|
import { DocumentStatus } from "@prisma/client";
|
||||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||||
import { useSubscription } from "@documenso/lib/stripe";
|
|
||||||
|
|
||||||
const DocumentsPage: NextPageWithLayout = (props: any) => {
|
const DocumentsPage: NextPageWithLayout = (props: any) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -145,24 +145,24 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 mb-12 flex flex-row-reverse items-center gap-x-4">
|
<div className="mt-3 mb-12 flex flex-wrap items-center justify-start gap-x-4 md:justify-end gap-y-4">
|
||||||
<div className="pt-5 block w-fit">
|
|
||||||
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
|
|
||||||
</div>
|
|
||||||
<SelectBox
|
<SelectBox
|
||||||
className="block w-1/4"
|
className="block flex-1 md:flex-none md:w-1/4"
|
||||||
label="Created"
|
|
||||||
options={createdFilter}
|
|
||||||
value={selectedCreatedFilter}
|
|
||||||
onChange={setSelectedCreatedFilter}
|
|
||||||
/>
|
|
||||||
<SelectBox
|
|
||||||
className="block w-1/4"
|
|
||||||
label="Status"
|
label="Status"
|
||||||
options={statusFilters}
|
options={statusFilters}
|
||||||
value={selectedStatusFilter}
|
value={selectedStatusFilter}
|
||||||
onChange={handleStatusFilterChange}
|
onChange={handleStatusFilterChange}
|
||||||
/>
|
/>
|
||||||
|
<SelectBox
|
||||||
|
className="block flex-1 md:flex-none md:w-1/4"
|
||||||
|
label="Created"
|
||||||
|
options={createdFilter}
|
||||||
|
value={selectedCreatedFilter}
|
||||||
|
onChange={setSelectedCreatedFilter}
|
||||||
|
/>
|
||||||
|
<div className="block w-fit pt-5">
|
||||||
|
{filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 max-w-[1100px]" hidden={!loading}>
|
<div className="mt-8 max-w-[1100px]" hidden={!loading}>
|
||||||
<div className="ph-item">
|
<div className="ph-item">
|
||||||
@@ -224,13 +224,13 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
{document.title || "#" + document.id}
|
{document.title || "#" + document.id}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap inline-flex py-3 gap-x-2 gap-y-1 flex-wrap max-w-[250px] text-sm text-gray-500">
|
<td className="inline-flex max-w-[250px] flex-wrap gap-x-2 gap-y-1 whitespace-nowrap py-3 text-sm text-gray-500">
|
||||||
{document.Recipient.map((item: any) => (
|
{document.Recipient.map((item: any) => (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
{item.sendStatus === "NOT_SENT" ? (
|
{item.sendStatus === "NOT_SENT" ? (
|
||||||
<span
|
<span
|
||||||
id="sent_icon"
|
id="sent_icon"
|
||||||
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||||
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -240,7 +240,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<span id="sent_icon">
|
<span id="sent_icon">
|
||||||
<span
|
<span
|
||||||
id="sent_icon"
|
id="sent_icon"
|
||||||
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||||
<EnvelopeIcon className="mr-1 inline h-4"></EnvelopeIcon>
|
<EnvelopeIcon className="mr-1 inline h-4"></EnvelopeIcon>
|
||||||
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||||
</span>
|
</span>
|
||||||
@@ -253,7 +253,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
<span id="read_icon">
|
<span id="read_icon">
|
||||||
<span
|
<span
|
||||||
id="sent_icon"
|
id="sent_icon"
|
||||||
className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-yellow-200 px-2 py-0.5 text-xs font-medium text-yellow-800">
|
||||||
<CheckIcon className="-mr-2 inline h-4"></CheckIcon>
|
<CheckIcon className="-mr-2 inline h-4"></CheckIcon>
|
||||||
<CheckIcon className="mr-1 inline h-4"></CheckIcon>
|
<CheckIcon className="mr-1 inline h-4"></CheckIcon>
|
||||||
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
{item.name ? item.name + " <" + item.email + ">" : item.email}
|
||||||
@@ -264,7 +264,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
|
|||||||
)}
|
)}
|
||||||
{item.signingStatus === "SIGNED" ? (
|
{item.signingStatus === "SIGNED" ? (
|
||||||
<span id="signed_icon">
|
<span id="signed_icon">
|
||||||
<span className="flex-shrink-0 h-6 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
<span className="inline-flex h-6 flex-shrink-0 items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||||
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>{" "}
|
<CheckBadgeIcon className="mr-1 inline h-5"></CheckBadgeIcon>{" "}
|
||||||
{item.email}
|
{item.email}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
32
apps/web/pages/forgot-password.tsx
Normal file
32
apps/web/pages/forgot-password.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { GetServerSideProps, GetServerSidePropsContext } from "next";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { getUserFromToken } from "@documenso/lib/server";
|
||||||
|
import ForgotPassword from "../components/forgot-password";
|
||||||
|
|
||||||
|
export default function ForgotPasswordPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Forgot Password | Documenso</title>
|
||||||
|
</Head>
|
||||||
|
<ForgotPassword />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServerSideProps({ req }: GetServerSidePropsContext) {
|
||||||
|
const user = await getUserFromToken(req);
|
||||||
|
|
||||||
|
if (user)
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
source: "/login",
|
||||||
|
destination: "/dashboard",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
name: documenso
|
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
|
|||||||
438
package-lock.json
generated
438
package-lock.json
generated
@@ -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=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -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 -p documenso -f ./docker/compose-without-app.yml up -d || docker-compose -p documenso -f ./docker/compose-without-app.yml up -d",
|
||||||
"docker:compose-up": "npm run docker:compose -- 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",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
packages/lib/helpers/index.ts
Normal file
1
packages/lib/helpers/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './strings';
|
||||||
13
packages/lib/helpers/strings.ts
Normal file
13
packages/lib/helpers/strings.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Truncates a title to a given max length substituting the middle with an ellipsis.
|
||||||
|
*/
|
||||||
|
export const truncate = (str: string, maxLength: number = 20) => {
|
||||||
|
if (str.length <= maxLength) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startLength = Math.ceil((maxLength - 3) / 2);
|
||||||
|
const endLength = Math.floor((maxLength - 3) / 2);
|
||||||
|
|
||||||
|
return `${str.slice(0, startLength)}...${str.slice(-endLength)}`;
|
||||||
|
};
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
import { 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}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
51
packages/lib/mail/resetPasswordSuccessTemplate.ts
Normal file
51
packages/lib/mail/resetPasswordSuccessTemplate.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
|
export const resetPasswordSuccessTemplate = (user: User) => {
|
||||||
|
return `
|
||||||
|
<div style="background-color: #eaeaea; padding: 2%;">
|
||||||
|
<div
|
||||||
|
style="text-align:left; margin: auto; font-size: 14px; color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
|
||||||
|
<img src="${NEXT_PUBLIC_WEBAPP_URL}/logo_h.png" alt="Documenso Logo"
|
||||||
|
style="width: 180px; display: block; margin-bottom: 14px;" />
|
||||||
|
|
||||||
|
<h2 style="text-align: left; margin-top: 20px; font-size: 24px; font-weight: bold">Password updated!</h2>
|
||||||
|
|
||||||
|
<p style="margin-top: 15px">
|
||||||
|
Hi ${user.name ? user.name : user.email},
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin-top: 15px">
|
||||||
|
We've changed your password as you asked. You can now sign in with your new password.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin-top: 15px">
|
||||||
|
Didn't request a password change? We are here to help you secure your account, just <a href="https://documenso.com">contact us</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin-top: 15px">
|
||||||
|
<p style="font-weight: bold">
|
||||||
|
The Documenso Team
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="text-align:left; margin-top: 30px">
|
||||||
|
<small>Want to send you own signing links?
|
||||||
|
<a href="https://documenso.com">Hosted Documenso is here!</a>.</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: left; line-height: 18px; color: #666666; margin: 24px">
|
||||||
|
<div style="margin-top: 12px">
|
||||||
|
<b>Need help?</b>
|
||||||
|
<br>
|
||||||
|
Contact us at <a href="mailto:hi@documenso.com">hi@documenso.com</a>
|
||||||
|
</div>
|
||||||
|
<hr size="1" style="height: 1px; border: none; color: #D8D8D8; background-color: #D8D8D8">
|
||||||
|
<div style="text-align: center">
|
||||||
|
<small>Easy and beautiful document signing by Documenso.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
export default resetPasswordSuccessTemplate;
|
||||||
46
packages/lib/mail/resetPasswordTemplate.ts
Normal file
46
packages/lib/mail/resetPasswordTemplate.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||||
|
|
||||||
|
export const resetPasswordTemplate = (ctaLink: string, ctaLabel: string) => {
|
||||||
|
const customContent = `
|
||||||
|
<h2 style="margin-top: 36px; font-size: 24px; font-weight: bold;">Forgot your password?</h2>
|
||||||
|
<p style="margin-top: 8px;">
|
||||||
|
That's okay, it happens! Click the button below to reset your password.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="margin: 30px 0px; text-align: center">
|
||||||
|
<a href="${ctaLink}" style="background-color: #37f095; white-space: nowrap; color: white; border-color: transparent; border-width: 1px; border-radius: 0.375rem; font-size: 18px; padding-left: 16px; padding-right: 16px; padding-top: 10px; padding-bottom: 10px; text-decoration: none; margin-top: 4px; margin-bottom: 4px;">
|
||||||
|
${ctaLabel}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 20px;">
|
||||||
|
<small>Want to send you own signing links? <a href="https://documenso.com">Hosted Documenso is here!</a>.</small>
|
||||||
|
</p>`;
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div style="background-color: #eaeaea; padding: 2%;">
|
||||||
|
<div
|
||||||
|
style="text-align:center; margin: auto; font-size: 14px; color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
|
||||||
|
<img src="${NEXT_PUBLIC_WEBAPP_URL}/logo_h.png" alt="Documenso Logo"
|
||||||
|
style="width: 180px; display: block; margin: auto; margin-bottom: 14px;" />
|
||||||
|
${customContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const footer = `
|
||||||
|
<div style="text-align: left; line-height: 18px; color: #666666; margin: 24px">
|
||||||
|
<div style="margin-top: 12px">
|
||||||
|
<b>Need help?</b>
|
||||||
|
<br>
|
||||||
|
Contact us at <a href="mailto:hi@documenso.com">hi@documenso.com</a>
|
||||||
|
</div>
|
||||||
|
<hr size="1" style="height: 1px; border: none; color: #D8D8D8; background-color: #D8D8D8">
|
||||||
|
<div style="text-align: center">
|
||||||
|
<small>Easy and beautiful document signing by Documenso.</small>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
return html + footer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default resetPasswordTemplate;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ReadStream } from "fs";
|
|
||||||
import nodemailer from "nodemailer";
|
import nodemailer from "nodemailer";
|
||||||
import nodemailerSendgrid from "nodemailer-sendgrid";
|
import nodemailerSendgrid from "nodemailer-sendgrid";
|
||||||
|
|
||||||
|
|||||||
14
packages/lib/mail/sendResetPassword.ts
Normal file
14
packages/lib/mail/sendResetPassword.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { resetPasswordTemplate } from "@documenso/lib/mail";
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||||
|
import { sendMail } from "./sendMail";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
|
export const sendResetPassword = async (user: User, token: string) => {
|
||||||
|
await sendMail(
|
||||||
|
user.email,
|
||||||
|
"Forgot password?",
|
||||||
|
resetPasswordTemplate(`${NEXT_PUBLIC_WEBAPP_URL}/auth/reset/${token}`, "Reset Your Password")
|
||||||
|
).catch((err) => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
};
|
||||||
11
packages/lib/mail/sendResetPasswordSuccessMail.ts
Normal file
11
packages/lib/mail/sendResetPasswordSuccessMail.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import resetPasswordSuccessTemplate from "./resetPasswordSuccessTemplate";
|
||||||
|
import { sendMail } from "./sendMail";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
|
export const sendResetPasswordSuccessMail = async (user: User) => {
|
||||||
|
await sendMail(user.email, "Password Reset Success!", resetPasswordSuccessTemplate(user)).catch(
|
||||||
|
(err) => {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
import { 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 = `
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
log("constructing body...")
|
|
||||||
const body = await buffer(req);
|
const body = await buffer(req);
|
||||||
log("constructed body")
|
|
||||||
|
|
||||||
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
|
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
|
||||||
log("event-type:", event.type);
|
log("event-type:", event.type);
|
||||||
@@ -70,23 +68,38 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
|
|||||||
if (event.type === "invoice.payment_succeeded") {
|
if (event.type === "invoice.payment_succeeded") {
|
||||||
const invoice = event.data.object as Stripe.Invoice;
|
const invoice = event.data.object as Stripe.Invoice;
|
||||||
|
|
||||||
|
if (invoice.billing_reason !== "subscription_cycle") {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: "Webhook received",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const customerId =
|
const customerId =
|
||||||
typeof invoice.customer === "string" ? invoice.customer : invoice.customer?.id;
|
typeof invoice.customer === "string" ? invoice.customer : invoice.customer?.id;
|
||||||
|
|
||||||
const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string);
|
const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string);
|
||||||
|
|
||||||
await prisma.subscription.update({
|
const hasSubscription = await prisma.subscription.findFirst({
|
||||||
where: {
|
where: {
|
||||||
customerId,
|
customerId,
|
||||||
},
|
},
|
||||||
data: {
|
|
||||||
status: SubscriptionStatus.ACTIVE,
|
|
||||||
planId: subscription.id,
|
|
||||||
priceId: subscription.items.data[0].price.id,
|
|
||||||
periodEnd: new Date(subscription.current_period_end * 1000),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasSubscription) {
|
||||||
|
await prisma.subscription.update({
|
||||||
|
where: {
|
||||||
|
customerId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: SubscriptionStatus.ACTIVE,
|
||||||
|
planId: subscription.id,
|
||||||
|
priceId: subscription.items.data[0].price.id,
|
||||||
|
periodEnd: new Date(subscription.current_period_end * 1000),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Webhook received",
|
message: "Webhook received",
|
||||||
@@ -98,15 +111,23 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
const customerId = failedInvoice.customer as string;
|
const customerId = failedInvoice.customer as string;
|
||||||
|
|
||||||
await prisma.subscription.update({
|
const hasSubscription = await prisma.subscription.findFirst({
|
||||||
where: {
|
where: {
|
||||||
customerId,
|
customerId,
|
||||||
},
|
},
|
||||||
data: {
|
|
||||||
status: SubscriptionStatus.PAST_DUE,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasSubscription) {
|
||||||
|
await prisma.subscription.update({
|
||||||
|
where: {
|
||||||
|
customerId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: SubscriptionStatus.PAST_DUE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Webhook received",
|
message: "Webhook received",
|
||||||
@@ -118,18 +139,26 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
const customerId = updatedSubscription.customer as string;
|
const customerId = updatedSubscription.customer as string;
|
||||||
|
|
||||||
await prisma.subscription.update({
|
const hasSubscription = await prisma.subscription.findFirst({
|
||||||
where: {
|
where: {
|
||||||
customerId,
|
customerId,
|
||||||
},
|
},
|
||||||
data: {
|
|
||||||
status: SubscriptionStatus.ACTIVE,
|
|
||||||
planId: updatedSubscription.id,
|
|
||||||
priceId: updatedSubscription.items.data[0].price.id,
|
|
||||||
periodEnd: new Date(updatedSubscription.current_period_end * 1000),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasSubscription) {
|
||||||
|
await prisma.subscription.update({
|
||||||
|
where: {
|
||||||
|
customerId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: SubscriptionStatus.ACTIVE,
|
||||||
|
planId: updatedSubscription.id,
|
||||||
|
priceId: updatedSubscription.items.data[0].price.id,
|
||||||
|
periodEnd: new Date(updatedSubscription.current_period_end * 1000),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Webhook received",
|
message: "Webhook received",
|
||||||
@@ -141,15 +170,23 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
const customerId = deletedSubscription.customer as string;
|
const customerId = deletedSubscription.customer as string;
|
||||||
|
|
||||||
await prisma.subscription.update({
|
const hasSubscription = await prisma.subscription.findFirst({
|
||||||
where: {
|
where: {
|
||||||
customerId,
|
customerId,
|
||||||
},
|
},
|
||||||
data: {
|
|
||||||
status: SubscriptionStatus.INACTIVE,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hasSubscription) {
|
||||||
|
await prisma.subscription.update({
|
||||||
|
where: {
|
||||||
|
customerId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: SubscriptionStatus.INACTIVE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Webhook received",
|
message: "Webhook received",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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])
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
38
turbo.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user