diff --git a/.env.example b/.env.example
index a1faef3f0..f5c2486fe 100644
--- a/.env.example
+++ b/.env.example
@@ -4,8 +4,8 @@
# Option 3: Use the provided dx setup (RECOMMENDED)
# => postgres://documenso:password@127.0.0.1:54320/documenso
#
-# ⚠ WARNING: The test database can be resetted or taken offline at any point.
-# ⚠ WARNING: Please be aware that nothing written to the test databae is private.
+# ⚠ WARNING: The test database can be reset or taken offline at any point.
+# ⚠ WARNING: Please be aware that nothing written to the test database is private.
DATABASE_URL=''
# URL
@@ -51,4 +51,4 @@ NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID=
#FEATURE FLAGS
# Allow users to register via the /signup page. Otherwise they will be redirect to the home page.
NEXT_PUBLIC_ALLOW_SIGNUP=true
-NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=true
\ No newline at end of file
+NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=false
diff --git a/.gitignore b/.gitignore
index 9965f5ca6..d7f66a11a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,3 +36,6 @@ yarn-error.log*
next-env.d.ts
.env
.env.example
+
+# turborepo
+.turbo
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 46768040d..5aeb61c1b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,7 +17,7 @@ The development branch is main. All pull request should be made aga
[clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Create a new branch:
-- Create a new branch (include the issue id and somthing readable):
+- Create a new branch (include the issue id and something readable):
```sh
git checkout -b doc-999-my-feature-or-fix
diff --git a/README.md b/README.md
index cf5b78ba1..97ebe6555 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,4 @@
-> We are launching TOMORROW on Product Hunt soon! Sign up to support the launch:
->
-
-
+
@@ -14,7 +11,7 @@
Learn more »
- Slack
+ Slack
·
Website
·
@@ -25,7 +22,7 @@
-
+
@@ -59,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.
+## Recognition
+
+
+
+
## Community and Next Steps 🎯
-The current project goal is to [release a production ready version](https://github.com/documenso/documenso/milestone/1) for self-hosting as soon as possible. If you want to help making that happen you can:
+We're currently working on a redesign of the application including a revamp of the codebase so Documenso can be more intuitive to use and robust to develop upon.
- Check out the first source code release in this repository and test it
- Tell us what you think in the current [Discussions](https://github.com/documenso/documenso/discussions)
-- Join the [Slack Channel](https://join.slack.com/t/documenso/shared_invite/zt-1qwxxsvli-nDyojjt~wakhgBGl9JRl2w) for any questions and getting to know to other community members
+- Join the [Slack Channel](https://documen.so/slack) for any questions and getting to know to other community members
- ⭐ the repository to help us raise awareness
- Spread the word on Twitter, that Documenso is working towards a more open signing tool
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release
@@ -74,8 +76,6 @@ The current project goal is to [release a production ready version](https://g
- To contribute please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
-
-
# Tech
Documenso is built using awesome open source tech including:
@@ -119,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.
- Run `npm run dev` in the root directory
- Want it even faster? Just use
- ```sh
+ ```sh
npm run d
```
@@ -137,31 +137,33 @@ Follow these steps to setup documenso on you local machine:
```sh
git clone https://github.com/documenso/documenso
```
-- Run npm i in root directory
-- Rename .env.example to .env
+- Run `npm i` in root directory
+- Rename `.env.example` to `.env`
- Set DATABASE_URL value in .env file
- You can use the provided test database url (may be wiped at any point)
- Or setup a local postgres sql instance (recommended)
-- Create the database scheme by running db-migrate:dev
+- Create the database scheme by running `db-migrate:dev`
- Setup your mail provider
- - Set SENDGRID_API_KEY value in .env file
+ - Set `SENDGRID_API_KEY` value in .env file
- You need a SendGrid account, which you can create [here](https://signup.sendgrid.com/).
- - Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the SMTP\_\* variables in your .env
-- Run npm run dev root directory to start
+ - Documenso uses [Nodemailer](https://nodemailer.com/about/) so you can easily use your own SMTP server by setting the `SMTP
+ \_
+ * variables` in your .env
+- Run `npm run dev` root directory to start
- Register a new user at http://localhost:3000/signup
---
-- Optional: Seed the database using npm run db-seed to create a test user and document
-- Optional: Upload and sign apps/web/ressources/example.pdf manually to test your setup
+- Optional: Seed the database using `npm run db-seed` to create a test user and document
+- Optional: Upload and sign `apps/web/resources/example.pdf` manually to test your setup
- Optional: Create your own signing certificate
- - A demo certificate is provided in `/app/web/ressources/certificate.p12`
+ - A demo certificate is provided in `/app/web/resources/certificate.p12`
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL) see **[Create your own signing certificate](#creating-your-own-signing-certificate)**.
## Updating
-- If you pull the newest version from main, using git pull, it may be necessary to regenerate your database client
+- If you pull the newest version from main, using `git pull`, it may be necessary to regenerate your database client
- You can do this by running the generate command in `/packages/prisma`:
```sh
npx prisma generate
@@ -172,16 +174,22 @@ Follow these steps to setup documenso on you local machine:
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
-1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:\
- openssl genrsa -out private.key 2048
+1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:
+
+ `openssl genrsa -out private.key 2048`
+
+2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
+
+ `openssl req -new -x509 -key private.key -out certificate.crt -days 365`
-2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:\
- openssl req -new -x509 -key private.key -out certificate.crt -days 365 \
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
-3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this: \
- openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt
+
+3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this:
+
+ `openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
+
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
-5. Place the certificate /apps/web/ressource/certificate.p12
+5. Place the certificate `/apps/web/resources/certificate.p12`
# Docker
@@ -195,4 +203,39 @@ Want to create a production ready docker image? Follow these steps:
# Deploying - Coming Soon™
- Docker support
-- One-Click-Deploy on Render.com Deploy
+- One-Click-Deploy on Render.com
+
+# Troubleshooting
+
+## I'm not receiving any emails when using the developer quickstart
+
+When using the developer quickstart an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing email locally for you to view.
+
+The Web UI can be found at http://localhost:9000 while the SMTP port will be on localhost:2500.
+
+## Support IPv6
+
+In case you are deploying to a cluster that uses only IPv6. You can use a custom command to pass a parameter to the NextJS start command
+
+For local docker run
+
+```bash
+docker run -it documenso:latest npm run start -- -H ::
+```
+
+For k8s or docker-compose
+
+```yaml
+containers:
+ - name: documenso
+ image: documenso:latest
+ imagePullPolicy: IfNotPresent
+ command:
+ - npm
+ args:
+ - run
+ - start
+ - --
+ - -H
+ - "::"
+```
diff --git a/apps/web/components/editor/pdf-editor.tsx b/apps/web/components/editor/pdf-editor.tsx
index dd9418a24..62fb9ade0 100644
--- a/apps/web/components/editor/pdf-editor.tsx
+++ b/apps/web/components/editor/pdf-editor.tsx
@@ -30,7 +30,7 @@ export default function PDFEditor(props: any) {
movedField.positionY = position.y.toFixed(0);
createOrUpdateField(props.document, movedField);
- // no instant redraw neccessary, postion information for saving or later rerender is enough
+ // no instant redraw neccessary, position information for saving or later rerender is enough
// setFields(newFields);
}
diff --git a/apps/web/components/editor/pdf-signer.tsx b/apps/web/components/editor/pdf-signer.tsx
index a3bb08389..392dd0508 100644
--- a/apps/web/components/editor/pdf-signer.tsx
+++ b/apps/web/components/editor/pdf-signer.tsx
@@ -71,7 +71,7 @@ export default function PDFSigner(props: any) {
-
+
Documenso
diff --git a/apps/web/components/forgot-password.tsx b/apps/web/components/forgot-password.tsx
new file mode 100644
index 000000000..8235a80d9
--- /dev/null
+++ b/apps/web/components/forgot-password.tsx
@@ -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
();
+ 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 (
+ <>
+
+
+
+
+
+ {resetSuccessful ? "Reset Password" : "Forgot Password?"}
+
+
+ {resetSuccessful
+ ? "Please check your email for reset instructions."
+ : "No worries, we'll send you reset instructions."}
+
+
+ {!resetSuccessful && (
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/apps/web/components/layout.tsx b/apps/web/components/layout.tsx
index 06a5bb2de..aa0fad8dc 100644
--- a/apps/web/components/layout.tsx
+++ b/apps/web/components/layout.tsx
@@ -3,11 +3,11 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { useSubscription } from "@documenso/lib/stripe";
+import { BillingWarning } from "./billing-warning";
import Navigation from "./navigation";
import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
import { SubscriptionStatus } from "@prisma/client";
import { useSession } from "next-auth/react";
-import { BillingWarning } from "./billing-warning";
function useRedirectToLoginIfUnauthenticated() {
const { data: session, status } = useSession();
diff --git a/apps/web/components/login.tsx b/apps/web/components/login.tsx
index 4f086a8e1..6c1ec2896 100644
--- a/apps/web/components/login.tsx
+++ b/apps/web/components/login.tsx
@@ -69,7 +69,7 @@ export default function Login(props: any) {
-
+
Sign in to your account
@@ -111,9 +111,11 @@ export default function Login(props: any) {
diff --git a/apps/web/components/logo.tsx b/apps/web/components/logo.tsx
index 64d61bc76..2534c95e1 100644
--- a/apps/web/components/logo.tsx
+++ b/apps/web/components/logo.tsx
@@ -8,71 +8,71 @@ export default function Logo(props: any) {
>
diff --git a/apps/web/components/navigation.tsx b/apps/web/components/navigation.tsx
index c09d5b912..7d0c4edfa 100644
--- a/apps/web/components/navigation.tsx
+++ b/apps/web/components/navigation.tsx
@@ -115,8 +115,7 @@ export default function TopNavigation() {
-
-
Documenso
+
diff --git a/apps/web/components/reset-password.tsx b/apps/web/components/reset-password.tsx
new file mode 100644
index 000000000..9f5f1d466
--- /dev/null
+++ b/apps/web/components/reset-password.tsx
@@ -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
;
+
+export default function ResetPassword() {
+ const router = useRouter();
+ const { token } = router.query;
+
+ const {
+ register,
+ formState: { errors, isSubmitting },
+ handleSubmit,
+ } = useForm({
+ 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 (
+ <>
+
+
+
+
+
+ Reset Password
+
+
+ {resetSuccessful ? "Your password has been reset." : "Please chose your new password"}
+
+
+ {!resetSuccessful && (
+
+ )}
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index c7b94279e..1ea48cefb 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -4,22 +4,15 @@ require("dotenv").config({ path: "../../.env" });
const nextConfig = {
reactStrictMode: true,
swcMinify: false,
+ transpilePackages: [
+ "@documenso/prisma",
+ "@documenso/lib",
+ "@documenso/ui",
+ "@documenso/pdf",
+ "@documenso/features",
+ "@documenso/signing",
+ "react-signature-canvas",
+ ],
};
-const transpileModules = require("next-transpile-modules")([
- "@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;
+module.exports = nextConfig;
diff --git a/apps/web/package.json b/apps/web/package.json
index a104e0fde..349936c8a 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -56,11 +56,10 @@
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"file-loader": "^6.2.0",
- "next-transpile-modules": "^10.0.0",
"postcss": "^8.4.19",
"sass": "^1.57.1",
"stripe-cli": "^0.1.0",
"tailwindcss": "^3.2.4",
"typescript": "4.8.4"
}
-}
\ No newline at end of file
+}
diff --git a/apps/web/pages/404.jsx b/apps/web/pages/404.jsx
index cbf22dea6..2c8ffe7ac 100644
--- a/apps/web/pages/404.jsx
+++ b/apps/web/pages/404.jsx
@@ -8,7 +8,7 @@ export default function Custom404() {
<>
-
+
Documenso
diff --git a/apps/web/pages/500.jsx b/apps/web/pages/500.jsx
index 3589da952..c90157fe4 100644
--- a/apps/web/pages/500.jsx
+++ b/apps/web/pages/500.jsx
@@ -1,15 +1,15 @@
+import Link from "next/link";
import { Button } from "@documenso/ui";
import Logo from "../components/logo";
import { ArrowSmallLeftIcon } from "@heroicons/react/20/solid";
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
-import Link from "next/link";
export default function Custom500() {
return (
<>
-
-
+
+
Documenso
diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx
index cd5541cbb..a1193a681 100644
--- a/apps/web/pages/_app.tsx
+++ b/apps/web/pages/_app.tsx
@@ -1,6 +1,7 @@
import { ReactElement, ReactNode } from "react";
import { NextPage } from "next";
import type { AppProps } from "next/app";
+import { Montserrat, Qwigley } from "next/font/google";
import { SubscriptionProvider } from "@documenso/lib/stripe/providers/subscription-provider";
import "../../../node_modules/placeholder-loading/src/scss/placeholder-loading.scss";
import "../../../node_modules/react-resizable/css/styles.css";
@@ -11,6 +12,20 @@ import "react-tooltip/dist/react-tooltip.css";
export { coloredConsole } from "@documenso/lib";
+const montserrat = Montserrat({
+ subsets: ["latin"],
+ weight: ["400", "700"],
+ display: "swap",
+ variable: "--font-sans",
+});
+
+const qwigley = Qwigley({
+ subsets: ["latin"],
+ weight: ["400"],
+ display: "swap",
+ variable: "--font-qwigley",
+});
+
export type NextPageWithLayout
= NextPage
& {
getLayout?: (page: ReactElement) => ReactNode;
};
@@ -27,8 +42,10 @@ export default function App({
return (
-
- {getLayout( )}
+
+
+ {getLayout( )}
+
);
diff --git a/apps/web/pages/api/auth/forgot-password.ts b/apps/web/pages/api/auth/forgot-password.ts
new file mode 100644
index 000000000..98e4a6676
--- /dev/null
+++ b/apps/web/pages/api/auth/forgot-password.ts
@@ -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) }),
+});
diff --git a/apps/web/pages/api/auth/reset-password.ts b/apps/web/pages/api/auth/reset-password.ts
new file mode 100644
index 000000000..78a81b7d4
--- /dev/null
+++ b/apps/web/pages/api/auth/reset-password.ts
@@ -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) }),
+});
diff --git a/apps/web/pages/api/auth/signup.ts b/apps/web/pages/api/auth/signup.ts
index b82bf5ea2..b67f1b50f 100644
--- a/apps/web/pages/api/auth/signup.ts
+++ b/apps/web/pages/api/auth/signup.ts
@@ -8,13 +8,13 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { email, password, source } = req.body;
const cleanEmail = email.toLowerCase();
- if (!cleanEmail || !cleanEmail.includes("@")) {
- res.status(422).json({ message: "Invalid email" });
+ if (!cleanEmail || !/.+@.+/.test(cleanEmail)) {
+ res.status(400).json({ message: "Invalid email" });
return;
}
if (!password || password.trim().length < 7) {
- return res.status(422).json({
+ return res.status(400).json({
message: "Password should be at least 7 characters long.",
});
}
diff --git a/apps/web/pages/api/documents/[id]/send.ts b/apps/web/pages/api/documents/[id]/send.ts
index 302e75001..ea3e91adc 100644
--- a/apps/web/pages/api/documents/[id]/send.ts
+++ b/apps/web/pages/api/documents/[id]/send.ts
@@ -6,53 +6,62 @@ import prisma from "@documenso/prisma";
import { Document as PrismaDocument, SendStatus } from "@prisma/client";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
- const user = await getUserFromToken(req, res);
- const { id: documentId } = req.query;
- const { resendTo: resendTo = [] } = req.body;
+ try {
+ const user = await getUserFromToken(req, res);
+ const { id: documentId } = req.query;
+ const { resendTo: resendTo = [] } = req.body;
- if (!user) return;
+ if (!user) {
+ return res.status(401).send("Unauthorized");
+ }
- if (!documentId) {
- res.status(400).send("Missing parameter documentId.");
- return;
- }
+ if (!documentId) {
+ return res.status(400).send("Missing parameter documentId.");
+ }
- const document: PrismaDocument = await getDocument(+documentId, req, res);
+ const document: PrismaDocument = await getDocument(+documentId, req, res);
- if (!document) res.status(404).end(`No document with id ${documentId} found.`);
+ if (!document) {
+ res.status(404).end(`No document with id ${documentId} found.`);
+ }
- let recipientCondition: any = {
- documentId: +documentId,
- sendStatus: SendStatus.NOT_SENT,
- };
-
- if (resendTo.length) {
- recipientCondition = {
+ let recipientCondition: any = {
documentId: +documentId,
- id: { in: resendTo },
+ sendStatus: SendStatus.NOT_SENT,
};
- }
- const recipients = await prisma.recipient.findMany({
- where: {
- ...recipientCondition,
- },
- });
+ if (resendTo.length) {
+ recipientCondition = {
+ documentId: +documentId,
+ id: { in: resendTo },
+ };
+ }
- if (!recipients.length) return res.status(200).send(recipients.length);
-
- let sentRequests = 0;
- recipients.forEach(async (recipient) => {
- await sendSigningRequest(recipient, document, user).catch((err) => {
- console.log(err);
- return res.status(502).end("Coud not send request for signing.");
+ const recipients = await prisma.recipient.findMany({
+ where: {
+ ...recipientCondition,
+ },
+ });
+
+ 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) {
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({
diff --git a/apps/web/pages/auth/reset/[token].tsx b/apps/web/pages/auth/reset/[token].tsx
new file mode 100644
index 000000000..33868f762
--- /dev/null
+++ b/apps/web/pages/auth/reset/[token].tsx
@@ -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 (
+ <>
+
+
Reset Password | Documenso
+
+
+ >
+ );
+}
+
+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: {},
+ };
+}
diff --git a/apps/web/pages/auth/reset/index.tsx b/apps/web/pages/auth/reset/index.tsx
new file mode 100644
index 000000000..f21145422
--- /dev/null
+++ b/apps/web/pages/auth/reset/index.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+import Logo from "../../../components/logo";
+
+export default function ResetPage() {
+ return (
+
+
+
+
+
+ Reset Password
+
+
+ The token you provided is invalid. Please try again.
+
+
+
+
+ );
+}
diff --git a/apps/web/pages/documents.tsx b/apps/web/pages/documents.tsx
index 905f59d09..cea08623c 100644
--- a/apps/web/pages/documents.tsx
+++ b/apps/web/pages/documents.tsx
@@ -4,6 +4,7 @@ import Head from "next/head";
import { useRouter } from "next/router";
import { uploadDocument } from "@documenso/features";
import { deleteDocument, getDocuments } from "@documenso/lib/api";
+import { useSubscription } from "@documenso/lib/stripe";
import { Button, IconButton, SelectBox } from "@documenso/ui";
import Layout from "../components/layout";
import type { NextPageWithLayout } from "./_app";
@@ -20,7 +21,6 @@ import {
} from "@heroicons/react/24/outline";
import { DocumentStatus } from "@prisma/client";
import { Tooltip as ReactTooltip } from "react-tooltip";
-import { useSubscription } from "@documenso/lib/stripe";
const DocumentsPage: NextPageWithLayout = (props: any) => {
const router = useRouter();
@@ -145,24 +145,24 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
-
-
- {filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
-
+
-
+
+
+ {filteredDocuments.length != 1 ? filteredDocuments.length + " Documents" : "1 Document"}
+
@@ -224,13 +224,13 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
{document.title || "#" + document.id}
-
+
{document.Recipient.map((item: any) => (
{item.sendStatus === "NOT_SENT" ? (
+ 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}
) : (
@@ -240,7 +240,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
+ 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">
{item.name ? item.name + " <" + item.email + ">" : item.email}
@@ -253,7 +253,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
+ 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">
{item.name ? item.name + " <" + item.email + ">" : item.email}
@@ -264,7 +264,7 @@ const DocumentsPage: NextPageWithLayout = (props: any) => {
)}
{item.signingStatus === "SIGNED" ? (
-
+
{" "}
{item.email}
diff --git a/apps/web/pages/documents/[id]/signed.tsx b/apps/web/pages/documents/[id]/signed.tsx
index bc11bc0a0..138f9e3e9 100644
--- a/apps/web/pages/documents/[id]/signed.tsx
+++ b/apps/web/pages/documents/[id]/signed.tsx
@@ -5,6 +5,7 @@ import prisma from "@documenso/prisma";
import { Button, IconButton } from "@documenso/ui";
import { NextPageWithLayout } from "../../_app";
import { ArrowDownTrayIcon, CheckBadgeIcon } from "@heroicons/react/24/outline";
+import { truncate } from "@documenso/lib/helpers";
const Signed: NextPageWithLayout = (props: any) => {
const router = useRouter();
@@ -21,7 +22,7 @@ const Signed: NextPageWithLayout = (props: any) => {
It's done!
- You signed "{props.document.title}"
+ You signed "{truncate(props.document.title)}"
You will be notfied when all recipients have signed.
diff --git a/apps/web/pages/forgot-password.tsx b/apps/web/pages/forgot-password.tsx
new file mode 100644
index 000000000..4591921a4
--- /dev/null
+++ b/apps/web/pages/forgot-password.tsx
@@ -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 (
+ <>
+
+ Forgot Password | Documenso
+
+
+ >
+ );
+}
+
+export async function getServerSideProps({ req }: GetServerSidePropsContext) {
+ const user = await getUserFromToken(req);
+
+ if (user)
+ return {
+ redirect: {
+ source: "/login",
+ destination: "/dashboard",
+ permanent: false,
+ },
+ };
+
+ return {
+ props: {},
+ };
+}
diff --git a/apps/web/ressources/certificate.p12 b/apps/web/resources/certificate.p12
similarity index 100%
rename from apps/web/ressources/certificate.p12
rename to apps/web/resources/certificate.p12
diff --git a/apps/web/ressources/example.pdf b/apps/web/resources/example.pdf
similarity index 100%
rename from apps/web/ressources/example.pdf
rename to apps/web/resources/example.pdf
diff --git a/apps/web/styles/tailwind.css b/apps/web/styles/tailwind.css
index 2ea56f9bf..fcae13ac3 100644
--- a/apps/web/styles/tailwind.css
+++ b/apps/web/styles/tailwind.css
@@ -6,35 +6,3 @@
min-height: 100%;
}
-html,
-body,
-:host {
- font-family: montserrat;
-}
-
-@font-face {
- font-family: "Qwigley";
- src: url("/fonts/Qwigley-Regular.ttf");
-}
-
-/* latin */
-@font-face {
- font-family: "Montserrat";
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: url("/fonts/montserrat.woff2") format("woff2");
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
- U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-
-/* latin */
-@font-face {
- font-family: "Montserrat";
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: url("/fonts/montserrat.woff2") format("woff2");
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
- U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js
index 89d211331..ee8e474dc 100644
--- a/apps/web/tailwind.config.js
+++ b/apps/web/tailwind.config.js
@@ -12,8 +12,8 @@ module.exports = {
theme: {
extend: {
fontFamily: {
- monteserrat: ["Monteserrat", "serif"],
- qwigley: ["Qwigley", "serif"],
+ sans: ["var(--font-sans)", ...defaultTheme.fontFamily.sans],
+ qwigley: ["var(--font-qwigley)", "serif"],
},
colors: {
neon: {
@@ -58,6 +58,19 @@ module.exports = {
900: "#000000",
950: "#000000",
},
+ brand: {
+ DEFAULT: "#A2E771",
+ 100: "#F4FCEE",
+ 200: "#E8F9DC",
+ 300: "#D1F3B9",
+ 400: "#BBED96",
+ 500: "#A2E771",
+ 600: "#8DE151",
+ 700: "#76DC2E",
+ 800: "#63C021",
+ 900: "#519D1B",
+ 950: "#488C18",
+ },
},
borderRadius: {
"4xl": "2rem",
diff --git a/docker/build.sh b/docker/build.sh
index 9a1ed88fb..aa2068910 100755
--- a/docker/build.sh
+++ b/docker/build.sh
@@ -22,7 +22,7 @@ echo "Git SHA: $GIT_SHA"
docker build -f "$SCRIPT_DIR/Dockerfile" \
--progress=plain \
- -t "documentso:latest" \
+ -t "documenso:latest" \
-t "documenso:$GIT_SHA" \
-t "documenso:$APP_VERSION" \
"$MONOREPO_ROOT"
diff --git a/docker/compose-without-app.yml b/docker/compose-without-app.yml
index e0b566e96..8b781a9a7 100644
--- a/docker/compose-without-app.yml
+++ b/docker/compose-without-app.yml
@@ -1,4 +1,3 @@
-name: documenso
services:
database:
image: postgres:15
diff --git a/package-lock.json b/package-lock.json
index 3294ac71b..20c623d5f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@documenso/prisma": "*",
"@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13",
+ "@hookform/resolvers": "^3.1.0",
"avatar-from-initials": "^1.0.3",
"bcryptjs": "^2.4.3",
"next": "13.2.4",
@@ -24,7 +25,8 @@
"react-dom": "18.2.0",
"react-hook-form": "^7.41.5",
"react-hot-toast": "^2.4.0",
- "react-signature-canvas": "^1.0.6"
+ "react-signature-canvas": "^1.0.6",
+ "zod": "^3.21.4"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
@@ -40,6 +42,7 @@
"npm-run-all": "^4.1.5",
"prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.5",
+ "turbo": "^1.9.9",
"typescript": "4.8.4"
}
},
@@ -92,7 +95,6 @@
"eslint": "8.27.0",
"eslint-config-next": "13.0.3",
"file-loader": "^6.2.0",
- "next-transpile-modules": "^10.0.0",
"postcss": "^8.4.19",
"sass": "^1.57.1",
"stripe-cli": "^0.1.0",
@@ -525,6 +527,14 @@
"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": {
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
@@ -633,36 +643,6 @@
"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": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz",
@@ -678,156 +658,6 @@
"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": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -7665,6 +7495,102 @@
"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": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@@ -8132,6 +8058,14 @@
"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": {
"name": "@documenso/features",
"version": "0.0.0"
@@ -8517,7 +8451,6 @@
"formidable": "^3.2.5",
"next": "13.2.4",
"next-auth": "^4.22.0",
- "next-transpile-modules": "^10.0.0",
"node-forge": "^1.3.1",
"node-signpdf": "^1.5.0",
"nodemailer": "^6.9.0",
@@ -8592,6 +8525,12 @@
"integrity": "sha512-x89rFxH3SRdYaA+JCXwfe+RkE1SFTo9GcOkZettHer71Y3T7V+ogKmfw5CjTazgS3d0ClJ7p1NA+SP7VQLQcLw==",
"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": {
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
@@ -8681,84 +8620,12 @@
"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": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz",
"integrity": "sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A==",
"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": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -13859,6 +13726,62 @@
"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": {
"version": "0.14.5",
"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",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
+ },
+ "zod": {
+ "version": "3.21.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
+ "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw=="
}
}
}
diff --git a/package.json b/package.json
index 9c2a55c14..b3e2c802a 100644
--- a/package.json
+++ b/package.json
@@ -2,15 +2,14 @@
"name": "documenso-monorepo",
"version": "0.0.0",
"scripts": {
- "dev": "npm run dev -w apps/web",
- "build": "npm i && cd apps && cd web && npm i && next build",
- "start": "cd apps && cd web && next start",
+ "dev": "turbo run dev --filter=web",
+ "build": "turbo run build --filter=web",
+ "start": "turbo run start --filter=web",
"db-migrate:dev": "prisma migrate dev",
"db-seed": "prisma db seed",
"db-studio": "prisma studio",
- "docker:compose": "docker-compose -f ./docker/compose-without-app.yml",
- "docker:compose-up": "npm run docker:compose -- up -d",
- "docker:compose-down": "npm run docker:compose -- down",
+ "docker:compose-up": "docker compose -p documenso -f ./docker/compose-without-app.yml up -d || docker-compose -p documenso -f ./docker/compose-without-app.yml up -d",
+ "docker:compose-down": "docker compose -p documenso -f ./docker/compose-without-app.yml down || docker-compose -p documenso -f ./docker/compose-without-app.yml down",
"stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook",
"dx": "npm install && run-s docker:compose-up db-migrate:dev",
"d": "npm install && run-s docker:compose-up db-migrate:dev && npm run db-seed && npm run dev"
@@ -27,6 +26,7 @@
"@documenso/prisma": "*",
"@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13",
+ "@hookform/resolvers": "^3.1.0",
"avatar-from-initials": "^1.0.3",
"bcryptjs": "^2.4.3",
"next": "13.2.4",
@@ -36,7 +36,8 @@
"react-dom": "18.2.0",
"react-hook-form": "^7.41.5",
"react-hot-toast": "^2.4.0",
- "react-signature-canvas": "^1.0.6"
+ "react-signature-canvas": "^1.0.6",
+ "zod": "^3.21.4"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
@@ -52,6 +53,7 @@
"npm-run-all": "^4.1.5",
"prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.5",
+ "turbo": "^1.9.9",
"typescript": "4.8.4"
}
-}
\ No newline at end of file
+}
diff --git a/packages/lib/helpers/index.ts b/packages/lib/helpers/index.ts
new file mode 100644
index 000000000..cf29797bb
--- /dev/null
+++ b/packages/lib/helpers/index.ts
@@ -0,0 +1 @@
+export * from './strings';
\ No newline at end of file
diff --git a/packages/lib/helpers/strings.ts b/packages/lib/helpers/strings.ts
new file mode 100644
index 000000000..eaf45b7f1
--- /dev/null
+++ b/packages/lib/helpers/strings.ts
@@ -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)}`;
+};
diff --git a/packages/lib/mail/baseTemplate.ts b/packages/lib/mail/baseTemplate.ts
index 6741a87b5..6e18c7114 100644
--- a/packages/lib/mail/baseTemplate.ts
+++ b/packages/lib/mail/baseTemplate.ts
@@ -1,10 +1,9 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
-import { Document as PrismaDocument } from "@prisma/client";
export const baseEmailTemplate = (message: string, content: string) => {
const html = `
-
+
${message}
${content}
diff --git a/packages/lib/mail/index.ts b/packages/lib/mail/index.ts
index 6d49cdb6b..e4d66dc44 100644
--- a/packages/lib/mail/index.ts
+++ b/packages/lib/mail/index.ts
@@ -2,3 +2,7 @@ export { signingRequestTemplate } from "./signingRequestTemplate";
export { signingCompleteTemplate } from "./signingCompleteTemplate";
export { sendSigningRequest as sendSigningRequest } from "./sendSigningRequest";
export { sendSigningDoneMail } from "./sendSigningDoneMail";
+export { resetPasswordTemplate } from "./resetPasswordTemplate";
+export { sendResetPassword } from "./sendResetPassword";
+export { resetPasswordSuccessTemplate } from "./resetPasswordSuccessTemplate";
+export { sendResetPasswordSuccessMail } from "./sendResetPasswordSuccessMail";
diff --git a/packages/lib/mail/resetPasswordSuccessTemplate.ts b/packages/lib/mail/resetPasswordSuccessTemplate.ts
new file mode 100644
index 000000000..25a0a9ff5
--- /dev/null
+++ b/packages/lib/mail/resetPasswordSuccessTemplate.ts
@@ -0,0 +1,51 @@
+import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
+import { User } from "@prisma/client";
+
+export const resetPasswordSuccessTemplate = (user: User) => {
+ return `
+
+
+
+
+
Password updated!
+
+
+ Hi ${user.name ? user.name : user.email},
+
+
+
+ We've changed your password as you asked. You can now sign in with your new password.
+
+
+
+ Didn't request a password change? We are here to help you secure your account, just contact us .
+
+
+
+
+ The Documenso Team
+
+
+
+
+ Want to send you own signing links?
+ Hosted Documenso is here! .
+
+
+
+
+
+
+
+ Easy and beautiful document signing by Documenso.
+
+
+`;
+};
+export default resetPasswordSuccessTemplate;
diff --git a/packages/lib/mail/resetPasswordTemplate.ts b/packages/lib/mail/resetPasswordTemplate.ts
new file mode 100644
index 000000000..b86b404fd
--- /dev/null
+++ b/packages/lib/mail/resetPasswordTemplate.ts
@@ -0,0 +1,46 @@
+import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
+
+export const resetPasswordTemplate = (ctaLink: string, ctaLabel: string) => {
+ const customContent = `
+
Forgot your password?
+
+ That's okay, it happens! Click the button below to reset your password.
+
+
+
+
+ ${ctaLabel}
+
+
+
+ Want to send you own signing links? Hosted Documenso is here! .
+
`;
+
+ const html = `
+
+
+
+ ${customContent}
+
+
+ `;
+
+ const footer = `
+
+
+
+
+ Easy and beautiful document signing by Documenso.
+
+
`;
+
+ return html + footer;
+};
+
+export default resetPasswordTemplate;
diff --git a/packages/lib/mail/sendMail.ts b/packages/lib/mail/sendMail.ts
index 101981f12..fd7c6fb61 100644
--- a/packages/lib/mail/sendMail.ts
+++ b/packages/lib/mail/sendMail.ts
@@ -1,4 +1,3 @@
-import { ReadStream } from "fs";
import nodemailer from "nodemailer";
import nodemailerSendgrid from "nodemailer-sendgrid";
diff --git a/packages/lib/mail/sendResetPassword.ts b/packages/lib/mail/sendResetPassword.ts
new file mode 100644
index 000000000..32e098a4c
--- /dev/null
+++ b/packages/lib/mail/sendResetPassword.ts
@@ -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;
+ });
+};
diff --git a/packages/lib/mail/sendResetPasswordSuccessMail.ts b/packages/lib/mail/sendResetPasswordSuccessMail.ts
new file mode 100644
index 000000000..6877700fb
--- /dev/null
+++ b/packages/lib/mail/sendResetPasswordSuccessMail.ts
@@ -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;
+ }
+ );
+};
diff --git a/packages/lib/mail/signingCompleteTemplate.ts b/packages/lib/mail/signingCompleteTemplate.ts
index 212e1f8ea..e32162906 100644
--- a/packages/lib/mail/signingCompleteTemplate.ts
+++ b/packages/lib/mail/signingCompleteTemplate.ts
@@ -1,6 +1,5 @@
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
import { baseEmailTemplate } from "./baseTemplate";
-import { Document as PrismaDocument } from "@prisma/client";
export const signingCompleteTemplate = (message: string) => {
const customContent = `
diff --git a/packages/lib/server/getServerErrorFromUnknown.ts b/packages/lib/server/getServerErrorFromUnknown.ts
index 8a4823082..fa53e8585 100644
--- a/packages/lib/server/getServerErrorFromUnknown.ts
+++ b/packages/lib/server/getServerErrorFromUnknown.ts
@@ -1,5 +1,5 @@
import { HttpError } from "@documenso/lib/server";
-import { NotFoundError, PrismaClientKnownRequestError } from "@prisma/client/runtime";
+import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
export function getServerErrorFromUnknown(cause: unknown): HttpError {
// Error was manually thrown and does not need to be parsed.
@@ -18,7 +18,7 @@ export function getServerErrorFromUnknown(cause: unknown): HttpError {
return new HttpError({ statusCode: 400, message: cause.message, cause });
}
- if (cause instanceof NotFoundError) {
+ if (cause instanceof PrismaClientKnownRequestError) {
return new HttpError({ statusCode: 404, message: cause.message, cause });
}
diff --git a/packages/lib/server/getUserFromToken.ts b/packages/lib/server/getUserFromToken.ts
index 0cb8b72bb..f38c4abd1 100644
--- a/packages/lib/server/getUserFromToken.ts
+++ b/packages/lib/server/getUserFromToken.ts
@@ -1,23 +1,17 @@
-import { NextApiRequest, NextApiResponse } from "next";
+import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next";
+import { NextRequest } from "next/server";
import prisma from "@documenso/prisma";
import { User as PrismaUser } from "@prisma/client";
import { getToken } from "next-auth/jwt";
-import { signOut } from "next-auth/react";
export async function getUserFromToken(
- req: NextApiRequest,
- res: NextApiResponse
+ req: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
+ res?: NextApiResponse // TODO: Remove this optional parameter
): Promise
{
const token = await getToken({ req });
const tokenEmail = token?.email?.toString();
- if (!token) {
- if (res.status) res.status(401).send("No session token found for request.");
- return null;
- }
-
- if (!tokenEmail) {
- res.status(400).send("No email found in session token.");
+ if (!token || !tokenEmail) {
return null;
}
@@ -26,7 +20,6 @@ export async function getUserFromToken(
});
if (!user) {
- if (res && res.status) res.status(401).end();
return null;
}
diff --git a/packages/lib/stripe/handlers/webhook.ts b/packages/lib/stripe/handlers/webhook.ts
index 390fc21de..609e652fe 100644
--- a/packages/lib/stripe/handlers/webhook.ts
+++ b/packages/lib/stripe/handlers/webhook.ts
@@ -25,9 +25,7 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
});
}
- log("constructing body...")
const body = await buffer(req);
- log("constructed body")
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
log("event-type:", event.type);
@@ -70,23 +68,38 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
if (event.type === "invoice.payment_succeeded") {
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 =
typeof invoice.customer === "string" ? invoice.customer : invoice.customer?.id;
const subscription = await stripe.subscriptions.retrieve(invoice.subscription as string);
- await prisma.subscription.update({
+ const hasSubscription = await prisma.subscription.findFirst({
where: {
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({
success: true,
message: "Webhook received",
@@ -98,15 +111,23 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
const customerId = failedInvoice.customer as string;
- await prisma.subscription.update({
+ const hasSubscription = await prisma.subscription.findFirst({
where: {
customerId,
},
- data: {
- status: SubscriptionStatus.PAST_DUE,
- },
});
+ if (hasSubscription) {
+ await prisma.subscription.update({
+ where: {
+ customerId,
+ },
+ data: {
+ status: SubscriptionStatus.PAST_DUE,
+ },
+ });
+ }
+
return res.status(200).json({
success: true,
message: "Webhook received",
@@ -118,18 +139,26 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
const customerId = updatedSubscription.customer as string;
- await prisma.subscription.update({
+ const hasSubscription = await prisma.subscription.findFirst({
where: {
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({
success: true,
message: "Webhook received",
@@ -141,15 +170,23 @@ export const webhookHandler = async (req: NextApiRequest, res: NextApiResponse)
const customerId = deletedSubscription.customer as string;
- await prisma.subscription.update({
+ const hasSubscription = await prisma.subscription.findFirst({
where: {
customerId,
},
- data: {
- status: SubscriptionStatus.INACTIVE,
- },
});
+ if (hasSubscription) {
+ await prisma.subscription.update({
+ where: {
+ customerId,
+ },
+ data: {
+ status: SubscriptionStatus.INACTIVE,
+ },
+ });
+ }
+
return res.status(200).json({
success: true,
message: "Webhook received",
diff --git a/packages/prisma/migrations/20230605122017_password_reset/migration.sql b/packages/prisma/migrations/20230605122017_password_reset/migration.sql
new file mode 100644
index 000000000..782a60880
--- /dev/null
+++ b/packages/prisma/migrations/20230605122017_password_reset/migration.sql
@@ -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;
diff --git a/packages/prisma/migrations/20230605164015_expire_password_reset_token/migration.sql b/packages/prisma/migrations/20230605164015_expire_password_reset_token/migration.sql
new file mode 100644
index 000000000..a3a70e575
--- /dev/null
+++ b/packages/prisma/migrations/20230605164015_expire_password_reset_token/migration.sql
@@ -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;
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 33fae736d..fc8463c9b 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -13,17 +13,18 @@ enum IdentityProvider {
}
model User {
- id Int @id @default(autoincrement())
- name String?
- email String @unique
- emailVerified DateTime?
- password String?
- source String?
- identityProvider IdentityProvider @default(DOCUMENSO)
- accounts Account[]
- sessions Session[]
- Document Document[]
- Subscription Subscription[]
+ id Int @id @default(autoincrement())
+ name String?
+ email String @unique
+ emailVerified DateTime?
+ password String?
+ source String?
+ identityProvider IdentityProvider @default(DOCUMENSO)
+ accounts Account[]
+ sessions Session[]
+ Document Document[]
+ Subscription Subscription[]
+ PasswordResetToken PasswordResetToken[]
}
enum SubscriptionStatus {
@@ -158,3 +159,12 @@ model Signature {
Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
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])
+}
diff --git a/packages/signing/addDigitalSignature.ts b/packages/signing/addDigitalSignature.ts
index f4f565c8f..8bd219bb5 100644
--- a/packages/signing/addDigitalSignature.ts
+++ b/packages/signing/addDigitalSignature.ts
@@ -10,7 +10,7 @@ export const addDigitalSignature = async (documentAsBase64: string): Promise void;
icon: React.ReactNode;
+ truncateTitle?: boolean;
};
export function Dialog({
@@ -29,11 +31,14 @@ export function Dialog({
formValues,
setLoading,
icon,
+ truncateTitle = true,
}: DialogProps) {
const unsentEmailsLength = formValues.filter(
(s: any) => s.email && s.sendStatus != "SENT"
).length;
+ const documentTitle = truncateTitle ? truncate(document.title) : document.title;
+
return (
@@ -71,7 +76,7 @@ export function Dialog({
- {`"${document.title}" will be sent to ${unsentEmailsLength} recipients.`}
+ {`"${documentTitle}" will be sent to ${unsentEmailsLength} recipients.`}
diff --git a/turbo.json b/turbo.json
new file mode 100644
index 000000000..ea51341b1
--- /dev/null
+++ b/turbo.json
@@ -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
+ }
+ }
+}