mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 12:32:34 +10:00
Compare commits
1 Commits
feat/passw
...
fix/improv
| Author | SHA1 | Date | |
|---|---|---|---|
| 19e960f593 |
@ -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 reset or taken offline at any point.
|
||||
# ⚠ WARNING: Please be aware that nothing written to the test database is private.
|
||||
# ⚠ 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.
|
||||
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=false
|
||||
NEXT_PUBLIC_ALLOW_SUBSCRIPTIONS=true
|
||||
43
README.md
43
README.md
@ -1,3 +1,10 @@
|
||||
<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">
|
||||
<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">
|
||||
@ -56,13 +63,6 @@
|
||||
|
||||
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 🎯
|
||||
|
||||
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:
|
||||
@ -198,32 +198,3 @@ Want to create a production ready docker image? Follow these steps:
|
||||
|
||||
- Docker support
|
||||
- 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
|
||||
- '::'
|
||||
```
|
||||
|
||||
@ -1,115 +0,0 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -111,11 +111,9 @@ export default function Login(props: any) {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm">
|
||||
<Link
|
||||
href="/forgot-password"
|
||||
className="hover:text-neon-700 font-medium text-gray-500">
|
||||
<a href="#" className="hover:text-neon-700 font-medium text-gray-500">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -1,143 +0,0 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
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) }),
|
||||
});
|
||||
@ -1,69 +0,0 @@
|
||||
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 cleanEmail = email.toLowerCase();
|
||||
|
||||
if (!cleanEmail || !/.+@.+/.test(cleanEmail)) {
|
||||
res.status(400).json({ message: "Invalid email" });
|
||||
if (!cleanEmail || !cleanEmail.includes("@")) {
|
||||
res.status(422).json({ message: "Invalid email" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password || password.trim().length < 7) {
|
||||
return res.status(400).json({
|
||||
return res.status(422).json({
|
||||
message: "Password should be at least 7 characters long.",
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
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: {},
|
||||
};
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
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: {},
|
||||
};
|
||||
}
|
||||
@ -22,7 +22,7 @@ echo "Git SHA: $GIT_SHA"
|
||||
|
||||
docker build -f "$SCRIPT_DIR/Dockerfile" \
|
||||
--progress=plain \
|
||||
-t "documenso:latest" \
|
||||
-t "documentso:latest" \
|
||||
-t "documenso:$GIT_SHA" \
|
||||
-t "documenso:$APP_VERSION" \
|
||||
"$MONOREPO_ROOT"
|
||||
|
||||
181
package-lock.json
generated
181
package-lock.json
generated
@ -15,7 +15,6 @@
|
||||
"@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",
|
||||
@ -25,8 +24,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.41.5",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-signature-canvas": "^1.0.6",
|
||||
"zod": "^3.21.4"
|
||||
"react-signature-canvas": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
@ -527,14 +525,6 @@
|
||||
"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",
|
||||
@ -3482,7 +3472,6 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
@ -7496,40 +7485,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/turbo": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.1.tgz",
|
||||
"integrity": "sha512-wq0YeSv6P/eEDXOL42jkMUr+T4z34dM8mdHu5u6C6OOAq8JuLJ72F/v4EVR1JmY8icyTkFz10ICLV0haUUYhbQ==",
|
||||
"version": "1.9.9",
|
||||
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.9.9.tgz",
|
||||
"integrity": "sha512-+ZS66LOT7ahKHxh6XrIdcmf2Yk9mNpAbPEj4iF2cs0cAeaDU3xLVPZFF0HbSho89Uxwhx7b5HBgPbdcjQTwQkg==",
|
||||
"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"
|
||||
"turbo-darwin-64": "1.9.9",
|
||||
"turbo-darwin-arm64": "1.9.9",
|
||||
"turbo-linux-64": "1.9.9",
|
||||
"turbo-linux-arm64": "1.9.9",
|
||||
"turbo-windows-64": "1.9.9",
|
||||
"turbo-windows-arm64": "1.9.9"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.9.9",
|
||||
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.9.9.tgz",
|
||||
"integrity": "sha512-VyfkXzTJpYLTAQ9krq2myyEq7RPObilpS04lgJ4OO1piq76RNmSpX9F/t9JCaY9Pj/4TL7i0d8PM7NGhwEA5Ag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@ -7539,58 +7515,6 @@
|
||||
"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",
|
||||
@ -8058,14 +7982,6 @@
|
||||
"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"
|
||||
@ -8525,12 +8441,6 @@
|
||||
"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",
|
||||
@ -10879,7 +10789,6 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
@ -13727,58 +13636,23 @@
|
||||
}
|
||||
},
|
||||
"turbo": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.1.tgz",
|
||||
"integrity": "sha512-wq0YeSv6P/eEDXOL42jkMUr+T4z34dM8mdHu5u6C6OOAq8JuLJ72F/v4EVR1JmY8icyTkFz10ICLV0haUUYhbQ==",
|
||||
"version": "1.9.9",
|
||||
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.9.9.tgz",
|
||||
"integrity": "sha512-+ZS66LOT7ahKHxh6XrIdcmf2Yk9mNpAbPEj4iF2cs0cAeaDU3xLVPZFF0HbSho89Uxwhx7b5HBgPbdcjQTwQkg==",
|
||||
"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": "1.9.9",
|
||||
"turbo-darwin-arm64": "1.9.9",
|
||||
"turbo-linux-64": "1.9.9",
|
||||
"turbo-linux-arm64": "1.9.9",
|
||||
"turbo-windows-64": "1.9.9",
|
||||
"turbo-windows-arm64": "1.9.9"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.9.9",
|
||||
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.9.9.tgz",
|
||||
"integrity": "sha512-VyfkXzTJpYLTAQ9krq2myyEq7RPObilpS04lgJ4OO1piq76RNmSpX9F/t9JCaY9Pj/4TL7i0d8PM7NGhwEA5Ag==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
@ -14121,11 +13995,6 @@
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
package.json
11
package.json
@ -8,8 +8,9 @@
|
||||
"db-migrate:dev": "prisma migrate dev",
|
||||
"db-seed": "prisma db seed",
|
||||
"db-studio": "prisma studio",
|
||||
"docker:compose-up": "docker compose -f ./docker/compose-without-app.yml up -d || docker-compose -f ./docker/compose-without-app.yml up -d",
|
||||
"docker:compose-down": "docker compose -f ./docker/compose-without-app.yml down || docker-compose -f ./docker/compose-without-app.yml down",
|
||||
"docker:compose": "docker compose -f ./docker/compose-without-app.yml || 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",
|
||||
"stripe:listen": "stripe listen --forward-to localhost:3000/api/stripe/webhook",
|
||||
"dx": "npm install && run-s docker:compose-up db-migrate:dev",
|
||||
"d": "npm install && run-s docker:compose-up db-migrate:dev && npm run db-seed && npm run dev"
|
||||
@ -26,7 +27,6 @@
|
||||
"@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,8 +36,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.41.5",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-signature-canvas": "^1.0.6",
|
||||
"zod": "^3.21.4"
|
||||
"react-signature-canvas": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
@ -56,4 +55,4 @@
|
||||
"turbo": "^1.9.9",
|
||||
"typescript": "4.8.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from "../constants";
|
||||
import { Document as PrismaDocument } from "@prisma/client";
|
||||
|
||||
export const baseEmailTemplate = (message: string, content: string) => {
|
||||
const html = `
|
||||
<div style="background-color: #eaeaea; padding: 2%;">
|
||||
<div style="text-align:center; margin: auto; font-size: 14px; color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
|
||||
<div style="text-align:center; margin: auto; font-size: 14px; font-color: #353434; max-width: 500px; border-radius: 0.375rem; background: white; padding: 50px">
|
||||
<img src="${NEXT_PUBLIC_WEBAPP_URL}/logo_h.png" alt="Documenso Logo" style="width: 180px; display: block; margin: auto; margin-bottom: 14px;">
|
||||
${message}
|
||||
${content}
|
||||
|
||||
@ -2,7 +2,3 @@ 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";
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
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;
|
||||
@ -1,46 +0,0 @@
|
||||
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,3 +1,4 @@
|
||||
import { ReadStream } from "fs";
|
||||
import nodemailer from "nodemailer";
|
||||
import nodemailerSendgrid from "nodemailer-sendgrid";
|
||||
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
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;
|
||||
});
|
||||
};
|
||||
@ -1,11 +0,0 @@
|
||||
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,5 +1,6 @@
|
||||
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 = `
|
||||
|
||||
@ -1,17 +1,23 @@
|
||||
import { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next";
|
||||
import { NextRequest } from "next/server";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
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: GetServerSidePropsContext["req"] | NextRequest | NextApiRequest,
|
||||
res?: NextApiResponse // TODO: Remove this optional parameter
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
): Promise<PrismaUser | null> {
|
||||
const token = await getToken({ req });
|
||||
const tokenEmail = token?.email?.toString();
|
||||
|
||||
if (!token || !tokenEmail) {
|
||||
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.");
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -20,6 +26,7 @@ export async function getUserFromToken(
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
if (res && res.status) res.status(401).end();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
-- 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;
|
||||
@ -1,8 +0,0 @@
|
||||
/*
|
||||
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,18 +13,17 @@ 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[]
|
||||
PasswordResetToken PasswordResetToken[]
|
||||
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[]
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
@ -159,12 +158,3 @@ 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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user